Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

di il
26 risposte

26 Risposte - Pagina 2

  • Re: Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

    iBaffiPro ha scritto:


    Se lascio tutto così sono costretto a buttare la WebApp nel cestino, è inutilizzabile.
    Quello che hai fatto finora non è sicuramente qualcosa di "serio" o comunque "commerciale"/"professionale". E immagino non sia fatto nemmeno benissimo (se ricordi, es., avevi usato quel spring.main.allow-circular-references che in generale non sarebbe comunque da usare).
    Quindi non è che devi per forza "buttare via" qualcosa. Si tratterebbe solo di sospendere un po' questo sviluppo e affrontare bene e meglio i concetti di Java. Ti sorprenderesti di quante cose ci siano da acquisire su Java e facendo un discreto e valido percorso otterresti una visione sul mondo Java che sarebbe molto differente da quella che hai adesso. Ti renderesti davvero conto di quello che hai scritto (!) e, per dire, potresti poi anche essere in grado di RI-scrivere quella webapp in 1/20 del tempo e magari 3 volte meglio.
  • Re: Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

    Questa è un'altra mia soluzione, più vicina ai consigli di AndBin, che funziona ma non so come gestire, ovvero non so come permettere che quel cancel(true) venga lanciato dal client e non dal programma.
    Nella nuova classe:
    
    @Service
    public class EseguiTestServerAsincrono {
        @Autowired
        EseguiTestServer eseguiTestServer;
        @Async
        public Future<EseguiTestServer> testCancelFuture(Utente utenteAutenticato, TestServer testServer) throws InterruptedException  {
            eseguiTestServer.esegui(utenteAutenticato,testServer);
            return new AsyncResult<EseguiTestServer>(eseguiTestServer);
        }
    }
    
    Nel controller:
    
        @Autowired
        EseguiTestServerAsincrono eseguiTestServerAsincrono;
    
        // Vedi sopra
        @RequestMapping(value = "/test-server", method = RequestMethod.POST)
        public String testServerPOST(
                ...
        ) {
            try {
                ...
                Future<EseguiTestServer> test = eseguiTestServerAsincrono.testCancelFuture(utenteAutenticato,testServerDalForm);
                Thread.sleep(5555);
                test.cancel(true);
                ...
                return "/test-server";
            }catch (Exception e){
                ...
                return "/test-server";
            }
        }
    
    In EseguiTestServer mi sono limitatto ad aggiungere il famoso Thread.currentThread().isInterrupted().
    Se nella pagina html aggiungo questo:
    
    <form th:action="@{/test-server}" th:object="${InterrompiAsincrono}" method="POST">
                        <input type="hidden" th:field="*{interrompi}" value="1">
                        <input type="submit" value="Interrompi il test">
    </form>
    
    e poi nel controller scrivo questo:
    
                Future<EseguiTestServer> test = eseguiTestServerAsincrono.testCancelFuture(utenteAutenticato,testServerDalForm);
                if(interrompiAsincrono!=null && interrompiAsincrono.getInterrompi()==1){
                    test.cancel(true);
                }
    
    non interrompo, giustamente, un ben nulla.
    P.S.: Il problema del spring.main.allow-circular-references l'ho risolto. Voglio finire questo 'lavoro' e poi approfondire meglio Java, questo si, ma prima voglio concluderlo. Non ho più molte cose da sistemare. Questa purtroppo è essenziale.
  • Re: Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

    iBaffiPro ha scritto:


    Questa è un'altra mia soluzione, più vicina ai consigli di AndBin, che funziona ma non so come gestire, ovvero non so come permettere che quel cancel(true) venga lanciato dal client e non dal programma.
    No, innanzitutto ci sono alcune cose da chiarire.

    1) Nel java.util.concurrent.Future<T> il T è la type variable che denota il tipo del risultato della computazione asincrona. Non ha molto senso che sia (nel tuo codice sopra) EseguiTestServer, poiché questo, essendo autowired, è un bean singleton.

    2) Se non ti interessa un valore di ritorno, si può mettere Future<?> e poi restituire:
    return new AsyncResult<>(null); // null = non usato

    Se poi vuoi che ad ogni utente ci sia associato MAX 1 task, allora devi mantenere una "mappa" username-->future del tipo:

    Map<String, Future<Xyz>>

    (Xyz un tipo preciso oppure ? come dicevo prima se non interessa il risultato)

    La implementazione concreta della map può essere un semplice HashMap MA attenzione, devi usare un oggetto di lock perché HashMap NON è thread-safe e inoltre dovrai fare delle sequenze di operazioni "composte" sulla map (quelle del tipo "se X fai Y" che devono essere "atomiche").

    Ragiona quindi su cosa voglia dire che:
    - per l'utente X c'è già un Future
    - per l'utente X non c'è già un Future

    iBaffiPro ha scritto:


    Voglio finire questo 'lavoro' e poi approfondire meglio Java, questo si, ma prima voglio concluderlo. Non ho più molte cose da sistemare. Questa purtroppo è essenziale.
    Va bene, termina pure questa applicazione ma non inventarti ulteriori nuove feature, che allungherebbero solo i tempi. Prendi questa sorta di "esercitazione" un po' come se un amico ti avesse prestato per qualche mese la sua Ferrari, che hai provato per un po' senza saperla guidare bene. Hai fatto delle prove con Spring Boot, hai visto le (tante) difficoltà .... ora sarà poi il caso di fare un percorso di studio più appropriato.
  • Re: Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

    Grazie per il tuo prezziosissimo aiuto.
    Intendi una cosa di questo tipo?
    
    @Service
    public class EseguiTestServerAsincrono {
    
        @Autowired
        EseguiTestServer eseguiTestServer;
    
        public Future<?> eseguiAsincrono(Utente utenteAutenticato, TestServer testServer) throws InterruptedException  {
            eseguiTestServer.esegui(utenteAutenticato,testServer);
            return new AsyncResult<>(null);
        }
    
        public HashMap<String, Future<?>> mappa = new HashMap<>();
    
        @Async
        public void eseguiTest(
                Utente utenteAutenticato,
                TestServer testServer,
                InterrompiAsincrono interrompiAsincrono
        ) throws InterruptedException {
            try{
                if(
                        interrompiAsincrono!=null &&
                                interrompiAsincrono.getInterrompi()!=null &&
                                interrompiAsincrono.getInterrompi().equals("si")){
                    if(mappa.containsKey(utenteAutenticato.getNome()))
                        mappa.get(utenteAutenticato.getNome()).cancel(true);
                }else {
                    if(mappa.containsKey(utenteAutenticato.getNome())){
                        mappa.get(utenteAutenticato.getNome()).cancel(true);
                        mappa.put(utenteAutenticato.getNome(),eseguiAsincrono(utenteAutenticato,testServer));
                    }else {
                        mappa.put(utenteAutenticato.getNome(),eseguiAsincrono(utenteAutenticato,testServer));
                    }
                }
            }catch (Exception e){
                System.out.println("Non è stato possibile eseguire l'operazione richiesta.");
                System.out.println(e.getCause());
                System.out.println(e.getMessage());
            }
    
        }
    
    }
    
    
    package it.applicazionijava.gestioneutenti.metodi_asincroni;
    
    public class InterrompiAsincrono {
        String interrompi;
    
        public String getInterrompi() {
            return interrompi;
        }
    
        public void setInterrompi(String interrompi) {
            this.interrompi = interrompi;
        }
    }
    
    
    
            <div class="row">
                <div class="col-lg-12 m-0 p-2">
                    <form th:action="@{/test-server}" th:object="${InterrompiAsincrono}" method="POST">
                        <input th:field="*{interrompi}">
                        <input type="submit" value="Interrompi il test" class="btn btn-primary">
                    </form>
                </div>
            </div>
    
    
        @Autowired
        EseguiTestServerAsincrono eseguiTestServerAsincrono;
    
        // Vedi sopra
        @RequestMapping(value = "/test-server", method = RequestMethod.POST)
        public String testServerPOST(
                Model model,
                Principal principal,
                HttpSession session,
                HttpServletRequest request,
                HttpServletResponse response,
                @ModelAttribute("TestServer") TestServer testServerDalForm,
                @ModelAttribute("InterrompiAsincrono") InterrompiAsincrono interrompiAsincronoDalForm
        ) {
            try {
    	    ...
                eseguiTestServerAsincrono.eseguiTest(utenteAutenticato,testServerDalForm,interrompiAsincronoDalForm);
    	    ...
                return "/test-server";
            }catch (Exception e){
    	    ...
                return "/test-server";
            }
        }
    
    Tra i cicli for:
    
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("1) Questo processo è stato interrotto.");
                    break;
                }
    
    Alla fine dei cicli for (il metodo sincrono non lo butto via e desidero continuare ad utilizzarlo per i casi più semplici così faccio un 'return null' invece che 'return' ma su questo non prestare attenzione):
    
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("2) Questo processo è stato interrotto.");
                    return null;
                }
    
    Io ottengo lo stesso problema che avevo con il mio approccio, non riesco a fermare questo dannatissimo metodo asincrono.
    Inoltre, se chiudo il browser e poi lo riavvio oppure semplicemente cambiando pagina, la variabile 'mappa' resta nella RAM oppure viene cancellata lasciando il metodo 'EseguiTestServerAsincrono' in esecuzione? Temo la seconda opzione...
  • Re: Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

    iBaffiPro ha scritto:


    Intendi una cosa di questo tipo?
    
    @Service
    public class EseguiTestServerAsincrono {
    
        @Autowired
        EseguiTestServer eseguiTestServer;
    
        public Future<?> eseguiAsincrono(Utente utenteAutenticato, TestServer testServer) throws InterruptedException  {
            eseguiTestServer.esegui(utenteAutenticato,testServer);
            return new AsyncResult<>(null);
        }
    
        public HashMap<String, Future<?>> mappa = new HashMap<>();
    
        @Async
        public void eseguiTest(
                Utente utenteAutenticato,
                TestServer testServer,
                InterrompiAsincrono interrompiAsincrono
        ) throws InterruptedException {
            try{
                if(
                        interrompiAsincrono!=null &&
                                interrompiAsincrono.getInterrompi()!=null &&
                                interrompiAsincrono.getInterrompi().equals("si")){
                    if(mappa.containsKey(utenteAutenticato.getNome()))
                        mappa.get(utenteAutenticato.getNome()).cancel(true);
                }else {
                    if(mappa.containsKey(utenteAutenticato.getNome())){
                        mappa.get(utenteAutenticato.getNome()).cancel(true);
                        mappa.put(utenteAutenticato.getNome(),eseguiAsincrono(utenteAutenticato,testServer));
                    }else {
                        mappa.put(utenteAutenticato.getNome(),eseguiAsincrono(utenteAutenticato,testServer));
                    }
                }
            }catch (Exception e){
                System.out.println("Non è stato possibile eseguire l'operazione richiesta.");
                System.out.println(e.getCause());
                System.out.println(e.getMessage());
            }
    
        }
    
    }
    
    No assolutamente, parecchio sbagliato.

    Innanzitutto è il metodo eseguiAsincrono che dovrebbe essere @Async, perché è lui che ritorna il Future. Non eseguiTest.

    Inoltre la logica che usa la map è "fumosa" e non hai applicato alcun locking quindi non c'è nemmeno garanzia di visibilità delle modifiche e mutua-esclusione tra diversi thread.

    E anche se mettessi il @Async nel punto giusto NON ti funzionerebbe lo stesso, per un concetto che non conosci.
    In Spring il @Async viene implementato tramite proxy, ovvero funziona asincrono SOLO se è invocato dall' "esterno" del bean.
    Cioè: la classe bean A ha un metodo x() @Async; il bean A lo inietti nel bean B, quindi in B un a.x() è asincrono.
    Perché in B viene iniettato non direttamente il bean A ma un proxy al bean A che ha la "macchineria" per gestire il funzionamento asincrono.

    Se la chiamata al @Async è fatta dall'interno della medesima classe, non funziona, cioè ... è sincrono come un normale metodo!


    Guarda, vedo di scrivere io un esempio, ma non so se riesco già di domani.
    Perché altrimenti temo che non ci arrivi neanche per Pasqua (che in questo momento non so che giorno cade quest'anno ... devo vedere il calendario )
  • Re: Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

    Grazie AndBin, Il metodo @Async è sempre stato su eseguiAsincrono poi l'ho spostato per vedere se cambiava qualcosa ma non è cambiato nulla come da tua previsione. Non immaginavo che fosse così complesso questo problema. Penso che la tua previsione sul fatto che non ci arrivi neppure per Pasqua sia da approfondire. Se volevi dire Pasqua 2023 allora si, posso concordare ma prima no di certo.
    Pasqua 2022 è domenica 17 aprile, praticamente tra 11 soli giorni.
  • Re: Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

    Ho aggiunto in locking di Map come mi hai giustamente suggerito e ti chiedo solo 2 rapide conferme:
    A) Quando un thread sta editando l'oggetto 'mappa' nessun altro thread può leggerlo oppure editarlo, giusto?
    B) Quando un thread sta leggendo l'oggetto 'mappa' nessun altro thread può editarlo ma tutti gli altri thread possono leggerlo, giusto?
    Se avvio il metodo ottengo questo:
    04
    05
    06
    In pratica quando il codice arriva qui tutto si ferma e 'mappa' non viene aggiornato.
    Se clicco su 'interrompi' nella relativa vista html ottengo:
    01
    02
    ma non interrompo un bel nulla perché l'oggetto 'mappa' è vuoto.
    C) Come faccio a riempire questo oggetto?
    Ti posto tutto il codice della classe.
    
    @Service
    // In questa classe vengono eseguiti i processi avviati dalla pagina "/test".
    public class EseguiTestServerAsincrono {
    
        @Autowired
        ...
    
        // Queste variabili contengono l'esito del metodo eseguiSincrono().
        private String SuccessoCompilazione = null;
        private String ErroreCompilazione = null;
        private String SuccessoInterruzione = null;
        private String ErroreInterruzione = null;
    
        // Questa variabile contiene l'output di eseguiSincrono().
        private String risultatoTest = null;
    
        // Questa variabile viene settata su false quando si verifica un'eccezione.
        private boolean test = true;
    
        // OGGETTO MAP
        public Map<String, Future<?>> mappa = new HashMap<>();
    
        // LOCKING DI MAP
        private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    
        // LOCKING DI MAP
        private void modificaLaMappa(String nomeUtente, Future<?> metodoAsincrono, String operazione) {
            reentrantReadWriteLock.writeLock().lock();
            try {
                if(operazione.equals("aggiungi")){
                    mappa.put(nomeUtente,metodoAsincrono); // Si aggiunge un nuovo elemento utente/metodo
                }else if(operazione.equals("rimuovi")){
                    mappa.remove(nomeUtente); // Si rimuove un utente e quindi anche il suo metodo associato
                }else if(operazione.equals("modifica")){
                    mappa.replace(nomeUtente,metodoAsincrono); // Si modifica un metodo di un elemento
                }
            }
            finally {
                reentrantReadWriteLock.writeLock().unlock();
            }
        }
    
        // LOCKING DI MAP
        private Future<?> ottieniMetodoMappa(String nomeUtente) {
            reentrantReadWriteLock.readLock().lock();
            Future<?> metodoAsincrono;
            try {
                metodoAsincrono = mappa.get(nomeUtente);
            } finally {
                reentrantReadWriteLock.readLock().unlock();
            }
            return metodoAsincrono;
        }
    
        // LOCKING DI MAP
        private boolean verificaPresenzaOggettoMappa(String nomeUtente) {
            reentrantReadWriteLock.readLock().lock();
            boolean esiste = false;
            try {
                esiste = mappa.containsKey(nomeUtente);
            } finally {
                reentrantReadWriteLock.readLock().unlock();
            }
            return esiste;
        }
    
        // Messaggi di output della classe.
        private String processoEseguito = "I processi sono stati eseguiti correttamente.";
        private String processoNonEseguito = "I processi non sono stati eseguiti correttamente.";
        private String processoInterrotto = "Il processo è stato interrotto";
        private String processoNonInterrotto = "Il processo non è stato interrotto";
    
        // Metodo per resettare le variabili della classe prima di ogni riavvio del metodo esegui().
        private void resetVariabili(){
            test = true;
            risultatoTest = null;
            SuccessoCompilazione = null;
            ErroreCompilazione = null;
            SuccessoInterruzione = null;
            ErroreInterruzione = null;
        }
    
        // Questo è il metodo principale della classe.
        // Classe con l'annotazione @Controller > metodo contrassegnato da @RequestMapping > metodo esegui()
        public void esegui(
                Utente utenteAutenticato,
                TestServerAsincrono testServerAsincrono,
                InterrompiAsincrono interrompiAsincrono
        ){
            try{
                if(
                        interrompiAsincrono!=null &&
                                interrompiAsincrono.getInterrompi()!=null &&
                                interrompiAsincrono.getInterrompi().equals("si")
                ){
                    System.out.println("01");
                    if(interrompiEseguiAsincrono(utenteAutenticato)){
                        System.out.println("02");
                        SuccessoInterruzione = processoInterrotto;
                        ErroreInterruzione = null;
                    }else {
                        System.out.println("03");
                        SuccessoInterruzione = null;
                        ErroreInterruzione = processoNonInterrotto;
                    }
                }else {
                    System.out.println("04");
                    if(
                            testServerAsincrono!=null &&
                                    testServerAsincrono.getMetodoDiEsecuzione()!=null &&
                                    testServerAsincrono.getMetodoDiEsecuzione().equals("asincrono")
                    ){
                        System.out.println("05");
                        if(interrompiEseguiAsincrono(utenteAutenticato)){
                            System.out.println("06");
                            modificaLaMappa(utenteAutenticato.getNome(),eseguiAsincrono(utenteAutenticato,testServerAsincrono),"aggiungi");
                            System.out.println("qui il codice non arriva...");
                        }else {
                            System.out.println("07");
                            risultatoTest = null;
                            SuccessoCompilazione = null;
                            ErroreCompilazione = processoNonEseguito;
                        }
                    }else if(
                            testServerAsincrono!=null &&
                                    testServerAsincrono.getMetodoDiEsecuzione()!=null &&
                                    testServerAsincrono.getMetodoDiEsecuzione().equals("sincrono")
                    ){
                        System.out.println("08");
                        if(interrompiEseguiAsincrono(utenteAutenticato)){
                            System.out.println("09");
                            eseguiSincrono(utenteAutenticato, testServerAsincrono);
                        }else {
                            System.out.println("10");
                            risultatoTest = null;
                            SuccessoCompilazione = null;
                            ErroreCompilazione = processoNonEseguito;
                        }
                    }else{
                        System.out.println("11");
                        risultatoTest = null;
                        SuccessoCompilazione = null;
                        ErroreCompilazione = processoNonEseguito;
                    }
                }
            }catch (Exception e){
                System.out.println("12");
                test = false;
                risultatoTest = null;
                SuccessoCompilazione = null;
                ErroreCompilazione = processoNonEseguito;
            }
        }
    
        // Questo metodo rende eseguiSincrono() un metodo asincrono.
        @Async
        public Future<?> eseguiAsincrono(Utente utenteAutenticato, TestServerAsincrono testServerAsincrono) {
            try{
                eseguiSincrono(utenteAutenticato,testServerAsincrono);
                return new AsyncResult<>(null);
            }catch (Exception e){
                test = false;
                risultatoTest = null;
                SuccessoCompilazione = null;
                ErroreCompilazione = processoNonEseguito;
                return null;
            }
        }
    
        // Questo metodo serve per interrompere il metodo eseguiAsincrono()
        public boolean interrompiEseguiAsincrono(Utente utenteAutenticato){
            try{
                if(
                        utenteAutenticato!=null &&
                                livelliDeiRuoli.utenteConRuoloMassimo(utenteAutenticato)
                ){
                    if(verificaPresenzaOggettoMappa(utenteAutenticato.getNome())){
                        Future<?> metodo = ottieniMetodoMappa(utenteAutenticato.getNome());
                        metodo.cancel(true);
                        modificaLaMappa(utenteAutenticato.getNome(),metodo,"modifica");
                        if(ottieniMetodoMappa(utenteAutenticato.getNome()).isCancelled()){
                            modificaLaMappa(utenteAutenticato.getNome(),null,"rimuovi");
                            if(verificaPresenzaOggettoMappa(utenteAutenticato.getNome())){
                                return false;
                            }else {
                                return true;
                            }
                        }else {
                            return false;
                        }
                    }else {
                        return true;
                    }
                }else {
                    return false;
                }
            }catch (Exception e){
                test = false;
                return false;
            }
        }
    
        // Metodo che avvia tutti i test in modalità sincrona.
        public void eseguiSincrono(Utente utenteAutenticato, TestServerAsincrono testServerAsincrono){
            // ...
        }
    
        // Metodi GET... e SET... per recuperare l'esito del processo esegui():
    
        public String getSuccessoCompilazione() {
            return SuccessoCompilazione;
        }
    
        public void setSuccessoCompilazione(String successoCompilazione) {
            SuccessoCompilazione = successoCompilazione;
        }
    
        public String getErroreCompilazione() {
            return ErroreCompilazione;
        }
    
        public void setErroreCompilazione(String erroreCompilazione) {
            ErroreCompilazione = erroreCompilazione;
        }
    
        public String getSuccessoInterruzione() {
            return SuccessoInterruzione;
        }
    
        public void setSuccessoInterruzione(String successoInterruzione) {
            SuccessoInterruzione = successoInterruzione;
        }
    
        public String getErroreInterruzione() {
            return ErroreInterruzione;
        }
    
        public void setErroreInterruzione(String erroreInterruzione) {
            ErroreInterruzione = erroreInterruzione;
        }
    
        public String getRisultatoTest() {
            return risultatoTest;
        }
    
        public void setRisultatoTest(String risultatoTest) {
            this.risultatoTest = risultatoTest;
        }
    
        public boolean isTest() {
            return test;
        }
    
        public void setTest(boolean test) {
            this.test = test;
        }
    
        public Map<String, Future<?>> getMappa() {
            return mappa;
        }
    
        public void setMappa(Map<String, Future<?>> mappa) {
            this.mappa = mappa;
        }
    }
    
    
    D) Inoltre c'è un altro problema: come monitoro il metodo asincrono? Se dopo 1 ora l'utente ritorna nella pagina dovrebbe capire se il processo è in esecuzione oppure terminato. Questo oggetto 'mappa', ai fini del monitoraggio, può avere senso metterlo nel @Controller?
  • Re: Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

    iBaffiPro ha scritto:


    Ho aggiunto in locking di Map come mi hai giustamente suggerito
    L'uso di un ReentrantReadWriteLock sì, è un locking. Ma probabilmente è un po' troppo. E' comunque tutto abbastanza "fumoso" (in particolare il esegui) e infine, in ogni caso, il @Async continua a NON funzionarti, per lo stesso motivo già detto prima.
    Il @Async viene gestito tramite proxy. Se il metodo @Async lo invochi tu dalla stessa classe in cui è definito il @Async, NON passi attraverso il proxy.

    iBaffiPro ha scritto:


    Questo oggetto 'mappa', ai fini del monitoraggio, può avere senso metterlo nel @Controller?
    No
  • Re: Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

    Anche mettendo @Async su esegui() il discorso non cambia e la domanda resta la stessa: come fermo questo metodo?
    Non capisco bene cosa intendi per "fumoso". Io ho una pagina html con 2 form, uno l'ho creato per far partire il metodo asincrono e l'altro per terminarlo su volere del client. Un form funziona e l'altro no.
    Non so più che pesci pigliare...
  • Re: Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

    Ok, non sono riuscito prima per via di miei impegni vari ma ecco un esempio più valido.

    Innanzitutto un @Service con un metodo @Async che esegue un task.
    package xyz;
    
    import java.math.BigInteger;
    import java.util.concurrent.Future;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.scheduling.annotation.AsyncResult;
    import org.springframework.stereotype.Service;
    
    @Service
    public class MyService {
        private static final Logger logger = LoggerFactory.getLogger(MyService.class);
    
        @Async
        public Future<Void> myAsyncTask(String user) {
            logger.info("User {}: INIZIO TASK", user);
    
            try {
                for (int i = 0; i < 10; i++) {
                    logger.info("User {}: sleep 4sec", user);
                    Thread.sleep(4000);
    
                    logger.info("User {}: calcoli", user);
                    BigInteger bi = BigInteger.TEN;
                    for (int n = 0; n < 1000000; n++) {
                        if (n % 10000 == 0 && Thread.interrupted()) {
                            throw new InterruptedException("Calcolo interrotto");
                        }
    
                        bi = bi.multiply(BigInteger.TWO);
                    }
    
                    logger.info("User {}: Calcolato valore {} bit", user, bi.bitLength());
                }
            } catch (InterruptedException e) {
                logger.info("User {}: il task è stato interrotto!", user);
            } finally {
                logger.info("User {}: FINE TASK", user);
            }
    
            return new AsyncResult<>(null);
        }
    }
    Quello sopra è solo un ESEMPIO. Esegue 10 volte due parti, una di sleep, una di calcoli puri. Lo sleep è perfettamente "interrompibile", è ben documentato nel javadoc di sleep. Le computazioni "pure" NO, non sono automaticamente interrompibili. Sei tu che devi testare "ogni tanto" se c'è la interruzione. Ogni quanto ... dipende. Ragionevolmente, che il test del interrupt non sia troppo pesante rispetto al resto del ciclo ma che sia comunque sufficientemente responsivo alla interruzione.

    Sulla mia macchina quel ciclo di calcolo impiega in tutto circa 30 secondi. Testando ogni 10000 cicli, risulta quindi che è indicativamente responsivo sui 0,3 secondi, quindi ragionevole.


    Ora una classe UserTask, che è altamente riutilizzabile, NON è legata ad un task specifico.
    package xyz;
    
    import java.util.concurrent.Future;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    import java.util.function.Supplier;
    
    public class UserTask<T> {
        private final String user;
        private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        private Future<T> future;
    
        public UserTask(String user) {
            this.user = user;
        }
    
        public String getUser() {
            return user;
        }
    
        public void startTask(Supplier<Future<T>> futureSupplier) {
            if (lock.writeLock().tryLock()) {
                try {
                    if (future != null && !future.isDone()) {
                        throw new IllegalStateException("Il task è già in esecuzione");
                    }
    
                    future = futureSupplier.get();
                } finally {
                    lock.writeLock().unlock();
                }
            } else {
                throw new IllegalStateException("Il task non può essere avviato in questo momento");
            }
        }
    
        public void stopTask(boolean mayInterruptIfRunning) {
            if (lock.writeLock().tryLock()) {
                try {
                    if (future != null) {
                        future.cancel(mayInterruptIfRunning);
                    }
                } finally {
                    lock.writeLock().unlock();
                }
            } else {
                throw new IllegalStateException("Il task non può essere stoppato in questo momento");
            }
        }
    }
    Nota che UserTask "incapsula" il Future e come puoi vedere non è esposto all'esterno. Questa è una buona cosa in generale.
    Ho usato anche io un ReentrantReadWriteLock ma per un motivo più specifico: perché ha il tryLock().

    La questione importante è: se un thread A sta facendo lo startTask e un secondo thread B nel medesimo istante vuole fare la stessa cosa per lo stesso user, cosa dovrebbe succedere?
    Ci sono due (almeno) soluzioni: il thread B sta bloccato in attesa di acquisire il lock, oppure la richiesta fallisce subito.
    La mia scelta è stata la seconda. Se il lock è già acquisito, tryLock() restituisce false e quindi si lancia l'eccezione per dire che non puoi fare l'azione in quel preciso momento.

    Nota che l'interno del try è "atomico" ... DEVE essere atomico. Il concetto "se non è (già) in esecuzione, lo avvio" è una operazione "composta", va fatta atomicamente. Per questo serve comunque un lock. Idem per il try nel stopTask.

    Il "difetto" purtroppo di Future è che se fai cancel, NON puoi attendere la terminazione del task. Il task potrebbe non essere (subito o affatto) responsivo, quindi potrebbe metterci del tempo a terminare materialmente.
    Con il solo Future purtroppo non è risolvibile, servirebbe un ulteriore meccanismo di sincronizzazione che però dovrebbe essere più "invasivo" sul task.

    Ora l'ultima parte, un @Service che gestisce i task per i vari user.
    package xyz;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class MyTasksService {
        @Autowired MyService myService;
    
        private final Map<String, UserTask<Void>> userTasks = new ConcurrentHashMap<>();
    
        public void startMyAsyncTask(String user) throws InterruptedException {
            UserTask<Void> userTask = getUserTask(user);
            userTask.startTask(() -> myService.myAsyncTask(user));
        }
    
        public void stopMyAsyncTask(String user) throws InterruptedException {
            UserTask<Void> userTask = getUserTask(user);
            userTask.stopTask(true);
        }
    
        private UserTask<Void> getUserTask(String user) {
            return userTasks.computeIfAbsent(user, UserTask::new);
        }
    }
    Qui ho usato un ConcurrentHashMap, che è già di suo altamente "concorrente". Grazie al computeIfAbsent, non c'è bisogno di sincronizzazione esplicita. In pratica mantengo una mappa di user->UserTask. Ogni user ha il SUO personale oggetto UserTask, che inizialmente viene creato con il Future null, ovviamente.

    Il computeIfAbsent è atomico, per uno user X: o dà un UserTask già esistente, o ne crea uno nuovo. Non può fare casini del tipo creare 2 UserTask per lo stesso user se ci sono due richieste concorrenti.

    Nota che myAsyncTask() è invocato sul MyService "iniettato". Quello che è iniettato è il proxy (verso il bean MyService) che ha la macchineria per gestire l'@Async, quindi funziona asincrono come ci si aspetta (se abilitato nella applicazione con @EnableAsync, ovviamente).

    Osserva come sia tutto semplice, minimale e pulito. Non ci sono if, else, if else if ecc... come hai fatto tu nel tuo esegui. Quello è "fumoso", come dicevo prima.

    Ah, ho usato <Void> per evitare alcune grane con i wildcard <?> che non sto ora a spiegare. Di fatto non cambia niente, il null passato al AsyncResult comunque non viene usato.


    P.S. ti prego, concludi questa esercitazione. Dopodiché prendi: "Il Nuovo Java" - Claudio De Sio Cesari - HOEPLI. O uno dei precedenti sempre di De Sio Cesari.
  • Re: Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

    Queste emoticon non sono abbastanza >>

    Prima di tutto nuovamente grazie per il tuo codice, come al solito ti distingui per generosità, disponibilità, competenza e tanti altri termini di pari valore.

    Per me è un brutto periodo ed il tempo che ho a disposizione è sempre meno ma voglio finire questa esercitazione entro questo mese per poi pensare ad un percorso di apprendimento più proficuo.
    Ho letto velocemente tutto quanto e non ho ancora testato le tue classi ma do per scontato che funzionino al 100%.
    L’errore che faccio io è mettere tutto nella stessa classe @Service infatti spostando l’annotazione @Async con i relativi metodi in una seconda classe con l’annotazione @Service il metodo cancel() funziona.

    Se metto il metodo con @Async in una seconda classe @Service posso:
    1) chiamarlo per primo;
    2) fare in modo che restituisca Future<?> come output;
    3) non invocarlo dalla stessa classe in cui nasce.

    Poi c’è il discorso che il mio codice è obbrobrioso e raccapricciante ma non è questo che causa il malfunzionamento.

    Ancora grazie
  • Re: Ci sono dei limiti di attesa per l'output di una pagina web? Come edito questi limiti con Spring Boot 2?

    andbin ha scritto:


    La questione importante è: se un thread A sta facendo lo startTask e un secondo thread B nel medesimo istante vuole fare la stessa cosa per lo stesso user, cosa dovrebbe succedere?
    Ci sono due (almeno) soluzioni: il thread B sta bloccato in attesa di acquisire il lock, oppure la richiesta fallisce subito.
    La mia scelta è stata la seconda. Se il lock è già acquisito, tryLock() restituisce false e quindi si lancia l'eccezione per dire che non puoi fare l'azione in quel preciso momento.
    Ti chiedo solo una precisazione. Io ho usato lock perché preferisco che l'utente resti in attesa. Volevo sapere se ho commesso un errore oppure se ritieni comunque accettabile la scelta.
Devi accedere o registrarti per scrivere nel forum
26 risposte