Il modulo event e la gestione degli eventi

La generazione di eventi da parte di un modulo (A) di Netsukuku e l'ascolto di tali eventi e la loro gestione da parte di un altro modulo (B) sono uno dei meccanismi di comunicazione tra due moduli distinti.

Le funzioni contenute in un modulo, servono di norma a gestire un particolare aspetto del programma. Di norma sono eseguite da un microthread a cui questo aspetto viene demandato. Quando durante questa esecuzione si vogliono inviare messaggi, richiedere servizi ad altri moduli, o comunque segnalare il completamento di particolari azioni, si emettono dei segnali, o eventi.

Nota: nel corso di questa trattazione i termini "emettere segnali" e "generare eventi" sono equivalenti.

I moduli che sono interessati a questi eventi lo hanno già dichiarato registrando come listeners di questi eventi alcune loro funzioni.
In particolare se queste funzioni sono definite come microfunc, come vedremo meglio in seguito, si ottiene che la gestione degli eventi da parte di questi moduli avvenga in distinti microthread, o in un microthread dispatcher che li gestisce uno alla volta, e così via.

Il modulo A dichiara di generare certi eventi

Di norma gli aspetti di un modulo sono gestiti da una istanza della classe principale definita nel moduolo stesso.
Nel suo costruttore, questa classe dice quali eventi può generare con una sintassi di questo tipo:

class A:
    def __init__(self):
        self.events = Event( ['EVENT1', 'EVENT2', 'FOOEVENT'] )
        ...
        self.events.add( ['EVENT3', 'EVENT4'] )

Il modulo B dichiara di voler gestire certi eventi generati dal modulo A

Anche qui tale dichiarazione avviene nel costruttore della classe principale del modulo, la cui istanza gestisce gli aspetti del programma demandati al modulo.
Nel costruttore dunque, il modulo B, che deve avere un riferimento alla istanza di A, dichiara di voler gestire i suoi eventi con una sintassi di questo tipo:

class B:
    def __init__(self, a)
        # a is an instance of the class A
        a.events.listen('EVENT1', self.func_for_ev1)

B dice che quando A genera eventi di tipo 'EVENT1' deve essere eseguito il suo metodo func_for_ev1.
Il metodo func_for_ev1 deve accettare i parametri che sono indicati dal modulo A quando genera eventi di tipo 'EVENT1'.

class B:
    ...

    def func_for_ev1(param1, param2, param3):
        ...

Se la funzione registrata come listener è una normale funzione, questa viene direttamente richiamata nel momento in cui A genera l'evento. Questo significa che l'algoritmo svolto da A viene bloccato. Se invece si vuole che la gestione degli eventi da parte del modulo B sia svolta in un diverso microthread per non bloccare l'algoritmo di A, basterà definire la funzione come microfunc (vedere il modulo micro)

class B:
    ...

    @microfunc()
    def func_for_ev1(param1, param2, param3):
        ...

Il modulo A genera un evento

Durante l'esecuzione del programma Netsukuku, il modulo A vuole segnalare al modulo B (e a chi altri fosse interessato) che è avvenuto qualcosa, cioè vuole generare un evento di tipo 'EVENT1'.

class A:
    ...
    def f(self):
        self.events.send('EVENT1', (1, 2, 'terzo'))

Nota: fare attenzione che i parametri sono passati come una tupla. Ricordare che per dichiarare una tupla con un solo elemento va messa una virgola prima di chiudere la parentesi. Ad esempio:

self.events.send('EVENT1', (1, ))

La classe Event

La classe Event definita nel modulo event realizza il meccanismo di dichiarazione/registrazione/generazione di eventi descritto sopra.
Infatti nel suo costruttore il modulo A crea una istanza di Event e la memorizza, per convenzione, nel suo membro self.events. Il modulo B usa il metodo listen della istanza di Event memorizzata da A in a.events per registrare le sue funzioni listeners. Il modulo A usa il metodo send della istanza di Event memorizzata in self.events per generare gli eventi, e questo fa si che le funzioni listeners siano richiamate.

Il decoratore wakeup_on_event

Nel modulo event viene definito anche un decoratore di funzioni chiamato wakeup_on_event.

Con questo decoratore si indica che una funzione può mettersi in attesa, durante la sua esecuzione, di un certo evento.
Al momento della definizione della funzione occorre indicare di quali eventi si può mettere in attesa la funzione nel suo interno. Ogni evento è definito dalla tupla (EventInstance, EventName) dove EventInstance è una istanza della classe Event (ad esempio neigh.events) e EventName è il nome dell'evento (ad esempio 'NEIGH_NEW')
Inoltre occorre che la funzione definisca l'argomento con keyword event_wait con un valore default arbitrario.

    @wakeup_on_event(events=[(wheater_events, 'IT_IS_SUNNY'), (self.neigh.events, 'NEIGH_NEW')])
    def go_out(param1, param2, event_wait=None):
        ...

        msg = event_wait[(wheater_events, 'IT_IS_SUNNY')]() # Block and wait the event 'IT_IS_SUNNY'
        ...

        msg2 = event_wait[(self.neigh.events, 'NEIGH_NEW')]() # Block and wait the event 'NEIGH_NEW'
        ...

La funzione al suo interno si ritrova valorizzato l'argomento event_wait con un dizionario che associa la tupla (EventInstance, EventName) con una funzione.
Richiamando questa funzione con la sintassi

msg = event_wait[(wheater_events, 'IT_IS_SUNNY')]()

si ottiene che il microthread si blocca (lasciando lo schedulatore agli altri) in attesa dell'evento. Quando l'evento viene generato la funzione restituisce i dati associati all'evento. Quindi avremo nella variabile msg la tupla (param1, param2, param3).