Sari la conținut

Evenimente de domeniu

Evenimentele de domeniu sunt mecanismul de decuplare care le permite funcționalităților să comunice fără dependențe directe. În loc ca funcționalitatea A să o apeleze pe funcționalitatea B, funcționalitatea A publică un eveniment, iar funcționalitatea B se abonează la el.

DomainEventBus este un bus de publicare/abonare de tip broadcast, în proces. Publicatorii apelează publish(event); abonații consumă un flux tipizat on<T>(). Fiecare eveniment implementează DomainEvent și poartă occurredAt.

Bus-ul rulează în interiorul cc_server. Fiecare publicator și fiecare abonat activ se află acolo. Niciun client nu vede acest bus. Ce vede un client este un subset selectat pe care serverul îl retransmite ca și cadre JSON-RPC notifications/*.

Fără evenimente, fiecare reacție între funcționalități este un import. Calea de notificări ar trebui să importe funcționalitatea de agenți, cea de pull request-uri, tichetele și întâlnirile doar ca să știe când să afișeze un toast — iar fiecare dintre acestea ar trebui apoi să știe că notificările există. Modulul de tichete ar importa pipeline-uri ca un tichet finalizat să poată avansa o rulare. Graful de dependențe se închide într-un nod, iar nucleul partajat încetează să mai fie partajat.

Publicarea unui eveniment rupe acest lanț. Calea de notificări se abonează la douăzeci și unu de clase de evenimente și nu importă niciuna dintre funcționalitățile care le emit. Dispecerul de trigger-e de pipeline se abonează la întregul bus și nu știe nimic despre pull request-uri, tichete sau întâlniri. Agregatele de observabilitate rezumă rezultatele rulărilor fără să pătrundă în logica de trimitere.

Costul este cel obișnuit: un publicator nu poate spune dacă cineva ascultă, iar un ascultător care nu este niciodată construit rămâne mut în mod silențios. TicketRemoteSyncHandler este exemplul constant: este declarat, acoperit de teste și niciodată conectat în producție.

Merită să fim precisi în privința unui singur lucru pe care bus-ul nu îl deține.

Pista de audit nu este o proiecție a fluxului de evenimente. Fiecare operație RPC care modifică datele este auditată implicit — RepoOp.audited are valoarea implicită true, iar o operație se sustrage doar declarând audited: false, un act deliberat, vizibil chiar pe operație. Dispecerul adaugă rândul „cine a făcut ce, de unde” imediat după un apel reușit. Nu există DomainEventAuditBridge. ActivityLogPersister scrie rânduri activity_log din evenimente ActivityLogged; acesta este un flux separat de auditul RPC.

Bus-ul de evenimente este transportul pentru notificări și pentru automatizare. Nu este sistemul de evidență de referință.

  • Notificări. Douăzeci și unu de clase de evenimente sunt transformate de server în cadre notifications/*, înregistrate într-un flux durabil per spațiu de lucru și difuzate sesiunilor conectate. Un cadru al cărui workspace_id numește un spațiu de lucru din care utilizatorul sesiunii nu face parte este eliminat pe server; clientul filtrează apoi restul la spațiul de lucru pe care îl are în vedere.
  • Automatizare bazată pe evenimente. Trigger-ele de pipeline se abonează la bus și pornesc automat pipeline-urile care corespund (vezi mai jos).
  • Reacții de ciclu de viață. Crearea unui spațiu de lucru populează agentul CEO și agenții specializați, șabloanele de pipeline incluse și suitele de eval-uri de început. O rulare de agent finalizată reia un pas de pipeline suspendat, închide o sarcină, alimentează supervizorul de obiective, poate trezi un verificator anume și poate declanșa din nou un lider de echipă. Un PR îmbinat sau un spațiu șters declanșează colectarea de gunoi a worktree-urilor.
  • Sincronizare între furnizori. Modificările locale ale tichetelor sunt trimise către furnizorii configurați de un coordonator care ascultă cinci evenimente de tichet.

Evenimentele acoperă întreaga suprafață a produsului. La nivel general:

  • Spații de lucru, agenți și repo-uri — crearea spațiului de lucru, finalizarea rulărilor, înregistrarea repo-urilor, actualizări de skill-uri
  • Pull request-uri — publicare, schimbări de stare, îmbinări, cereri de revizuire, mențiuni, urmărirea PR-urilor proprii, PR-uri detectate extern
  • Mesagerie — mesaje, spații și progresul de provisionare a spațiilor
  • Tichete — creare, atribuire, reatribuire, detalii, stare și cele trei rezultate finale
  • Sarcini — cadre de ciclu de viață secvențiate pentru o singură rulare trimisă, astfel încât clienții să le poată ordona și elimina duplicatele
  • Pipeline-uri — doar evenimente finale de rulare (finalizat, anulat, eșuat)
  • Observabilitate — intrări de jurnal de activitate și praguri de buget
  • Identitate și apartenență — membru adăugat, eliminat, rol schimbat
  • Calendar și întâlniri — expirarea autentificării, începerea întâlnirilor și finalizarea înregistrărilor
  • Stații — preluare, colectare și închidere neașteptată

Memoria, orchestrarea, documentele de plan, artefactele, aprobările, preluarea sincronizării tichetelor, reîmprospătarea calendarului, îmbinările de worktree-uri, crearea utilizatorilor, invitațiile și revocarea dispozitivelor nu sunt evenimente de domeniu; vezi referința.

Pentru catalogul complet — toate cele 53 de clase concrete de evenimente, payload-urile lor și cei 24 de abonați care le consumă — vezi Referința pentru evenimentele de domeniu. Pagina aceea este inventarul; aceasta este modelul.

Controlul accesului în timp real se bazează pe două mecanisme diferite

Secțiune intitulată „Controlul accesului în timp real se bazează pe două mecanisme diferite”

Evenimentele de apartenență și cele de dispozitiv par simetrice, dar nu sunt, ceea ce contează dacă raționezi despre cât de repede își face efectul o revocare.

Revocarea unui dispozitiv chiar îi încheie sesiunile. Serverul supraveghează tabelul de dispozitive asociate și elimină orice sesiune activă al cărei dispozitiv a ieșit din setul activ, în câteva secunde.

Eliminarea unui membru nu închide socketul. WorkspaceMemberRemoved este consumat: LocalRpcServer și RemoteRelayHost elimină abonamentele la spațiile de lucru atașate acelui utilizator, iar RemoteEventForwarder își invalidează cache-ul de apartenență, astfel încât alte cadre pentru acel spațiu de lucru să nu mai fie trimise. Ceea ce respinge următorul RPC este tot poarta de rol, care re-rezolvă apartenența la fiecare apel — așa că următoarea lor cerere este refuzată cu “Not a member of this workspace”. Efectul este imediat pentru orice încearcă să facă; pur și simplu nu se obține prin închiderea conexiunii lor.

Trigger-ele de pipeline se abonează la evenimente de domeniu și pornesc automat pipeline-urile care corespund. PipelineTriggerDispatcher ascultă întregul bus, apoi:

  1. Iese imediat dacă tipul evenimentului nu este unul pe care știe să îl mapeze la un payload
  2. Caută trigger-ele activate care corespund acelui tip de eveniment, în fiecare spațiu de lucru
  3. Filtrează fiecare la propriul spațiu de lucru și aplică filtrul de potrivire a payload-ului
  4. Pornește o rulare pentru fiecare trigger care supraviețuiește

Constrângerea importantă este pasul 1. Bus-ul transportă 53 de tipuri de evenimente; selectorul de adăugare a trigger-elor pornește o rulare pentru aceste paisprezece (ramuri de payload din EventPayloadMapper): ExternalPrDetected, PullRequestPublished, PullRequestStatusChanged, PrMerged, MessageReceived, TicketAssigned, TicketCompleted, TicketFailed, TicketCancelled, BudgetThresholdCrossed, RepoAdded, MeetingRecordingStopped, SkillUpdated și SpaceDeleted. Nimic altceva nu poate porni un pipeline.

RemoteEventForwarder traduce evenimentele din clasa notificărilor în cadre și le trimite prin RPC sesiunilor conectate, unde un mapper de cadre le transformă în notificări din aplicație. Un client subțire vede așadar o proiecție a fluxului de evenimente al serverului fără să dețină vreo execuție — ceea ce este exact ideea din spatele separării clientului subțire.

Totuși, nu orice cadru trimis este o notificare. Fluxul ciclului de viață al sarcinilor și reatribuirea tichetelor sunt trimise în timp real pentru a conduce interfața, dar nu sunt înregistrate în fluxul de notificări și nu afișează niciun toast.