Il modulo p2p

La classe PartecipantNode rappresenta un potenziale nodo partecipante. Ha l'attributo partecipant che dice se effettivamente lo è (in un dato servizio P2P, cioè in una data MapP2P)

La classe MapP2P eredita dalla classe Map e ha come dataclass la PartecipantNode.
Il suo attributo pid viene specificato al momento della creazione, nel costruttore. E' l'ID del servizio P2P che questa mappa rappresenta.
Il metodo partecipate si segna che il nodo partecipa, cioè mette a True l'attributo participant delle istanze (una per livello) che rappresentano se stesso nella sua mappa.
Il metodo node_del viene overridato dalla classe base Map solo per rendere tale metodo una microfunc. Sarà dichiarato gestore dell'evento NODE_DELETED della Maproute del nodo.
Il metodo me_changed sostituisce il me_change della classe base Map. Rende tale metodo una microfunc con dispatcher. Ha un nome diverso solo per accettare anche un altro parametro, il old_me, che non userà; questo per poterlo dichiarare come gestore dell'evento ME_CHANGED della Maproute del nodo.

La classe P2P

La classe P2P è la base dalla quale erediteranno tutte le classi che implementano un servizio p2p.

Eredita a sua volta dalla classe RPCDispatcher (vedi il modulo RPC). Viene istanziata passando le istanze di Radar, Maproute, e il PID (P2p ID) del servizio.
Nel suo costruttore si istanzia la MapP2P con le stesse dimensioni della Maproute e con il mio stesso NIP. Inoltre registra dei gestori (i metodi node_del e me_changed della classe MapP2P appena istanziata) per gli eventi ME_CHANGED e NODE_DELETED della Maproute. In questo modo si incarica di tenere sincronizzate queste due mappe.
Ha un attributo partecipant che valorizza inizialmente a False.
Dichiara metodi remotable il partecipant_add e il msg_send.
Essendo una derivata della classe RPCDispatcher richiama il suo costruttore indicando che la sua root_instance è se stessa (self).

Partecipanti al servizio

Per dichiarare che il nodo corrente partecipa ad un servizio peer-to-peer uso il metodo participate dell'istanza di P2P relativa a quel servizio.
Questo per prima cosa valorizza a True l'attributo participant. Questo fa sì che in futuro il metodo p2p_hook (che resetta la mappa) richiami di nuovo automaticamente il metodo participate.
Il metodo poi richiama il metodo participate sulla mappa mapP2P.
Infine propaga l'informazione ai vicini attraverso il metodo remotable partecipant_add.

Il metodo p2p.partecipant_add (richiamato da un nodo remoto) si segna sulla mappa (se già non lo sapeva) che quel nodo partecipa, e propaga l'informazione ai vicini.

Richiesta di un servizio

Il metodo h serve per ricavare da una generica chiave un indirizzo NIP.
Nella classe base P2P la chiave è lo stesso NIP che viene restituito. Si tratta di una implementazione ovviamente poco utile.
Il metodo deve essere quindi ridefinito nel modulo che implementerà un certo servizio, con un algoritmo che partendo da una chiave significativa per quel servizio, restituisca un NIP.
Per esempio nel modulo ANDNA la chiave sarà la stringa hostname, per poter poi trovare il nip del register node di quell'hostname; nel modulo Coord la chiave sarà una tupla (lvl, ip), per poter trovare il nip del coordinator node per il gnodo di livello lvl di quel ip; e così via.

Il NIP ottenuto con il metodo h potrebbe indicare un nodo non presente o che non partecipa a questo servizio P2P.
A questo punto va quindi utilizzato il metodo H per ottenere un indirizzo NIP valido.
Vediamo come funziona il metodo H.
Supponiamo che il nostro nodo abbia IP 1.2.3.4 (self.mapp2p.me = [4,3,2,1]) e che vogliamo trovare un IP valido per il IP 1.2.12.34 (IP = [34,12,2,1])
Partendo dal livello l più alto cerco nella mia mappa (self.mapp2p) un gnodo di livello l che sia un partecipante attivo. Il più vicino possibile all'identificativo richiesto, cioè IP[l].
Partendo dal livello 3, in questo caso trovo di sicuro che il ID = 1 si presta bene, perché è lo stesso mio gnodo di livello 3 ed io sono un partecipante. Quindi passo al livello 2. Anche qui trovo che ID = 2 si presta bene, per lo stesso motivo.
Arrivato al livello 1 cerco un ID vicino a IP[1], che è 12. Posso vedere nella mia mappa se il gnodo 12, all'interno del ggnodo 2, all'interno del gggnodo 1, è partecipante al servizio. Se non lo è provo con 11, poi con 13, poi con 10, poi con 14, e così via.
Se arrivo ad un gnodo partecipante, mettiamo ad esempio 5, che è diverso da quello a cui appartengo io (self.mapp2p.me[1] = 3) allora non posso scendere ad un dettaglio più basso, perché nella mia mappa non ho informazioni sui nodi di quel gnodo. Quindi interrompo qui.
In questo caso, quindi, il valore ritornato da H([34,12,2,1]) sarà la lista [None,5,2,1]. Il valore None all'indice 0 mi dice che non ho dettagli a quel livello.

Attenzione ora. Io so che devo chiedere informazioni o servizi al nodo 1.2.12.34, oppure al nodo partecipante più "vicino" (per vicino si intende qui in senso di numeri, concetto molto astratto). So che il gnodo di livello 1 e indirizzo 1.2.5 contiene alcuni nodi partecipanti, ma non posso sapere quale di questi sia il più "vicino" al 1.2.12.34.
Il mio messaggio deve quindi giungere al primo border node che incontro in quel gnodo. Ma il protocollo TCP/IP non mi consente di specificare una tale destinazione. Per questo motivo ho bisogno di un metodo che instradi i miei pacchetti passo passo verso il border node di cui parlavo.
Vediamo come si realizza questo.
Il metodo msg_send ha come argomenti il mittente del messaggio (sender_nip=[4,3,2,1]) il destinatario "perfetto" del messaggio (hip=[34,12,2,1]) e il messaggio (msg) che è una tupla (func_name, args).
Per prima cosa trasforma il hip con il metodo H sulla base delle conoscenze di questo nodo (vedi sopra). In questo caso lo trasforma in hip=[None,5,2,1]
Poi usa il metodo neigh_get. Questo, a partire da un hip (che potrebbe avere i livelli più bassi a None) restituisce l'oggetto Neigh del vicino da usare come gateway. Trovato questo vicino richiama su di lui lo stesso metodo msg_send, che è remotable, passando sender_nip, il nuovo hip e il messaggio msg. TODO: l'hip passato al metodo remoto è quello con dentro i possibili None. Questo non causa problemi la prossima volta che si esegue H? Forse andrebbe passato l'hip originale?
Notare: Il metodo neigh_get mi dice il mio vicino migliore per raggiungere il gnodo 1.2.5. Questo vicino non è detto che sia partecipante al servizio. Nonostante ciò, di sicuro il vicino conosce l'esistenza di questo servizio (vedi più sotto la descrizione del metodo p2p_hook della classe P2PAll) quindi la chiamata "n.ntkd.p2p.PID_"+str(self.mapp2p.pid)+".msg_send(sender_nip, hip, msg)" funziona comunque.
Dopo alcuni passi il metodo msg_send verrà richiamato proprio sull'host destinatario (quello perfetto o il più vicino partecipante). Dopo aver richiamato il metodo H il nodo si accorge di essere lui stesso il destinatario e che quindi deve eseguire l'azione richiesta.

Per eseguire l'azione richiesta richiama il metodo msg_exec. Questo usa il messaggio (la tupla func_name e args) con il suo metodo dispatch, essendo la classe P2P una derivata di RPCDispatcher. In questo modo viene eseguito il metodo remotable indicato nella chiamata del servizio.
Il valore restituito dal metodo remotable indicato nella chiamata al servizio viene a sua volta restituito dal metodo msg_exec; poi viene a sua volta restituito dal metodo remotable msg_send chiamato nell'ultimo passo dell'instradamento; viene poi restituito a ritroso dallo stesso metodo chiamato nei vari passi dell'instradamento, fino ad essere restituito dal metodo msg_send chiamato in modo locale sul nodo mittente. TODO: Domanda: non sarebbe più performante e meno pesante per l'intera rete se la risposta avvenisse in modo diretto dal destinatario al chiamante con una connessione diretta visto che ora il destinatario ha l'IP preciso del mittente nell'argomento "sender_nip"?
Inoltre, anche nel caso in cui l'indirizzo restituito dal metodo H fosse completo, senza parti uguali a None, sarebbe meglio usare subito un TCPClient verso quel IP.

La chiamata a msg_send non viene fatta direttamente, si vuole fare uso del meccanismo del FakeRmt (vedi il modulo RPC). Si usa quindi il metodo peer per ottenere una istanza di FakeRmt che sia a conoscenza delle informazioni da passare al metodo msg_send.
Il metodo peer si può richiamare specificando l'argomento keyword key oppure direttamente l'argomento keyword hip. Di norma si usa specificando la chiave da cui calcolarsi l'hip a cui rivolgersi. TODO: anche in questo caso a partire da key si eseguono sia il metodo h sia il metodo H. Invece dovrebbe essere eseguito solo h per avere l'hip perfetto da passare al metodo msg_send. Giusto?
Ad esempio il servizio Coordinator si usa così:
   co2 = self.coordnode.peer(key = (lvl+1, newnip))
   newnip = co2.going_in(lvl)
   co2.close()

TODO: Fino a che punto è stato implementato l'algoritmo descritto nel documento Ntk_p2p_over_ntk.pdf ?
Mi pare di non vedere nel codice l'implementazione dei 31 nodi backup, né del timeout di rimozione dei dati registrati, né dell'ingresso di un nuovo nodo nella rete che automaticamente sostituisce il precedente come più vicino all'hip perfetto.
Queste implementazioni si possono fare a livello della classe P2P o vanno per forza fatte sulle singole classi derivate di un servizio specifico?

La classe P2PAll

La classe P2PAll gestisce tutti i servizi p2p registrati.
Ha come membro un dizionario ( self.service = {} ) nel quale memorizzerà tutte le istanze di P2P che si creerà per i servizi noti al nodo (non solo quelli in cui partecipa).

Quando un nodo ha completato un hook alla rete, ad esempio dopo la fase di startup (TODO: Anche questo gestore dell'evento HOOKED dovrebbe essere registrato solo dopo il primo hook che avviene nella fase di startup solo per l'ativazione delle interfacce di rete. Vedi la fase di startup) viene richiamato il metodo microfunc p2p_hook.
Questo metodo cerca un vicino, possibilmente del nostro stesso gnodo di livello 1, oppure del nostro stesso gnodo di livello 2, e così via. Al vicino "più vicino" (come gnodo) che trova, richiede l'elenco dei servizi noti e le reative mappe MapP2P usando il metodo remotable pid_getall.
Questo metodo restituisce una lista di tuple. Ogni tupla ha il PID del servizio e una struttura dati che descrive la mappa MapP2P relativa a quel servizio (vedi il metodo map_data_pack della classe Map).
Una volta ottenuta, per ogni servizio noto, la struttura descrivente la mappa MapP2P relativa a quel servizio, ne fa la copia sulla propria mappa MapP2P relativa a quel servizio (vedi il metodo map_data_merge della classe Map).
Notare che il servizio potrebbe non essere noto al nodo. Potrebbe trattarsi di un servizio opzionale di cui il nodo corrente non conosce l'uso, né i servizi supportati, né l'algoritmo di associazione key -> hip, e così via.
In questo caso non ci sarà ancora l'istanza della classe derivata di P2P relativa a questo servizio nel dizionario service. In questo caso viene creata al volo una istanza della classe base P2P. Di default questa istanza dirà che il nodo non è partecipante al servizio (P2P.partecipant = False). Questo è comunque sufficiente a inoltrare, con il meccanismo del metodo msg_send della classe P2P descritto sopra, i messaggi tra un sender e un hip.
Poi esegue il P2P.participate sulle istanze dei servizi a cui il nodo corrente è interessato. Il nodo dichiara di essere interessato ad un servizio nello stesso momento in cui registra la classe che implementa il servizio. La registrazione viene fatta con il metodo p2p_register della classe P2PAll e avviene di norma (ad esempio vedi il modulo Coord) nel costruttore della classe derivata di P2P.
Infine emette il segnale P2P_HOOKED.

Netsukuku/ita/ModuloP2P (last edited 2009-06-01 14:33:06 by lukisi)