Comando di gestione errori di feedback con CQRS

2

Stiamo sviluppando un contesto utilizzando l'approccio CQRS. Siamo finiti con i gestori di comandi che emettevano eventi. Sembra non essere una buona idea per noi. Tuttavia non possiamo trovare alcun approccio alternativo. Abbiamo difficoltà con due particolari gruppi di scenari:

  1. Creazione di una nuova aggregazione.

La creazione di un aggregato può avere esito positivo. In caso di successo tutto è semplice - aggregato mantiene l'evento AggregateCreated (compilato all'interno del suo costruttore). Ma in caso di errore, l'aggregato non può emettere / pubblicare nulla perché non esiste nemmeno. In tal caso, il nostro gestore comandi emtis CreatingAggregateFailed che riusciamo a individuare come perdita del dominio.

  1. Risorsa non trovata

Secondo scenario per quanto riguarda la non creazione di particolari risorse. Ad esempio, rimuovere la risorsa non esistente. Nella nostra implementazione Repository :: find () solleva l'eccezione NotFound. L'excetpinon a sua volta viene catturato in un gestore che emette l'evento AggregateNotFound.

Sulla base di questi eventi creiamo gestori di processi e risposte. Ma sembra un po 'strano che gli eventi di dominio (applicazione?) Vengano emessi al di fuori degli aggregati. Anche se questi eventi ingombranti si applicano alla non esistenza di tale aggregato.

Consentitemi di considerare lo scenario più semplice. Invio il comando AddTeamMember ($ teamGuid, $ userGuid, $ role). Se team, utente e ruolo esistono ma il comando viola qualsiasi aggregato invariante aggregato può registrare l'evento MemberRejected - per quanto tutto sia buono. Ma né l'utente, il team o il ruolo dato potrebbero non esistere nemmeno. Quindi non esiste un aggregato in grado di registrare un evento. Ho bisogno di un feedback su questo fallimento per intraprendere azioni appropriate (sia in Process Manager o semplicemente informare il mio emittente di comandi). Considero MembershipRequest aggregato come destinatario del comando. Quindi ho sempre aggregato valido per pubblicare eventi e gli eventi sono significativi all'interno dell'agenda. Ma questo introduce ulteriore compelxity. Ho bisogno di un aggregato intermedio per gestire "risorsa non trovata" per ogni comando possibile.

Ho trovato una nuova idea. Lo illustrerò con esempi di codice.

Versione del gestore precedente

/**
 * @param CreateChannel $command
 */
protected function handleCreate(CreateChannel $command): void
{
    try {
        $channel = new Channel($command->getGuid(), $command->getSymbol(), $command->getLangCodes());
        $this->channelRepository->save($channel);
    } catch (InvalidData $e) {
        $this->eventBus->publish($channel, new ChannelCreationFailed($command->getGuid()));
    }
}

Nuova idea

/**
 * @param CreateChannel $command
 */
protected function handleCreate(CreateChannel $command): void
{
    try {
        $channel = new Channel($command->getGuid());
        $channel->create($command->getSymbol(), $command->getLangCodes()))
        $this->channelRepository->save($channel);
    } catch (InvalidChannelData $e) {
        // ?
    } finally {
        $this->eventBus->publish($channel, new ChannelCreationFailed($command->getGuid()));
    }
}

Ma questo ha alcuni ovvi inconvenienti. Innanzitutto, il livello di servizio influisce sulla progettazione aggregata. Reinventa anche la creazione del concetto di linguaggio oggetto. Esegue il controllo con qualsiasi altro metodo se il aggregato è in stato "creato". Forse è opportuno distinguere tra creazione di oggetti e creazione di aggregati? Questo approccio non presuppone eccezioni sollevate dal costruttore. Il costruttore otterrebbe sempre solo guida valida (garantita dal comando) e nient'altro.

Avere sempre aggregato con guid risolve anche aggregati di riferimento non esistenti. Usando il doppio invio è possibile generare eccezioni "non trovate" dall'aggregato sorgente

$team->addMember($userGuid, $usersRepository);

Ma questo aggregato di makse è api brutto rispetto a

$team->addMember($user);
    
posta ayeo 30.08.2018 - 13:38
fonte

4 risposte

1

Non pubblicare eventi di dominio di cui ai tuoi esperti di dominio non interessa.

Se uno specifico caso di errore è una parte identificata di un processo aziendale (tipicamente qualcosa che emergerebbe durante una sessione di Storming di eventi con gli uomini d'affari), trova un termine per esso nell'Ubiquitous Language e pubblica definitivamente un evento per esso .

Ma in casi non nominali come interruzioni di rete, interruzioni di sistema, errori di configurazione e simili, non vorrei passare attraverso il normale ciclo di pubblicazione di un pub. Nel caso di un comando richiesto da un utente attraverso un'interfaccia utente, avvisare che si è verificato un errore. Se il comando è stato eseguito da un gestore di processi, saprà che qualcosa è andato storto e forse

  • Riprova
  • esegue un comando di compensazione
  • o notifica agli amministratori

a seconda della situazione. Se viene inviato un comando di compensazione e un nuovo evento emesso di conseguenza, gli ID di correlazione possono aiutarti a rintracciare quale evento o comando originale ha attivato la compensazione e con l'aiuto dei log, diagnosticare e risolvere il problema.

    
risposta data 31.08.2018 - 17:45
fonte
2

Nel sourcing di eventi, normalmente registriamo solo gli eventi se lo stato del modello cambia. Quindi un evento "WeDidn'tChangeTheStateOfTheModel" non ha molto senso.

Tuttavia, esiste una preoccupazione trasversale denominata telemetria che si preoccupa molto delle richieste gestite, dei guasti e così via. Ma questo è un dominio diverso, con compromessi di costo completamente diversi

(Esempio: perdere un ordine per un cliente è negativo, quindi il costo per l'azienda di perdere l'evento dominio è alto, ma se il conteggio degli eventi di telemetria ci dice che un ordine è stato eseguito con successo elaborato non corrisponde esattamente al conteggio degli eventi di dominio corrispondenti, probabilmente non costa molto al business).

Di solito al business interessa molto di più una cronologia degli ordini duratura rispetto al log web.

Sì, mi sarei aspettato che il gestore comandi emettesse eventi di telemetria, ma non eventi di dominio.

    
risposta data 30.08.2018 - 15:55
fonte
1

Ai fini della tua domanda, consente di distinguere tra due tipi di errore, dominio e tecnico.

Supponiamo che uno studente si registri per un corso, ma alcuni servizi di backend falliscono. In questo caso gli eventi StudentCreated (), StudentDebited () e ClassRoomSeatReserved () dovrebbero essere stati mantenuti nell'archivio degli eventi in modo che quando il servizio non funzionante sia di nuovo online, gli eventi potrebbero essere rieseguiti.

L'eccezione TECHNICAL avrebbe dovuto essere registrata e prelevata da theDevOps e conteneva qualche riferimento al batch di eventi che non funzionava. Questa è la coerenza finale con una possibile saga.

Quando il servizio riceve il comando è necessario eseguire la convalida sul comando e inviare eventuali errori DOMAIN al chiamante e, facoltativamente, creare eventi di dominio se è INTERESSANTE al dominio.

I 2 processi sono separati e servono a scopi diversi.

Per quanto riguarda il coinvolgimento di Aggregate in questo, ad esempio lo StudentAggegate (), esso viene coinvolto solo dopo che gli eventi sono stati mantenuti e quindi originati.

The Aggregate non è un record nel database, è una classe che governa il comportamento dei suoi fratelli all'interno del contesto limitato.

Lascia che ti spieghi con il pattern factory e un'interfaccia fluente, qualcosa del genere:

Se (! StudentRegisterCommand.IsModelValid ())

// optionally create domain events.
return ModelValidationErrors() 

eventlist = Factory.CreateStudent (StudentRegisterCommand) .Debit (StudentRegisterCommand.Cource.Cost, StudentRegisterCommand.Card) .ReserveSeat (StudentRegisterCommand.Cource) .Events ()

Repository.PersistEvents (EventList)

Bus.AddEvents (EventList)

Solo ora il mio RegistrarService riceve l'evento per creare il mio nuovo StudentAggegate. Se l'addebito fallisce, la prenotazione del posto si inverte e il FinanceService:

Studente = StudentService.GetStudent ()

eventlist = Factory.NotifyStudent (Student.Name) .Message ("Si prega di contattarci come la tua carta è stata rifiutata."). ByMail (Student.email) .SMS (Student.Phone) .UnReserveSeat (Student, Student.Cource )

Repository.PersistEvents (EventList)

Bus.AddEvents (EventList)

L'aggregato è sia una classe che

Spero che questo aiuti.

    
risposta data 30.08.2018 - 21:05
fonte
0

Mi sembra che il problema sia dovuto al mix di approcci orientati agli oggetti e al servizio.

Se segui un approccio OO e vuoi che l'evento sia emesso dal tuo oggetto, allora l'oggetto dovrebbe incapsulare gli oggetti Command e Query.

  • Object.Save () / ObjectCollection.New () dovrebbe creare il comando e lanciare l'evento se il comando fallisce

  • Object.Get () / ObjectCollection.Find () dovrebbe creare la query e lanciare l'evento non trovato se fallisce.

Se si utilizza un approccio di servizio, gli eventi appartengono al servizio o al repository e non fanno assolutamente parte dell'aggregato.

Non penso che sia un problema per questi oggetti per creare eventi di dominio orfani poiché dovranno comunque fare riferimento al dominio. Ciò non causa alcun accoppiamento tra diversi domini / aggregati

Se vuoi seguire l'approccio OO, allora inserirò il repository in ObjectCollection. Chiedete al repository di incapsulare la roba di CQRS e lancia i propri errori che vengono convertiti in eventi all'interno di ObjectCollection.

ObjectCollection stesso può sempre esistere come collezione vuota.

    
risposta data 30.08.2018 - 14:55
fonte

Leggi altre domande sui tag