Files
zitadel/backend/v3/domain/invoker_event.go
T
c4221a9c0f refactor: invoker implementations and clean up code (#10962)
This pull request refactors the command execution and transaction
handling logic within the `domain` package. The primary goal is to
simplify the invoker chain, clarify transaction management, and improve
the overall structure for better maintainability and readability.

### Key Changes:

- **Simplified Invoker Chain**: The previous chain of invokers has been
refactored. A new `transactionInvoker` is introduced to explicitly
manage database transactions for commands that require them. This
responsibility is no longer spread across different parts of the
invocation flow.
- **`Transactional` Interface**: A new `Transactional` interface has
been introduced. Commands that need to run within a database transaction
now implement this interface. This provides a clear and explicit
contract for transactional operations.
- **Consolidated Transaction Logic**: The logic for starting,
committing, and rolling back transactions is now centralized within the
`transactionInvoker` and the `eventStoreInvoker`. The `EnsureTx` method
on `InvokeOpts` has been removed, and transaction handling is now
managed automatically by the invocation framework.
- **Improved Event Handling**: The `eventStoreInvoker` and its
`eventCollector` have been rewritten to be more robust and to correctly
handle nested and batched commands, ensuring that all events are
collected and published within the same transaction.
- **Code Cleanup**: A significant amount of now-unused code, including
the old `invoker_test.go`, has been removed. Various comments and
variable declarations have also been cleaned up.
- **Updated Command Implementations**: All commands in the `domain`
package (e.g., `ActivateOrgCommand`, `DeactivateOrgCommand`,
`DeleteOrgCommand`, `UpdateOrgCommand`) have been updated to use the new
`Transactional` interface where appropriate, removing manual transaction
management.
- **Enhanced Testing**: Tests have been added and updated to cover the
new transaction and event handling logic, ensuring the refactoring is
sound.
- **Use `PrimaryKeyCondition`**: Use the
`repository.PrimaryKeyCondition(...)` method instead of using
`database.And(...)` where possible.

These changes lead to a cleaner, more decoupled architecture for command
handling in ZITADEL.

---------

Co-authored-by: Marco A. <[email protected]>
2025-10-27 14:04:06 +00:00

167 lines
4.2 KiB
Go

package domain
import (
"context"
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
legacy_es "github.com/zitadel/zitadel/internal/eventstore"
)
// eventStoreInvoker checks if the [EventProducer].Events function returns any events.
// If it does, it collects the events and publishes them to the event store.
type eventStoreInvoker struct {
invoker
collector eventCollector
}
func NewEventStoreInvoker(next Invoker) *eventStoreInvoker {
return &eventStoreInvoker{
invoker: invoker{next: next},
}
}
type EventProducer interface {
// Events returns the events that should be pushed to the event store after the command is executed.
// If the command does not produce events, it should `return nil, nil`.
Events(ctx context.Context, opts *InvokeOpts) ([]legacy_es.Command, error)
}
func (i *eventStoreInvoker) Invoke(ctx context.Context, executor Executor, opts *InvokeOpts) (err error) {
if _, ok := executor.(*batchExecutor); ok {
return i.collect(ctx, executor, opts)
}
if _, ok := executor.(EventProducer); !ok {
return i.execute(ctx, executor, opts)
}
return i.collect(ctx, executor, opts)
}
func (i *eventStoreInvoker) collect(ctx context.Context, executor Executor, opts *InvokeOpts) (err error) {
if i.collector == nil {
var close func(err error) error
close, err = i.ensureTx(ctx, opts)
if err != nil {
return err
}
defer func() {
if err != nil {
err = close(err)
return
}
commands := i.collector.commands()
if len(commands) == 0 {
err = close(nil)
return
}
err = eventstore.Publish(ctx, opts.LegacyEventstore(), opts.DB(), commands...)
err = close(err)
}()
i.collector = initEventCollector(i.invoker, nil, executor)
} else {
i.collector = i.collector.initSub(executor)
}
defer func() {
if i.collector.parent() == nil {
return
}
i.collector = i.collector.parent()
}()
err = i.execute(ctx, executor, opts)
if err != nil {
return err
}
return i.collector.collect(ctx, opts)
}
type eventCollector interface {
collect(ctx context.Context, opts *InvokeOpts) error
commands() []legacy_es.Command
initSub(executor Executor) eventCollector
parent() eventCollector
}
type batchCollector struct {
invoker
executor Executor
collectors []eventCollector
previous eventCollector
}
// collect implements [eventCollector].
func (b batchCollector) collect(ctx context.Context, opts *InvokeOpts) error {
return nil
}
// commands implements [eventCollector].
func (b batchCollector) commands() []legacy_es.Command {
var commands []legacy_es.Command
for _, collector := range b.collectors {
commands = append(commands, collector.commands()...)
}
return commands
}
// initSub implements [eventCollector].
func (b *batchCollector) initSub(executor Executor) eventCollector {
b.collectors = append(b.collectors, initEventCollector(b.invoker, b, executor))
return b.collectors[len(b.collectors)-1]
}
// parent implements [eventCollector].
func (b batchCollector) parent() eventCollector {
return b.previous
}
var _ eventCollector = (*batchCollector)(nil)
type commandCollector struct {
invoker
producer EventProducer
sub eventCollector
previous eventCollector
cmds []legacy_es.Command
}
// collect implements [eventCollector].
func (c *commandCollector) collect(ctx context.Context, opts *InvokeOpts) (err error) {
c.cmds, err = c.producer.Events(ctx, opts)
return err
}
// commands implements [eventCollector].
func (c commandCollector) commands() []legacy_es.Command {
if c.sub == nil {
return c.cmds
}
return append(c.cmds, c.sub.commands()...)
}
// initSub implements [eventCollector].
func (c *commandCollector) initSub(executor Executor) eventCollector {
c.sub = initEventCollector(c.invoker, c, executor)
return c.sub
}
// parent implements [eventCollector].
func (c commandCollector) parent() eventCollector {
return c.previous
}
var _ eventCollector = (*commandCollector)(nil)
func initEventCollector(i invoker, parent eventCollector, executor Executor) eventCollector {
if _, ok := executor.(*batchExecutor); ok {
return &batchCollector{
invoker: i,
executor: executor,
previous: parent,
}
}
return &commandCollector{
invoker: i,
producer: executor.(EventProducer),
previous: parent,
}
}