fix(permissions): chunked synchronization of role permission events (#9403)

# Which Problems Are Solved

Setup fails to push all role permission events when running Zitadel with
CockroachDB. `TransactionRetryError`s were visible in logs which finally
times out the setup job with `timeout: context deadline exceeded`

# How the Problems Are Solved

As suggested in the [Cockroach documentation](timeout: context deadline
exceeded), _"break down larger transactions"_. The commands to be pushed
for the role permissions are chunked in 50 events per push. This
chunking is only done with CockroachDB.

# Additional Changes

- gci run fixed some unrelated imports
- access to `command.Commands` for the setup job, so we can reuse the
sync logic.

# Additional Context

Closes #9293

---------

Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com>
This commit is contained in:
Tim Möhlmann
2025-02-26 18:06:50 +02:00
committed by GitHub
parent 77499ce603
commit e670b9126c
13 changed files with 461 additions and 169 deletions

View File

@@ -2,6 +2,7 @@ package command
import (
"context"
"fmt"
"io"
"os"
"testing"
@@ -13,6 +14,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/repository/permission"
"github.com/zitadel/zitadel/internal/repository/user"
)
@@ -29,6 +31,93 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}
func TestCommands_pushChunked(t *testing.T) {
aggregate := permission.NewAggregate("instanceID")
cmds := make([]eventstore.Command, 100)
for i := 0; i < 100; i++ {
cmds[i] = permission.NewAddedEvent(context.Background(), aggregate, "role", fmt.Sprintf("permission%d", i))
}
type args struct {
size uint16
}
tests := []struct {
name string
args args
eventstore func(*testing.T) *eventstore.Eventstore
wantEvents int
wantErr error
}{
{
name: "push error",
args: args{
size: 100,
},
eventstore: expectEventstore(
expectPushFailed(io.ErrClosedPipe, cmds...),
),
wantEvents: 0,
wantErr: io.ErrClosedPipe,
},
{
name: "single chunk",
args: args{
size: 100,
},
eventstore: expectEventstore(
expectPush(cmds...),
),
wantEvents: len(cmds),
},
{
name: "aligned chunks",
args: args{
size: 50,
},
eventstore: expectEventstore(
expectPush(cmds[0:50]...),
expectPush(cmds[50:100]...),
),
wantEvents: len(cmds),
},
{
name: "odd chunks",
args: args{
size: 30,
},
eventstore: expectEventstore(
expectPush(cmds[0:30]...),
expectPush(cmds[30:60]...),
expectPush(cmds[60:90]...),
expectPush(cmds[90:100]...),
),
wantEvents: len(cmds),
},
{
name: "partial error",
args: args{
size: 30,
},
eventstore: expectEventstore(
expectPush(cmds[0:30]...),
expectPush(cmds[30:60]...),
expectPushFailed(io.ErrClosedPipe, cmds[60:90]...),
),
wantEvents: len(cmds[0:60]),
wantErr: io.ErrClosedPipe,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.eventstore(t),
}
gotEvents, err := c.pushChunked(context.Background(), tt.args.size, cmds...)
require.ErrorIs(t, err, tt.wantErr)
assert.Len(t, gotEvents, tt.wantEvents)
})
}
}
func TestCommands_asyncPush(t *testing.T) {
// make sure the test terminates on deadlock
background := context.Background()