mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-16 02:47:41 +00:00
fix(projections): added check to make sure there cannot be 2 projections for the same table (#10439)
# Which Problems Are Solved It should not be possible to start 2 projections with the same name. If this happens, it can cause issues with the event store such as events being skipped/unprocessed and can be very hard/time-consuming to diagnose. # How the Problems Are Solved A check was added to make sure no 2 projections have the same table Closes https://github.com/zitadel/zitadel/issues/10453 --------- Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com>
This commit is contained in:
@@ -209,10 +209,18 @@ func Init(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Start(ctx context.Context) {
|
||||
func Start(ctx context.Context) error {
|
||||
projectionTableMap := make(map[string]bool, len(projections))
|
||||
for _, projection := range projections {
|
||||
table := projection.String()
|
||||
if projectionTableMap[table] {
|
||||
return fmt.Errorf("projeciton for %s already added", table)
|
||||
}
|
||||
projectionTableMap[table] = true
|
||||
|
||||
projection.Start(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProjectInstance(ctx context.Context) error {
|
||||
|
130
internal/query/projection/projection_mock.go
Normal file
130
internal/query/projection/projection_mock.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: projection.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -source projection.go -destination ./projection_mock.go -package projection
|
||||
//
|
||||
|
||||
// Package projection is a generated GoMock package.
|
||||
package projection
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
eventstore "github.com/zitadel/zitadel/internal/eventstore"
|
||||
handler "github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// Mockprojection is a mock of projection interface.
|
||||
type Mockprojection struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockprojectionMockRecorder
|
||||
}
|
||||
|
||||
// MockprojectionMockRecorder is the mock recorder for Mockprojection.
|
||||
type MockprojectionMockRecorder struct {
|
||||
mock *Mockprojection
|
||||
}
|
||||
|
||||
// NewMockprojection creates a new mock instance.
|
||||
func NewMockprojection(ctrl *gomock.Controller) *Mockprojection {
|
||||
mock := &Mockprojection{ctrl: ctrl}
|
||||
mock.recorder = &MockprojectionMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *Mockprojection) EXPECT() *MockprojectionMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Execute mocks base method.
|
||||
func (m *Mockprojection) Execute(ctx context.Context, startedEvent eventstore.Event) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Execute", ctx, startedEvent)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Execute indicates an expected call of Execute.
|
||||
func (mr *MockprojectionMockRecorder) Execute(ctx, startedEvent any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*Mockprojection)(nil).Execute), ctx, startedEvent)
|
||||
}
|
||||
|
||||
// Init mocks base method.
|
||||
func (m *Mockprojection) Init(ctx context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Init", ctx)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Init indicates an expected call of Init.
|
||||
func (mr *MockprojectionMockRecorder) Init(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*Mockprojection)(nil).Init), ctx)
|
||||
}
|
||||
|
||||
// ProjectionName mocks base method.
|
||||
func (m *Mockprojection) ProjectionName() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ProjectionName")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ProjectionName indicates an expected call of ProjectionName.
|
||||
func (mr *MockprojectionMockRecorder) ProjectionName() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectionName", reflect.TypeOf((*Mockprojection)(nil).ProjectionName))
|
||||
}
|
||||
|
||||
// Start mocks base method.
|
||||
func (m *Mockprojection) Start(ctx context.Context) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Start", ctx)
|
||||
}
|
||||
|
||||
// Start indicates an expected call of Start.
|
||||
func (mr *MockprojectionMockRecorder) Start(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*Mockprojection)(nil).Start), ctx)
|
||||
}
|
||||
|
||||
// String mocks base method.
|
||||
func (m *Mockprojection) String() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "String")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// String indicates an expected call of String.
|
||||
func (mr *MockprojectionMockRecorder) String() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*Mockprojection)(nil).String))
|
||||
}
|
||||
|
||||
// Trigger mocks base method.
|
||||
func (m *Mockprojection) Trigger(ctx context.Context, opts ...handler.TriggerOpt) (context.Context, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{ctx}
|
||||
for _, a := range opts {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Trigger", varargs...)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Trigger indicates an expected call of Trigger.
|
||||
func (mr *MockprojectionMockRecorder) Trigger(ctx any, opts ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{ctx}, opts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Trigger", reflect.TypeOf((*Mockprojection)(nil).Trigger), varargs...)
|
||||
}
|
69
internal/query/projection/projection_test.go
Normal file
69
internal/query/projection/projection_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/stretchr/testify/require"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestStart(t *testing.T) {
|
||||
duplicateName := gofakeit.Name()
|
||||
tests := []struct {
|
||||
name string
|
||||
projections func(t *testing.T) []projection
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
projections: func(t *testing.T) []projection {
|
||||
ctrl := gomock.NewController(t)
|
||||
projections := make([]projection, 5)
|
||||
|
||||
for i := range 5 {
|
||||
mock := NewMockprojection(ctrl)
|
||||
mock.EXPECT().Start(gomock.Any())
|
||||
mock.EXPECT().String().Return(gofakeit.Name())
|
||||
projections[i] = mock
|
||||
}
|
||||
|
||||
return projections
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same projection used twice error",
|
||||
projections: func(t *testing.T) []projection {
|
||||
projections := make([]projection, 5)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
mock := NewMockprojection(ctrl)
|
||||
mock.EXPECT().String().Return(duplicateName)
|
||||
mock.EXPECT().Start(gomock.Any())
|
||||
projections[0] = mock
|
||||
|
||||
for i := 1; i < 4; i++ {
|
||||
mock := NewMockprojection(ctrl)
|
||||
mock.EXPECT().String().Return(gofakeit.Name())
|
||||
mock.EXPECT().Start(gomock.Any())
|
||||
projections[i] = mock
|
||||
}
|
||||
|
||||
mock = NewMockprojection(ctrl)
|
||||
mock.EXPECT().String().Return(duplicateName)
|
||||
projections[4] = mock
|
||||
|
||||
return projections
|
||||
},
|
||||
err: fmt.Errorf("projeciton for %s already added", duplicateName),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
projections = tt.projections(t)
|
||||
err := Start(t.Context())
|
||||
require.Equal(t, tt.err, err)
|
||||
})
|
||||
}
|
||||
}
|
@@ -90,7 +90,10 @@ func StartQueries(
|
||||
return nil, err
|
||||
}
|
||||
if startProjections {
|
||||
projection.Start(ctx)
|
||||
err = projection.Start(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
repo.caches, err = startCaches(
|
||||
|
Reference in New Issue
Block a user