mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-23 03:47:43 +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>
(cherry picked from commit 10bd747105)
This commit is contained in:
@@ -205,10 +205,18 @@ func Init(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(ctx context.Context) {
|
func Start(ctx context.Context) error {
|
||||||
|
projectionTableMap := make(map[string]bool, len(projections))
|
||||||
for _, projection := range 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)
|
projection.Start(ctx)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProjectInstance(ctx context.Context) error {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if startProjections {
|
if startProjections {
|
||||||
projection.Start(ctx)
|
err = projection.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repo.caches, err = startCaches(
|
repo.caches, err = startCaches(
|
||||||
|
|||||||
Reference in New Issue
Block a user