mirror of
				https://github.com/zitadel/zitadel.git
				synced 2025-11-04 06:38:48 +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:
		@@ -207,10 +207,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