diff --git a/pkg/common/moerr/cause.go b/pkg/common/moerr/cause.go index 5fb309a3d7ff4..83eb822cb3daa 100644 --- a/pkg/common/moerr/cause.go +++ b/pkg/common/moerr/cause.go @@ -104,10 +104,11 @@ var ( CauseAllocateTasks = NewInternalError(context.Background(), "allocateTask") CauseTruncateTasks = NewInternalError(context.Background(), "truncateTasks") //pkg/incrservice - CauseDoAllocate = NewInternalError(context.Background(), "doAllocate") - CauseDoUpdate = NewInternalError(context.Background(), "doUpdate") - CauseDestroyTables = NewInternalError(context.Background(), "destroyTables") - CauseAllocate = NewInternalError(context.Background(), "allocate") + CauseDoAllocate = NewInternalError(context.Background(), "doAllocate") + CauseDoUpdate = NewInternalError(context.Background(), "doUpdate") + CauseDoForceSetOffset = NewInternalError(context.Background(), "doForceSetOffset") + CauseDestroyTables = NewInternalError(context.Background(), "destroyTables") + CauseAllocate = NewInternalError(context.Background(), "allocate") //pkg/lockservice CauseCleanCommitState = NewInternalError(context.Background(), "cleanCommitState") CauseValidateService = NewInternalError(context.Background(), "validateService") diff --git a/pkg/common/moerr/cause_test.go b/pkg/common/moerr/cause_test.go index 06fda44cb80af..1204af449f6b6 100644 --- a/pkg/common/moerr/cause_test.go +++ b/pkg/common/moerr/cause_test.go @@ -98,6 +98,7 @@ var causeArray = []error{ CauseDoAllocate, CauseDoUpdate, + CauseDoForceSetOffset, CauseDestroyTables, CauseAllocate, diff --git a/pkg/frontend/test/incrservice_mock.go b/pkg/frontend/test/incrservice_mock.go index 90e50336f4097..6753acf211cc2 100644 --- a/pkg/frontend/test/incrservice_mock.go +++ b/pkg/frontend/test/incrservice_mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: pkg/incrservice/types_2.go +// Source: pkg/incrservice/types.go // Package mock_frontend is a generated GoMock package. package mock_frontend @@ -93,34 +93,48 @@ func (mr *MockAutoIncrementServiceMockRecorder) Delete(ctx, tableID, txn interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAutoIncrementService)(nil).Delete), ctx, tableID, txn) } +// DiscardOffsetReset mocks base method. +func (m *MockAutoIncrementService) DiscardOffsetReset(ctx context.Context, tableID uint64, txn client.TxnOperator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DiscardOffsetReset", ctx, tableID, txn) + ret0, _ := ret[0].(error) + return ret0 +} + +// DiscardOffsetReset indicates an expected call of DiscardOffsetReset. +func (mr *MockAutoIncrementServiceMockRecorder) DiscardOffsetReset(ctx, tableID, txn interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiscardOffsetReset", reflect.TypeOf((*MockAutoIncrementService)(nil).DiscardOffsetReset), ctx, tableID, txn) +} + // GetLastAllocateTS mocks base method. -func (m *MockAutoIncrementService) GetLastAllocateTS(ctx context.Context, tableID uint64, colName string) (timestamp.Timestamp, error) { +func (m *MockAutoIncrementService) GetLastAllocateTS(ctx context.Context, tableID uint64, tableVersion uint32, txn client.TxnOperator, colName string) (timestamp.Timestamp, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLastAllocateTS", ctx, tableID, colName) + ret := m.ctrl.Call(m, "GetLastAllocateTS", ctx, tableID, tableVersion, txn, colName) ret0, _ := ret[0].(timestamp.Timestamp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetLastAllocateTS indicates an expected call of GetLastAllocateTS. -func (mr *MockAutoIncrementServiceMockRecorder) GetLastAllocateTS(ctx, tableID, colName interface{}) *gomock.Call { +func (mr *MockAutoIncrementServiceMockRecorder) GetLastAllocateTS(ctx, tableID, tableVersion, txn, colName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastAllocateTS", reflect.TypeOf((*MockAutoIncrementService)(nil).GetLastAllocateTS), ctx, tableID, colName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastAllocateTS", reflect.TypeOf((*MockAutoIncrementService)(nil).GetLastAllocateTS), ctx, tableID, tableVersion, txn, colName) } // InsertValues mocks base method. -func (m *MockAutoIncrementService) InsertValues(ctx context.Context, tableID uint64, vecs []*vector.Vector, rows int, estimate int64) (uint64, error) { +func (m *MockAutoIncrementService) InsertValues(ctx context.Context, tableID uint64, tableVersion uint32, txn client.TxnOperator, vecs []*vector.Vector, rows int, estimate int64) (uint64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertValues", ctx, tableID, vecs, rows, estimate) + ret := m.ctrl.Call(m, "InsertValues", ctx, tableID, tableVersion, txn, vecs, rows, estimate) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(error) return ret0, ret1 } // InsertValues indicates an expected call of InsertValues. -func (mr *MockAutoIncrementServiceMockRecorder) InsertValues(ctx, tableID, vecs, rows, estimate interface{}) *gomock.Call { +func (mr *MockAutoIncrementServiceMockRecorder) InsertValues(ctx, tableID, tableVersion, txn, vecs, rows, estimate interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertValues", reflect.TypeOf((*MockAutoIncrementService)(nil).InsertValues), ctx, tableID, vecs, rows, estimate) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertValues", reflect.TypeOf((*MockAutoIncrementService)(nil).InsertValues), ctx, tableID, tableVersion, txn, vecs, rows, estimate) } // Reload mocks base method. @@ -151,6 +165,20 @@ func (mr *MockAutoIncrementServiceMockRecorder) Reset(ctx, oldTableID, newTableI return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockAutoIncrementService)(nil).Reset), ctx, oldTableID, newTableID, keep, txn) } +// SetOffset mocks base method. +func (m *MockAutoIncrementService) SetOffset(ctx context.Context, tableID uint64, colName string, offset uint64, txn client.TxnOperator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetOffset", ctx, tableID, colName, offset, txn) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetOffset indicates an expected call of SetOffset. +func (mr *MockAutoIncrementServiceMockRecorder) SetOffset(ctx, tableID, colName, offset, txn interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetOffset", reflect.TypeOf((*MockAutoIncrementService)(nil).SetOffset), ctx, tableID, colName, offset, txn) +} + // UUID mocks base method. func (m *MockAutoIncrementService) UUID() string { m.ctrl.T.Helper() @@ -188,6 +216,18 @@ func (m *MockincrTableCache) EXPECT() *MockincrTableCacheMockRecorder { return m.recorder } +// acquire mocks base method. +func (m *MockincrTableCache) acquire() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "acquire") +} + +// acquire indicates an expected call of acquire. +func (mr *MockincrTableCacheMockRecorder) acquire() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "acquire", reflect.TypeOf((*MockincrTableCache)(nil).acquire)) +} + // adjust mocks base method. func (m *MockincrTableCache) adjust(ctx context.Context, cols []incrservice.AutoColumn) error { m.ctrl.T.Helper() @@ -287,6 +327,30 @@ func (mr *MockincrTableCacheMockRecorder) insertAutoValues(ctx, tableID, vecs, r return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "insertAutoValues", reflect.TypeOf((*MockincrTableCache)(nil).insertAutoValues), ctx, tableID, vecs, rows, estimate) } +// release mocks base method. +func (m *MockincrTableCache) release() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "release") +} + +// release indicates an expected call of release. +func (mr *MockincrTableCacheMockRecorder) release() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "release", reflect.TypeOf((*MockincrTableCache)(nil).release)) +} + +// retire mocks base method. +func (m *MockincrTableCache) retire() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "retire") +} + +// retire indicates an expected call of retire. +func (mr *MockincrTableCacheMockRecorder) retire() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "retire", reflect.TypeOf((*MockincrTableCache)(nil).retire)) +} + // table mocks base method. func (m *MockincrTableCache) table() uint64 { m.ctrl.T.Helper() @@ -301,6 +365,20 @@ func (mr *MockincrTableCacheMockRecorder) table() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "table", reflect.TypeOf((*MockincrTableCache)(nil).table)) } +// version mocks base method. +func (m *MockincrTableCache) version() uint32 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "version") + ret0, _ := ret[0].(uint32) + return ret0 +} + +// version indicates an expected call of version. +func (mr *MockincrTableCacheMockRecorder) version() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "version", reflect.TypeOf((*MockincrTableCache)(nil).version)) +} + // MockvalueAllocator is a mock of valueAllocator interface. type MockvalueAllocator struct { ctrl *gomock.Controller @@ -367,6 +445,20 @@ func (mr *MockvalueAllocatorMockRecorder) close() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "close", reflect.TypeOf((*MockvalueAllocator)(nil).close)) } +// forceSetOffset mocks base method. +func (m *MockvalueAllocator) forceSetOffset(ctx context.Context, tableID uint64, col string, offset uint64, txnOp client.TxnOperator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "forceSetOffset", ctx, tableID, col, offset, txnOp) + ret0, _ := ret[0].(error) + return ret0 +} + +// forceSetOffset indicates an expected call of forceSetOffset. +func (mr *MockvalueAllocatorMockRecorder) forceSetOffset(ctx, tableID, col, offset, txnOp interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "forceSetOffset", reflect.TypeOf((*MockvalueAllocator)(nil).forceSetOffset), ctx, tableID, col, offset, txnOp) +} + // updateMinValue mocks base method. func (m *MockvalueAllocator) updateMinValue(ctx context.Context, tableID uint64, col string, minValue uint64, txnOp client.TxnOperator) error { m.ctrl.T.Helper() @@ -461,6 +553,20 @@ func (mr *MockIncrValueStoreMockRecorder) Delete(ctx, tableID interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockIncrValueStore)(nil).Delete), ctx, tableID) } +// ForceSetOffset mocks base method. +func (m *MockIncrValueStore) ForceSetOffset(ctx context.Context, tableID uint64, colName string, offset uint64, txnOp client.TxnOperator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ForceSetOffset", ctx, tableID, colName, offset, txnOp) + ret0, _ := ret[0].(error) + return ret0 +} + +// ForceSetOffset indicates an expected call of ForceSetOffset. +func (mr *MockIncrValueStoreMockRecorder) ForceSetOffset(ctx, tableID, colName, offset, txnOp interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForceSetOffset", reflect.TypeOf((*MockIncrValueStore)(nil).ForceSetOffset), ctx, tableID, colName, offset, txnOp) +} + // GetColumns mocks base method. func (m *MockIncrValueStore) GetColumns(ctx context.Context, tableID uint64, txnOp client.TxnOperator) ([]incrservice.AutoColumn, error) { m.ctrl.T.Helper() @@ -476,6 +582,20 @@ func (mr *MockIncrValueStoreMockRecorder) GetColumns(ctx, tableID, txnOp interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetColumns", reflect.TypeOf((*MockIncrValueStore)(nil).GetColumns), ctx, tableID, txnOp) } +// SetOffset mocks base method. +func (m *MockIncrValueStore) SetOffset(ctx context.Context, tableID uint64, colName string, offset uint64, txnOp client.TxnOperator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetOffset", ctx, tableID, colName, offset, txnOp) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetOffset indicates an expected call of SetOffset. +func (mr *MockIncrValueStoreMockRecorder) SetOffset(ctx, tableID, colName, offset, txnOp interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetOffset", reflect.TypeOf((*MockIncrValueStore)(nil).SetOffset), ctx, tableID, colName, offset, txnOp) +} + // UpdateMinValue mocks base method. func (m *MockIncrValueStore) UpdateMinValue(ctx context.Context, tableID uint64, col string, minValue uint64, txnOp client.TxnOperator) error { m.ctrl.T.Helper() diff --git a/pkg/frontend/test/mock_incr/types.go b/pkg/frontend/test/mock_incr/types.go index f6e802a32b152..3253ed6c42374 100644 --- a/pkg/frontend/test/mock_incr/types.go +++ b/pkg/frontend/test/mock_incr/types.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: pkg/incrservice/types_2.go +// Source: pkg/incrservice/types.go // Package mock_incr is a generated GoMock package. package mock_incr @@ -93,34 +93,48 @@ func (mr *MockAutoIncrementServiceMockRecorder) Delete(ctx, tableID, txn interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAutoIncrementService)(nil).Delete), ctx, tableID, txn) } +// DiscardOffsetReset mocks base method. +func (m *MockAutoIncrementService) DiscardOffsetReset(ctx context.Context, tableID uint64, txn client.TxnOperator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DiscardOffsetReset", ctx, tableID, txn) + ret0, _ := ret[0].(error) + return ret0 +} + +// DiscardOffsetReset indicates an expected call of DiscardOffsetReset. +func (mr *MockAutoIncrementServiceMockRecorder) DiscardOffsetReset(ctx, tableID, txn interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiscardOffsetReset", reflect.TypeOf((*MockAutoIncrementService)(nil).DiscardOffsetReset), ctx, tableID, txn) +} + // GetLastAllocateTS mocks base method. -func (m *MockAutoIncrementService) GetLastAllocateTS(ctx context.Context, tableID uint64, colName string) (timestamp.Timestamp, error) { +func (m *MockAutoIncrementService) GetLastAllocateTS(ctx context.Context, tableID uint64, tableVersion uint32, txn client.TxnOperator, colName string) (timestamp.Timestamp, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetLastAllocateTS", ctx, tableID, colName) + ret := m.ctrl.Call(m, "GetLastAllocateTS", ctx, tableID, tableVersion, txn, colName) ret0, _ := ret[0].(timestamp.Timestamp) ret1, _ := ret[1].(error) return ret0, ret1 } // GetLastAllocateTS indicates an expected call of GetLastAllocateTS. -func (mr *MockAutoIncrementServiceMockRecorder) GetLastAllocateTS(ctx, tableID, colName interface{}) *gomock.Call { +func (mr *MockAutoIncrementServiceMockRecorder) GetLastAllocateTS(ctx, tableID, tableVersion, txn, colName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastAllocateTS", reflect.TypeOf((*MockAutoIncrementService)(nil).GetLastAllocateTS), ctx, tableID, colName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastAllocateTS", reflect.TypeOf((*MockAutoIncrementService)(nil).GetLastAllocateTS), ctx, tableID, tableVersion, txn, colName) } // InsertValues mocks base method. -func (m *MockAutoIncrementService) InsertValues(ctx context.Context, tableID uint64, vecs []*vector.Vector, rows int, estimate int64) (uint64, error) { +func (m *MockAutoIncrementService) InsertValues(ctx context.Context, tableID uint64, tableVersion uint32, txn client.TxnOperator, vecs []*vector.Vector, rows int, estimate int64) (uint64, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertValues", ctx, tableID, vecs, rows, estimate) + ret := m.ctrl.Call(m, "InsertValues", ctx, tableID, tableVersion, txn, vecs, rows, estimate) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(error) return ret0, ret1 } // InsertValues indicates an expected call of InsertValues. -func (mr *MockAutoIncrementServiceMockRecorder) InsertValues(ctx, tableID, vecs, rows, estimate interface{}) *gomock.Call { +func (mr *MockAutoIncrementServiceMockRecorder) InsertValues(ctx, tableID, tableVersion, txn, vecs, rows, estimate interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertValues", reflect.TypeOf((*MockAutoIncrementService)(nil).InsertValues), ctx, tableID, vecs, rows, estimate) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertValues", reflect.TypeOf((*MockAutoIncrementService)(nil).InsertValues), ctx, tableID, tableVersion, txn, vecs, rows, estimate) } // Reload mocks base method. @@ -151,6 +165,20 @@ func (mr *MockAutoIncrementServiceMockRecorder) Reset(ctx, oldTableID, newTableI return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockAutoIncrementService)(nil).Reset), ctx, oldTableID, newTableID, keep, txn) } +// SetOffset mocks base method. +func (m *MockAutoIncrementService) SetOffset(ctx context.Context, tableID uint64, colName string, offset uint64, txn client.TxnOperator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetOffset", ctx, tableID, colName, offset, txn) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetOffset indicates an expected call of SetOffset. +func (mr *MockAutoIncrementServiceMockRecorder) SetOffset(ctx, tableID, colName, offset, txn interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetOffset", reflect.TypeOf((*MockAutoIncrementService)(nil).SetOffset), ctx, tableID, colName, offset, txn) +} + // UUID mocks base method. func (m *MockAutoIncrementService) UUID() string { m.ctrl.T.Helper() @@ -188,6 +216,18 @@ func (m *MockincrTableCache) EXPECT() *MockincrTableCacheMockRecorder { return m.recorder } +// acquire mocks base method. +func (m *MockincrTableCache) acquire() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "acquire") +} + +// acquire indicates an expected call of acquire. +func (mr *MockincrTableCacheMockRecorder) acquire() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "acquire", reflect.TypeOf((*MockincrTableCache)(nil).acquire)) +} + // adjust mocks base method. func (m *MockincrTableCache) adjust(ctx context.Context, cols []incrservice.AutoColumn) error { m.ctrl.T.Helper() @@ -287,6 +327,30 @@ func (mr *MockincrTableCacheMockRecorder) insertAutoValues(ctx, tableID, vecs, r return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "insertAutoValues", reflect.TypeOf((*MockincrTableCache)(nil).insertAutoValues), ctx, tableID, vecs, rows, estimate) } +// release mocks base method. +func (m *MockincrTableCache) release() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "release") +} + +// release indicates an expected call of release. +func (mr *MockincrTableCacheMockRecorder) release() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "release", reflect.TypeOf((*MockincrTableCache)(nil).release)) +} + +// retire mocks base method. +func (m *MockincrTableCache) retire() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "retire") +} + +// retire indicates an expected call of retire. +func (mr *MockincrTableCacheMockRecorder) retire() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "retire", reflect.TypeOf((*MockincrTableCache)(nil).retire)) +} + // table mocks base method. func (m *MockincrTableCache) table() uint64 { m.ctrl.T.Helper() @@ -301,6 +365,20 @@ func (mr *MockincrTableCacheMockRecorder) table() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "table", reflect.TypeOf((*MockincrTableCache)(nil).table)) } +// version mocks base method. +func (m *MockincrTableCache) version() uint32 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "version") + ret0, _ := ret[0].(uint32) + return ret0 +} + +// version indicates an expected call of version. +func (mr *MockincrTableCacheMockRecorder) version() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "version", reflect.TypeOf((*MockincrTableCache)(nil).version)) +} + // MockvalueAllocator is a mock of valueAllocator interface. type MockvalueAllocator struct { ctrl *gomock.Controller @@ -367,6 +445,20 @@ func (mr *MockvalueAllocatorMockRecorder) close() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "close", reflect.TypeOf((*MockvalueAllocator)(nil).close)) } +// forceSetOffset mocks base method. +func (m *MockvalueAllocator) forceSetOffset(ctx context.Context, tableID uint64, col string, offset uint64, txnOp client.TxnOperator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "forceSetOffset", ctx, tableID, col, offset, txnOp) + ret0, _ := ret[0].(error) + return ret0 +} + +// forceSetOffset indicates an expected call of forceSetOffset. +func (mr *MockvalueAllocatorMockRecorder) forceSetOffset(ctx, tableID, col, offset, txnOp interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "forceSetOffset", reflect.TypeOf((*MockvalueAllocator)(nil).forceSetOffset), ctx, tableID, col, offset, txnOp) +} + // updateMinValue mocks base method. func (m *MockvalueAllocator) updateMinValue(ctx context.Context, tableID uint64, col string, minValue uint64, txnOp client.TxnOperator) error { m.ctrl.T.Helper() @@ -461,6 +553,20 @@ func (mr *MockIncrValueStoreMockRecorder) Delete(ctx, tableID interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockIncrValueStore)(nil).Delete), ctx, tableID) } +// ForceSetOffset mocks base method. +func (m *MockIncrValueStore) ForceSetOffset(ctx context.Context, tableID uint64, colName string, offset uint64, txnOp client.TxnOperator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ForceSetOffset", ctx, tableID, colName, offset, txnOp) + ret0, _ := ret[0].(error) + return ret0 +} + +// ForceSetOffset indicates an expected call of ForceSetOffset. +func (mr *MockIncrValueStoreMockRecorder) ForceSetOffset(ctx, tableID, colName, offset, txnOp interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForceSetOffset", reflect.TypeOf((*MockIncrValueStore)(nil).ForceSetOffset), ctx, tableID, colName, offset, txnOp) +} + // GetColumns mocks base method. func (m *MockIncrValueStore) GetColumns(ctx context.Context, tableID uint64, txnOp client.TxnOperator) ([]incrservice.AutoColumn, error) { m.ctrl.T.Helper() @@ -476,6 +582,20 @@ func (mr *MockIncrValueStoreMockRecorder) GetColumns(ctx, tableID, txnOp interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetColumns", reflect.TypeOf((*MockIncrValueStore)(nil).GetColumns), ctx, tableID, txnOp) } +// SetOffset mocks base method. +func (m *MockIncrValueStore) SetOffset(ctx context.Context, tableID uint64, colName string, offset uint64, txnOp client.TxnOperator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetOffset", ctx, tableID, colName, offset, txnOp) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetOffset indicates an expected call of SetOffset. +func (mr *MockIncrValueStoreMockRecorder) SetOffset(ctx, tableID, colName, offset, txnOp interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetOffset", reflect.TypeOf((*MockIncrValueStore)(nil).SetOffset), ctx, tableID, colName, offset, txnOp) +} + // UpdateMinValue mocks base method. func (m *MockIncrValueStore) UpdateMinValue(ctx context.Context, tableID uint64, col string, minValue uint64, txnOp client.TxnOperator) error { m.ctrl.T.Helper() diff --git a/pkg/incrservice/allocator.go b/pkg/incrservice/allocator.go index 62aa60eb38564..b90cac1a08887 100644 --- a/pkg/incrservice/allocator.go +++ b/pkg/incrservice/allocator.go @@ -16,6 +16,7 @@ package incrservice import ( "context" + "sync" "sync/atomic" "time" @@ -30,14 +31,18 @@ import ( ) const ( - defaultAllocateTimeout = time.Minute * 3 + defaultAllocateTimeout = time.Minute * 3 + defaultForceSetOffsetTimeout = time.Second * 10 ) type allocator struct { logger *log.MOLogger store IncrValueStore c chan action + done chan struct{} stopper *stopper.Stopper + mu sync.RWMutex + closed bool } func newValueAllocator( @@ -47,6 +52,7 @@ func newValueAllocator( a := &allocator{ logger: getLogger(sid).Named("incrservice"), c: make(chan action, 1024), + done: make(chan struct{}), stopper: stopper.NewStopper("valueAllocator"), store: store, } @@ -96,7 +102,13 @@ func (a *allocator) allocate( if err != nil { return 0, 0, timestamp.Timestamp{}, err } - <-c + select { + case <-c: + case <-ctx.Done(): + return 0, 0, timestamp.Timestamp{}, context.Cause(ctx) + case <-a.done: + return 0, 0, timestamp.Timestamp{}, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } if v := err2.Load(); v != nil { if e, ok := v.(error); ok && e != nil { return 0, 0, timestamp.Timestamp{}, e @@ -116,7 +128,7 @@ func (a *allocator) asyncAllocate( if err != nil { return err } - a.c <- action{ + return a.enqueue(ctx, action{ ctx: ctx, txnOp: txnOp, accountID: accountId, @@ -124,8 +136,7 @@ func (a *allocator) asyncAllocate( tableID: tableID, col: col, count: count, - applyAllocate: apply} - return nil + applyAllocate: apply}) } func (a *allocator) updateMinValue( @@ -145,7 +156,7 @@ func (a *allocator) updateMinValue( err = e close(c) } - a.c <- action{ + if err := a.enqueue(ctx, action{ ctx: ctx, txnOp: txnOp, accountID: accountId, @@ -154,11 +165,69 @@ func (a *allocator) updateMinValue( col: col, minValue: minValue, applyUpdate: fn, + }); err != nil { + return err + } + select { + case <-c: + case <-ctx.Done(): + return context.Cause(ctx) + case <-a.done: + return moerr.NewTxnNeedRetryWithDefChanged(ctx) } - <-c return err } +func (a *allocator) forceSetOffset( + ctx context.Context, + tableID uint64, + col string, + offset uint64, + txnOp client.TxnOperator, +) error { + accountID, err := getAccountID(ctx) + if err != nil { + return err + } + done := make(chan struct{}) + if err := a.enqueue(ctx, action{ + ctx: ctx, + txnOp: txnOp, + accountID: accountID, + actionType: forceUpdateType, + tableID: tableID, + col: col, + minValue: offset, + applyUpdate: func(e error) { err = e; close(done) }, + }); err != nil { + return err + } + select { + case <-done: + return err + case <-ctx.Done(): + return context.Cause(ctx) + case <-a.done: + return moerr.NewTxnNeedRetryWithDefChanged(ctx) + } +} + +func (a *allocator) enqueue(ctx context.Context, act action) error { + a.mu.RLock() + defer a.mu.RUnlock() + if a.closed { + return moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + select { + case a.c <- act: + return nil + case <-ctx.Done(): + return context.Cause(ctx) + case <-a.done: + return moerr.NewTxnNeedRetryWithDefChanged(ctx) + } +} + func (a *allocator) run(ctx context.Context) { for { select { @@ -170,11 +239,32 @@ func (a *allocator) run(ctx context.Context) { a.doAllocate(act) case updateType: a.doUpdate(act) + case forceUpdateType: + a.doForceSetOffset(act) + case barrierType: + act.applyUpdate(nil) } } } } +func (a *allocator) doForceSetOffset(act action) { + baseCtx := act.ctx + if baseCtx == nil { + baseCtx = context.Background() + } + ctx := defines.AttachAccountId(baseCtx, act.accountID) + ctx, cancel := context.WithTimeoutCause(ctx, defaultForceSetOffsetTimeout, moerr.CauseDoForceSetOffset) + defer cancel() + if err := context.Cause(ctx); err != nil { + act.applyUpdate(err) + return + } + err := a.store.ForceSetOffset(ctx, act.tableID, act.col, act.minValue, act.txnOp) + err = moerr.AttachCause(ctx, err) + act.applyUpdate(err) +} + func (a *allocator) doAllocate(act action) { baseCtx := act.ctx if baseCtx == nil { @@ -232,13 +322,28 @@ func (a *allocator) doUpdate(act action) { } func (a *allocator) close() { + a.mu.Lock() + if a.closed { + a.mu.Unlock() + return + } + drained := make(chan struct{}) + a.c <- action{ + actionType: barrierType, + applyUpdate: func(error) { close(drained) }, + } + <-drained + a.closed = true + close(a.done) + a.mu.Unlock() a.stopper.Stop() - close(a.c) } var ( - allocType = 0 - updateType = 1 + allocType = 0 + updateType = 1 + forceUpdateType = 2 + barrierType = 3 ) type action struct { diff --git a/pkg/incrservice/allocator_test.go b/pkg/incrservice/allocator_test.go index 404e6f76fca41..9e870966573a7 100644 --- a/pkg/incrservice/allocator_test.go +++ b/pkg/incrservice/allocator_test.go @@ -17,7 +17,9 @@ package incrservice import ( "context" "sync" + "sync/atomic" "testing" + "time" "github.com/lni/goutils/leaktest" "github.com/matrixorigin/matrixone/pkg/catalog" @@ -182,6 +184,40 @@ func TestAsyncAlloc(t *testing.T) { }) } +func TestAllocatorEnqueueHoldsCloseLockUntilSendCompletes(t *testing.T) { + a := &allocator{c: make(chan action, 1), done: make(chan struct{})} + a.c <- action{} + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + result := make(chan error, 1) + go func() { result <- a.enqueue(ctx, action{}) }() + + require.Eventually(t, func() bool { + if a.mu.TryLock() { + a.mu.Unlock() + return false + } + return true + }, time.Second, time.Millisecond) + cancel() + require.ErrorIs(t, <-result, context.Canceled) +} + +func TestAllocatorCloseDrainsAcceptedActions(t *testing.T) { + runAllocatorTests(t, func(va valueAllocator) { + a := va.(*allocator) + ctx := defines.AttachAccountId(context.Background(), catalog.System_Account) + require.NoError(t, a.store.Create(ctx, 0, []AutoColumn{{ColName: "auto", Step: 1}}, nil)) + var completed atomic.Int64 + for range 100 { + require.NoError(t, a.asyncAllocate(ctx, 0, "auto", 1, nil, + func(uint64, uint64, timestamp.Timestamp, error) { completed.Add(1) })) + } + a.close() + require.Equal(t, int64(100), completed.Load()) + }) +} + func runAllocatorTests( t *testing.T, fn func(valueAllocator), diff --git a/pkg/incrservice/column_cache.go b/pkg/incrservice/column_cache.go index 1c635c605acef..93d6d85a7f3b1 100644 --- a/pkg/incrservice/column_cache.go +++ b/pkg/incrservice/column_cache.go @@ -39,14 +39,17 @@ var ( type columnCache struct { sync.RWMutex - logger *log.MOLogger - col AutoColumn - cfg Config - ranges *ranges - allocator valueAllocator - allocating bool - allocatingC chan error - overflow bool + logger *log.MOLogger + col AutoColumn + cfg Config + ranges *ranges + allocator valueAllocator + allocating bool + allocatingC chan error + overflow bool + terminal bool + terminalValue uint64 + terminalTS timestamp.Timestamp // For the load scenario, if the machine is good enough, there will be very many goroutines to // concurrently fetch the value of the self-increasing column, which will immediately trigger // the cache of the self-increasing column to be insufficient and thus go to the store to allocate @@ -57,6 +60,7 @@ type columnCache struct { concurrencyApply atomic.Uint64 allocateCount atomic.Uint64 committed bool + retired bool } func newColumnCache( @@ -93,7 +97,13 @@ func (col *columnCache) current(ctx context.Context) (uint64, error) { if err := col.waitPrevAllocatingLocked(ctx); err != nil { return 0, err } - return col.ranges.current(), nil + if v := col.ranges.current(); v != 0 { + return v, nil + } + if col.terminal { + return col.terminalValue, nil + } + return 0, nil } func (col *columnCache) insertAutoValues( @@ -270,8 +280,18 @@ func (col *columnCache) updateTo( col.Lock() contains := col.ranges.updateTo(manualValue) + if col.terminal && manualValue >= col.terminalValue { + col.terminal = false + col.terminalValue = 0 + col.terminalTS = timestamp.Timestamp{} + col.overflow = true + contains = true + } // mark col next() is overflow if manualValue == math.MaxUint64 { + col.terminal = false + col.terminalValue = 0 + col.terminalTS = timestamp.Timestamp{} col.overflow = true } col.Unlock() @@ -310,7 +330,7 @@ func (col *columnCache) applyAutoValues( return true, nil } - if col.ranges.empty() { + if col.ranges.empty() && !col.terminal { if err := col.allocateLocked(ctx, tableID, rows, cul, txnOp); err != nil { return false, err } @@ -335,7 +355,15 @@ func (col *columnCache) applyAutoValues( if overflow { return apply(i, 0) } - if err := apply(i, col.ranges.next()); err != nil { + value := col.ranges.next() + if value == 0 && col.terminal { + value = col.terminalValue + col.terminal = false + col.terminalValue = 0 + col.terminalTS = timestamp.Timestamp{} + col.overflow = true + } + if err := apply(i, value); err != nil { return err } } @@ -349,8 +377,11 @@ func (col *columnCache) preAllocate( txnOp client.TxnOperator) { col.Lock() defer col.Unlock() + if col.retired { + return + } - if col.ranges.left() >= count { + if col.ranges.left() >= count || col.terminal { return } @@ -363,7 +394,7 @@ func (col *columnCache) preAllocate( if col.cfg.CountPerAllocate > count { count = col.cfg.CountPerAllocate } - col.allocator.asyncAllocate( + err := col.allocator.asyncAllocate( ctx, tableID, col.col.ColName, @@ -376,6 +407,9 @@ func (col *columnCache) preAllocate( col.applyAllocate(0, 0, timestamp.Timestamp{}, err) } }) + if err != nil { + col.applyAllocateLocked(0, 0, timestamp.Timestamp{}, err) + } } func (col *columnCache) allocateLocked( @@ -387,6 +421,9 @@ func (col *columnCache) allocateLocked( if err := col.waitPrevAllocatingLocked(ctx); err != nil { return err } + if col.retired { + return moerr.NewTxnNeedRetryWithDefChanged(ctx) + } col.allocating = true col.allocatingC = make(chan error, 1) @@ -425,9 +462,10 @@ func (col *columnCache) allocateLocked( func (col *columnCache) maybeAllocate(ctx context.Context, tableID uint64, txnOp client.TxnOperator) error { col.Lock() committed := col.committed - low := col.ranges.left() <= col.cfg.LowCapacity + low := col.ranges.left() <= col.cfg.LowCapacity && !col.terminal + retired := col.retired col.Unlock() - if low && committed { + if low && committed && !retired { accountId, err := defines.GetAccountId(ctx) if err != nil { return err @@ -440,6 +478,12 @@ func (col *columnCache) maybeAllocate(ctx context.Context, tableID uint64, txnOp return nil } +func (col *columnCache) retire() { + col.Lock() + col.retired = true + col.Unlock() +} + func (col *columnCache) applyAllocate( from uint64, to uint64, @@ -463,13 +507,30 @@ func (col *columnCache) applyAllocateLocked( } } - if to > from { + // A wrapped exclusive upper bound means the allocation reached the end of + // uint64. Keep its final value separately because max+step is not representable. + if to < from { + terminalValue := to - col.col.Step + if from < terminalValue { + col.ranges.addWithTimestamp(from, terminalValue, allocateAt) + } + col.terminal = true + col.terminalValue = terminalValue + col.terminalTS = allocateAt + } else if to > from { col.ranges.addWithTimestamp(from, to, allocateAt) } close(col.allocatingC) col.allocating = false } +func (col *columnCache) oldestAllocateAtLocked() timestamp.Timestamp { + if !col.ranges.empty() { + return col.ranges.oldestAllocateAt() + } + return col.terminalTS +} + func (col *columnCache) waitPrevAllocatingLocked(ctx context.Context) error { for { if !col.allocating { diff --git a/pkg/incrservice/column_cache_test.go b/pkg/incrservice/column_cache_test.go index c4cceb08af31a..bf9bda8c8ba15 100644 --- a/pkg/incrservice/column_cache_test.go +++ b/pkg/incrservice/column_cache_test.go @@ -625,3 +625,32 @@ func TestLastAllocateAtEmptyInitial(t *testing.T) { }, ) } + +func TestTerminalValueRetainsAllocateTimestamp(t *testing.T) { + ts := timestamp.Timestamp{PhysicalTime: 1000, LogicalTime: 1} + cc := &columnCache{ + col: AutoColumn{Step: 1}, + ranges: &ranges{step: 1}, + allocatingC: make(chan error, 1), + } + + cc.applyAllocateLocked(math.MaxUint64, 0, ts, nil) + + require.True(t, cc.terminal) + require.Equal(t, uint64(math.MaxUint64), cc.terminalValue) + require.Equal(t, ts, cc.oldestAllocateAtLocked()) +} + +func TestWrappedAllocationPreservesAllTerminalValues(t *testing.T) { + cc := &columnCache{ + col: AutoColumn{Step: 1}, + ranges: &ranges{step: 1}, + allocatingC: make(chan error, 1), + } + + cc.applyAllocateLocked(math.MaxUint64-1, 0, timestamp.Timestamp{}, nil) + + require.Equal(t, uint64(math.MaxUint64-1), cc.ranges.next()) + require.True(t, cc.terminal) + require.Equal(t, uint64(math.MaxUint64), cc.terminalValue) +} diff --git a/pkg/incrservice/private_table_cache.go b/pkg/incrservice/private_table_cache.go new file mode 100644 index 0000000000000..2db15afb65e3a --- /dev/null +++ b/pkg/incrservice/private_table_cache.go @@ -0,0 +1,202 @@ +// Copyright 2026 Matrix Origin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package incrservice + +import ( + "context" + "sync" + + "github.com/matrixorigin/matrixone/pkg/common/moerr" + "github.com/matrixorigin/matrixone/pkg/container/vector" + "github.com/matrixorigin/matrixone/pkg/pb/timestamp" +) + +// lazyPrivateTableCache keeps an ALTER reset transaction-private without +// reserving an allocation range until that same transaction actually needs an +// auto value. This preserves ALTER-only next-value semantics while still +// making ALTER followed by INSERT in one transaction observe the reset. +type lazyPrivateTableCache struct { + tableID uint64 + cols []AutoColumn + build func(context.Context) (incrTableCache, error) + + lifecycle struct { + sync.Mutex + users int + retired bool + closed bool + } + mu struct { + sync.Mutex + cache incrTableCache + build *privateCacheBuild + } +} + +type privateCacheBuild struct { + ready chan struct{} + cache incrTableCache + err error +} + +func newLazyPrivateTableCache( + tableID uint64, + cols []AutoColumn, + build func(context.Context) (incrTableCache, error), +) incrTableCache { + return &lazyPrivateTableCache{ + tableID: tableID, cols: cols, build: build, + } +} + +func (c *lazyPrivateTableCache) load(ctx context.Context) (incrTableCache, error) { + c.lifecycle.Lock() + retired := c.lifecycle.retired + c.lifecycle.Unlock() + if retired { + return nil, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + + c.mu.Lock() + if c.mu.cache != nil { + cache := c.mu.cache + cache.acquire() + c.mu.Unlock() + return cache, nil + } + if c.mu.build != nil { + build := c.mu.build + c.mu.Unlock() + select { + case <-build.ready: + if build.err != nil { + return nil, build.err + } + // Serialize the acquire with retire. A completed generation can + // be retired before this waiter is scheduled after ready closes. + c.mu.Lock() + if c.mu.cache != build.cache { + c.mu.Unlock() + return nil, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + cache := build.cache + cache.acquire() + c.mu.Unlock() + return cache, nil + case <-ctx.Done(): + return nil, ctx.Err() + } + } + build := &privateCacheBuild{ready: make(chan struct{})} + c.mu.build = build + c.mu.Unlock() + + cache, err := c.build(ctx) + + c.mu.Lock() + c.lifecycle.Lock() + retired = c.lifecycle.retired + c.lifecycle.Unlock() + if err == nil && !retired { + c.mu.cache = cache + build.cache = cache + cache.acquire() + } else if err != nil { + build.err = err + } else { + build.err = moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + c.mu.build = nil + close(build.ready) + c.mu.Unlock() + if err != nil { + return nil, err + } + if retired { + cache.retire() + return nil, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + return cache, nil +} + +func (c *lazyPrivateTableCache) table() uint64 { return c.tableID } +func (c *lazyPrivateTableCache) epoch() uint32 { return 0 } +func (c *lazyPrivateTableCache) columns() []AutoColumn { + return c.cols +} +func (c *lazyPrivateTableCache) acquire() { + c.lifecycle.Lock() + c.lifecycle.users++ + c.lifecycle.Unlock() +} +func (c *lazyPrivateTableCache) release() { + c.lifecycle.Lock() + c.lifecycle.users-- + c.lifecycle.Unlock() +} +func (c *lazyPrivateTableCache) retire() { + c.lifecycle.Lock() + if c.lifecycle.retired { + c.lifecycle.Unlock() + return + } + c.lifecycle.retired = true + c.lifecycle.closed = true + c.lifecycle.Unlock() + c.mu.Lock() + cache := c.mu.cache + c.mu.cache = nil + c.mu.Unlock() + if cache != nil { + cache.retire() + } +} +func (c *lazyPrivateTableCache) commit() { panic("private reset cache cannot be committed") } +func (c *lazyPrivateTableCache) insertAutoValues(ctx context.Context, tableID uint64, vecs []*vector.Vector, rows int, estimate int64) (uint64, error) { + cache, err := c.load(ctx) + if err != nil { + return 0, err + } + defer cache.release() + return cache.insertAutoValues(ctx, tableID, vecs, rows, estimate) +} +func (c *lazyPrivateTableCache) currentValue(ctx context.Context, tableID uint64, col string) (uint64, error) { + cache, err := c.load(ctx) + if err != nil { + return 0, err + } + defer cache.release() + return cache.currentValue(ctx, tableID, col) +} +func (c *lazyPrivateTableCache) getLastAllocateTS(ctx context.Context, colName string) (timestamp.Timestamp, error) { + cache, err := c.load(ctx) + if err != nil { + return timestamp.Timestamp{}, err + } + defer cache.release() + return cache.getLastAllocateTS(ctx, colName) +} +func (c *lazyPrivateTableCache) adjust(ctx context.Context, cols []AutoColumn) error { + cache, err := c.load(ctx) + if err != nil { + return err + } + defer cache.release() + return cache.adjust(ctx, cols) +} +func (c *lazyPrivateTableCache) close() error { + c.retire() + return nil +} diff --git a/pkg/incrservice/service.go b/pkg/incrservice/service.go index ad32d9afb4397..af81250521ce5 100644 --- a/pkg/incrservice/service.go +++ b/pkg/incrservice/service.go @@ -37,6 +37,26 @@ var ( lazyDeleteInterval = time.Second * 10 ) +type privateResetKey struct { + txnID string + tableID uint64 +} + +type privateResetRegistration struct { + ready chan struct{} +} + +type privateResetCallback struct { + key privateResetKey + registration *privateResetRegistration +} + +type txnEpochCacheCallback struct { + tableID uint64 + cache incrTableCache + registration *privateResetRegistration +} + type service struct { sid string logger *log.MOLogger @@ -44,14 +64,20 @@ type service struct { store IncrValueStore allocator valueAllocator stopper *stopper.Stopper + builders sync.WaitGroup mu struct { sync.Mutex - closed bool - destroyed map[uint64]deleteCtx - tables map[uint64]incrTableCache - creates map[string][]uint64 - deletes map[string][]deleteCtx + closed bool + destroyed map[uint64]deleteCtx + tables map[uint64]incrTableCache + generation map[uint64]uint64 + generationBuilds map[uint64]uint64 + private map[privateResetKey]incrTableCache + privateCallbacks map[privateResetKey]*privateResetRegistration + createdResets map[privateResetKey]incrTableCache + creates map[string][]uint64 + deletes map[string][]deleteCtx } } @@ -72,6 +98,11 @@ func NewIncrService( } s.mu.destroyed = make(map[uint64]deleteCtx) s.mu.tables = make(map[uint64]incrTableCache, 1024) + s.mu.generation = make(map[uint64]uint64, 1024) + s.mu.generationBuilds = make(map[uint64]uint64) + s.mu.private = make(map[privateResetKey]incrTableCache) + s.mu.privateCallbacks = make(map[privateResetKey]*privateResetRegistration) + s.mu.createdResets = make(map[privateResetKey]incrTableCache) s.mu.creates = make(map[string][]uint64, 1024) s.mu.deletes = make(map[string][]deleteCtx, 1024) if err := s.stopper.RunTask(s.destroyTables); err != nil { @@ -112,6 +143,7 @@ func (s *service) Create( ctx, s.sid, tableID, + 0, cols, s.cfg, s.allocator, @@ -208,15 +240,20 @@ func (s *service) Delete( func (s *service) GetLastAllocateTS( ctx context.Context, tableID uint64, + autoIncrEpoch uint32, + txnOp client.TxnOperator, colName string, ) (timestamp.Timestamp, error) { - tc, err := s.getCommittedTableCache( + tc, err := s.acquireTableCacheForEpoch( ctx, - tableID) + tableID, + autoIncrEpoch, + txnOp) if err != nil { return timestamp.Timestamp{}, err } - ts, err := tc.getLastAllocateTS(colName) + defer tc.release() + ts, err := tc.getLastAllocateTS(ctx, colName) if err != nil { return timestamp.Timestamp{}, err } @@ -227,16 +264,21 @@ func (s *service) GetLastAllocateTS( func (s *service) InsertValues( ctx context.Context, tableID uint64, + autoIncrEpoch uint32, + txnOp client.TxnOperator, vecs []*vector.Vector, rows int, estimate int64, ) (uint64, error) { - ts, err := s.getCommittedTableCache( + ts, err := s.acquireTableCacheForEpoch( ctx, - tableID) + tableID, + autoIncrEpoch, + txnOp) if err != nil { return 0, err } + defer ts.release() return ts.insertAutoValues( ctx, tableID, @@ -250,12 +292,13 @@ func (s *service) CurrentValue( ctx context.Context, tableID uint64, col string) (uint64, error) { - ts, err := s.getCommittedTableCache( + ts, err := s.acquireCommittedTableCache( ctx, tableID) if err != nil { return 0, err } + defer ts.release() return ts.currentValue(ctx, tableID, col) } @@ -264,42 +307,437 @@ func (s *service) Reload( tableID uint64, ) error { s.mu.Lock() - defer s.mu.Unlock() - + if s.mu.closed { + s.mu.Unlock() + return moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + s.bumpGenerationLocked(tableID) c, ok := s.mu.tables[tableID] if !ok { + s.mu.Unlock() + return nil + } + + // drop cache, will be reloaded when next query + delete(s.mu.tables, tableID) + s.mu.Unlock() + c.retire() + return nil +} + +func (s *service) SetOffset( + ctx context.Context, + tableID uint64, + colName string, + offset uint64, + txnOp client.TxnOperator, +) error { + var ( + txnKey string + ownedCreate bool + createCache incrTableCache + createEpoch uint32 + createGeneration uint64 + createResetKey privateResetKey + staleCreateCache incrTableCache + trackGeneration bool + ) + + s.mu.Lock() + if s.mu.closed { + s.mu.Unlock() + return moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + s.builders.Add(1) + if txnOp != nil { + txnKey = string(txnOp.Txn().ID) + ownedCreate = s.ownsCreateLocked(txnKey, tableID) + if ownedCreate { + createResetKey = privateResetKey{txnID: txnKey, tableID: tableID} + staleCreateCache = s.mu.createdResets[createResetKey] + delete(s.mu.createdResets, createResetKey) + createCache = s.mu.tables[tableID] + if createCache != nil { + createEpoch = createCache.epoch() + s.startGenerationBuildLocked(tableID) + trackGeneration = true + createGeneration = s.bumpGenerationLocked(tableID) + } + } + } + s.mu.Unlock() + defer s.builders.Done() + if trackGeneration { + defer s.finishGenerationBuild(tableID) + } + if staleCreateCache != nil { + staleCreateCache.retire() + } + + if ownedCreate { + if createCache == nil { + return moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + } else { + if err := s.Reload(ctx, tableID); err != nil { + return err + } + } + + // ALTER TABLE AUTO_INCREMENT explicitly resets the next value. The caller + // has already checked table data and holds the DDL lock, so bypass the + // store-level monotonic guard that protects normal pre-allocation updates. + if err := s.allocator.forceSetOffset(ctx, tableID, colName, offset, txnOp); err != nil { + return err + } + if txnOp == nil { return nil } - if err := c.close(); err != nil { + cols, err := s.store.GetColumns(ctx, tableID, txnOp) + if err != nil { return err } + if len(cols) == 0 { + return moerr.NewNoSuchTableNoCtx("", fmt.Sprintf("%d", tableID)) + } - // drop cache, will be reloaded when next query - delete(s.mu.tables, tableID) + if ownedCreate { + // CREATE TABLE (including clone/copy ALTER) is tracked by + // handleCreatesLocked. Publish the post-reset cache through that path so + // the committed table cannot retain its pre-reset range. + replacement, err := newTableCache( + ctx, + s.sid, + tableID, + createEpoch, + cols, + s.cfg, + s.allocator, + txnOp, + false, + ) + if err != nil { + return err + } + + s.mu.Lock() + if s.mu.closed || + !s.ownsCreateLocked(txnKey, tableID) || + s.mu.generation[tableID] != createGeneration || + s.mu.tables[tableID] != createCache { + s.mu.Unlock() + replacement.retire() + return moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + s.mu.tables[tableID] = replacement + s.mu.createdResets[createResetKey] = createCache + s.mu.Unlock() + return nil + } + + private := newLazyPrivateTableCache( + tableID, + cols, + func(buildCtx context.Context) (incrTableCache, error) { + return s.buildPrivateTableCache( + buildCtx, + func() (incrTableCache, error) { + return newTableCache( + buildCtx, s.sid, tableID, 0, cols, s.cfg, s.allocator, txnOp, false) + }) + }) + if err := s.installPrivateReset(ctx, tableID, txnOp, private); err != nil { + return err + } return nil } -func (s *service) Close() { - s.stopper.Stop() +func (s *service) ownsCreateLocked(txnKey string, tableID uint64) bool { + for _, id := range s.mu.creates[txnKey] { + if id == tableID { + return true + } + } + return false +} + +func (s *service) startGenerationBuildLocked(tableID uint64) uint64 { + s.mu.generationBuilds[tableID]++ + return s.mu.generation[tableID] +} + +func (s *service) finishGenerationBuild(tableID uint64) { + s.mu.Lock() + if s.mu.generationBuilds[tableID] <= 1 { + delete(s.mu.generationBuilds, tableID) + delete(s.mu.generation, tableID) + } else { + s.mu.generationBuilds[tableID]-- + } + s.mu.Unlock() +} +func (s *service) bumpGenerationLocked(tableID uint64) uint64 { + if s.mu.generationBuilds[tableID] == 0 { + delete(s.mu.generation, tableID) + return 0 + } + s.mu.generation[tableID]++ + return s.mu.generation[tableID] +} + +func (s *service) buildPrivateTableCache( + ctx context.Context, + build func() (incrTableCache, error), +) (incrTableCache, error) { + s.mu.Lock() + if s.mu.closed { + s.mu.Unlock() + return nil, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + s.builders.Add(1) + s.mu.Unlock() + defer s.builders.Done() + + cache, err := build() + if err != nil { + return nil, err + } + s.mu.Lock() + closed := s.mu.closed + s.mu.Unlock() + if closed { + cache.retire() + return nil, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + return cache, nil +} + +func (s *service) DiscardOffsetReset( + ctx context.Context, + tableID uint64, + txnOp client.TxnOperator, +) error { + if txnOp == nil { + return nil + } + key := privateResetKey{txnID: string(txnOp.Txn().ID), tableID: tableID} + s.mu.Lock() + registration := s.mu.privateCallbacks[key] + s.mu.Unlock() + if registration != nil { + <-registration.ready + } + s.mu.Lock() + private := s.mu.private[key] + delete(s.mu.private, key) + previous := s.mu.createdResets[key] + delete(s.mu.createdResets, key) + var current incrTableCache + if previous != nil && s.ownsCreateLocked(key.txnID, tableID) { + current = s.mu.tables[tableID] + s.bumpGenerationLocked(tableID) + s.mu.tables[tableID] = previous + previous = nil + } + s.mu.Unlock() + if private != nil { + private.retire() + } + if current != nil { + current.retire() + } + if previous != nil { + previous.retire() + } + return nil +} + +func (s *service) Close() { s.mu.Lock() if s.mu.closed { s.mu.Unlock() return } s.mu.closed = true + s.mu.Unlock() + + s.stopper.Stop() + s.builders.Wait() + + s.mu.Lock() + tables := make([]incrTableCache, 0, len(s.mu.tables)+len(s.mu.private)+len(s.mu.createdResets)) for _, tc := range s.mu.tables { - if err := tc.close(); err != nil { - panic(err) - } + tables = append(tables, tc) + } + for _, tc := range s.mu.private { + tables = append(tables, tc) } + for _, tc := range s.mu.createdResets { + tables = append(tables, tc) + } + s.mu.private = make(map[privateResetKey]incrTableCache) + s.mu.privateCallbacks = make(map[privateResetKey]*privateResetRegistration) + s.mu.createdResets = make(map[privateResetKey]incrTableCache) + s.mu.generation = make(map[uint64]uint64) + s.mu.generationBuilds = make(map[uint64]uint64) s.mu.Unlock() + for _, tc := range tables { + tc.retire() + } s.allocator.close() s.store.Close() } +func (s *service) acquireTableCacheForEpoch( + ctx context.Context, + tableID uint64, + autoIncrEpoch uint32, + txnOp client.TxnOperator, +) (incrTableCache, error) { + if txnOp != nil { + key := privateResetKey{txnID: string(txnOp.Txn().ID), tableID: tableID} + s.mu.Lock() + if s.mu.closed { + s.mu.Unlock() + return nil, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + if private, ok := s.mu.private[key]; ok { + // A reset cache is transaction-private and authoritative while it + // exists. Never mask a private-cache error by falling back to a + // committed AUTO_INCREMENT epoch cache. + private.acquire() + s.mu.Unlock() + return private, nil + } + s.mu.Unlock() + } + return s.getCommittedTableCacheForEpoch(ctx, tableID, autoIncrEpoch, txnOp) +} + +func (s *service) installPrivateReset( + ctx context.Context, + tableID uint64, + txnOp client.TxnOperator, + private incrTableCache, +) error { + key := privateResetKey{txnID: string(txnOp.Txn().ID), tableID: tableID} + s.mu.Lock() + if s.mu.closed { + s.mu.Unlock() + private.retire() + return moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + registration := s.mu.privateCallbacks[key] + owner := registration == nil + if owner { + registration = &privateResetRegistration{ready: make(chan struct{})} + s.mu.privateCallbacks[key] = registration + } + s.mu.Unlock() + + if owner { + if err := s.appendPrivateResetCallback(txnOp, privateResetCallback{ + key: key, + registration: registration, + }); err != nil { + s.mu.Lock() + if s.mu.privateCallbacks[key] == registration { + delete(s.mu.privateCallbacks, key) + } + s.mu.Unlock() + close(registration.ready) + private.retire() + return err + } + } else { + <-registration.ready + } + + s.mu.Lock() + if s.mu.closed || s.mu.privateCallbacks[key] != registration { + s.mu.Unlock() + if owner { + close(registration.ready) + } + private.retire() + return moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + old := s.mu.private[key] + s.mu.private[key] = private + s.mu.Unlock() + if owner { + close(registration.ready) + } + if old != nil { + old.retire() + } + return nil +} + +func (s *service) appendPrivateResetCallback( + txnOp client.TxnOperator, + callback privateResetCallback, +) (err error) { + defer func() { + if recover() != nil { + err = moerr.NewTxnNeedRetryWithDefChanged(context.Background()) + } + }() + txnOp.AppendEventCallback( + client.ClosedEvent, + client.NewTxnEventCallbackWithValue(s.privateResetClosed, callback), + ) + return nil +} + +func (s *service) privateResetClosed( + _ context.Context, + _ client.TxnOperator, + _ client.TxnEvent, + v any, +) error { + callback := v.(privateResetCallback) + <-callback.registration.ready + s.mu.Lock() + private := s.mu.private[callback.key] + delete(s.mu.private, callback.key) + if s.mu.privateCallbacks[callback.key] == callback.registration { + delete(s.mu.privateCallbacks, callback.key) + } + s.mu.Unlock() + if private != nil { + private.retire() + } + return nil +} + +func (s *service) txnEpochCacheClosed( + _ context.Context, + _ client.TxnOperator, + event client.TxnEvent, + v any, +) error { + callback := v.(txnEpochCacheCallback) + <-callback.registration.ready + if event.Txn.Status == txn.TxnStatus_Committed { + return nil + } + + s.mu.Lock() + if s.mu.tables[callback.tableID] != callback.cache { + s.mu.Unlock() + return nil + } + s.bumpGenerationLocked(callback.tableID) + delete(s.mu.tables, callback.tableID) + s.mu.Unlock() + callback.cache.retire() + return nil +} + func (s *service) doCreateLocked( tableID uint64, c incrTableCache, @@ -341,6 +779,7 @@ func (s *service) getCommittedTableCache( ctx, s.sid, tableID, + 0, cols, s.cfg, s.allocator, @@ -354,6 +793,164 @@ func (s *service) getCommittedTableCache( return c, nil } +func (s *service) getCommittedTableCacheForEpoch( + ctx context.Context, + tableID uint64, + autoIncrEpoch uint32, + txnOp client.TxnOperator, +) (incrTableCache, error) { + s.mu.Lock() + if s.mu.closed { + s.mu.Unlock() + return nil, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + c, ok := s.mu.tables[tableID] + if ok && c.epoch() == autoIncrEpoch { + c.acquire() + s.mu.Unlock() + return c, nil + } + if ok && c.epoch() > autoIncrEpoch { + s.mu.Unlock() + return nil, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + if _, ok := s.mu.destroyed[tableID]; ok { + s.mu.Unlock() + return nil, moerr.NewNoSuchTableNoCtx("", fmt.Sprintf("%d", tableID)) + } + generation := s.startGenerationBuildLocked(tableID) + s.builders.Add(1) + s.mu.Unlock() + defer s.builders.Done() + defer s.finishGenerationBuild(tableID) + + cols, err := s.store.GetColumns(ctx, tableID, nil) + if err != nil { + return nil, err + } + if len(cols) == 0 { + return nil, moerr.NewNoSuchTableNoCtx("", fmt.Sprintf("%d", tableID)) + } + + s.mu.Lock() + if s.mu.closed || s.mu.generation[tableID] != generation { + s.mu.Unlock() + return nil, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + if current, ok := s.mu.tables[tableID]; ok { + if current.epoch() == autoIncrEpoch { + current.acquire() + s.mu.Unlock() + return current, nil + } + if current.epoch() > autoIncrEpoch { + s.mu.Unlock() + return nil, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + } + s.mu.Unlock() + + replacement, err := newTableCache( + ctx, + s.sid, + tableID, + autoIncrEpoch, + cols, + s.cfg, + s.allocator, + nil, + true, + ) + if err != nil { + return nil, err + } + var registration *privateResetRegistration + if txnOp != nil { + registration = &privateResetRegistration{ready: make(chan struct{})} + callback := txnEpochCacheCallback{ + tableID: tableID, + cache: replacement, + registration: registration, + } + if err := s.appendTxnEpochCacheCallback(txnOp, callback); err != nil { + close(registration.ready) + _ = replacement.close() + return nil, err + } + defer close(registration.ready) + } + + s.mu.Lock() + if s.mu.closed || s.mu.generation[tableID] != generation { + s.mu.Unlock() + _ = replacement.close() + return nil, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + if _, ok := s.mu.destroyed[tableID]; ok { + s.mu.Unlock() + _ = replacement.close() + return nil, moerr.NewNoSuchTableNoCtx("", fmt.Sprintf("%d", tableID)) + } + if current, ok := s.mu.tables[tableID]; ok { + if current.epoch() == autoIncrEpoch { + current.acquire() + s.mu.Unlock() + _ = replacement.close() + return current, nil + } + if current.epoch() > autoIncrEpoch { + s.mu.Unlock() + _ = replacement.close() + return nil, moerr.NewTxnNeedRetryWithDefChanged(ctx) + } + c = current + } else { + c = nil + } + s.mu.tables[tableID] = replacement + replacement.acquire() + s.mu.Unlock() + if c != nil { + c.retire() + } + return replacement, nil +} + +func (s *service) appendTxnEpochCacheCallback( + txnOp client.TxnOperator, + callback txnEpochCacheCallback, +) (err error) { + defer func() { + if recover() != nil { + err = moerr.NewTxnNeedRetryWithDefChanged(context.Background()) + } + }() + txnOp.AppendEventCallback( + client.ClosedEvent, + client.NewTxnEventCallbackWithValue(s.txnEpochCacheClosed, callback), + ) + return nil +} + +func (s *service) acquireCommittedTableCache( + ctx context.Context, + tableID uint64, +) (incrTableCache, error) { + for { + c, err := s.getCommittedTableCache(ctx, tableID) + if err != nil { + return nil, err + } + s.mu.Lock() + if s.mu.tables[tableID] == c { + c.acquire() + s.mu.Unlock() + return c, nil + } + s.mu.Unlock() + } +} + func (s *service) txnClosed(ctx context.Context, txnOp client.TxnOperator, event client.TxnEvent, v any) error { s.mu.Lock() defer s.mu.Unlock() @@ -371,11 +968,16 @@ func (s *service) handleCreatesLocked(txnMeta txn.TxnMeta) { } for _, id := range tables { + resetKey := privateResetKey{txnID: key, tableID: id} + if previous := s.mu.createdResets[resetKey]; previous != nil { + previous.retire() + delete(s.mu.createdResets, resetKey) + } if tc, ok := s.mu.tables[id]; ok { if txnMeta.Status == txn.TxnStatus_Committed { tc.commit() } else { - _ = tc.close() + tc.retire() delete(s.mu.tables, id) s.logger.Info( "incrservice.cache.destroyed", @@ -399,7 +1001,7 @@ func (s *service) handleDeletesLocked(txnMeta txn.TxnMeta) { if txnMeta.Status == txn.TxnStatus_Committed { for _, ctx := range tables { if tc, ok := s.mu.tables[ctx.tableID]; ok { - _ = tc.close() + tc.retire() delete(s.mu.tables, ctx.tableID) s.mu.destroyed[ctx.tableID] = ctx s.logger.Info( diff --git a/pkg/incrservice/service_test.go b/pkg/incrservice/service_test.go index ca429cae9c74c..36ef61ce2d5d6 100644 --- a/pkg/incrservice/service_test.go +++ b/pkg/incrservice/service_test.go @@ -16,12 +16,20 @@ package incrservice import ( "context" + "errors" "fmt" + "math" + "runtime" + "sync" + "sync/atomic" "testing" "time" "github.com/lni/goutils/leaktest" "github.com/matrixorigin/matrixone/pkg/catalog" + "github.com/matrixorigin/matrixone/pkg/common/moerr" + "github.com/matrixorigin/matrixone/pkg/container/types" + "github.com/matrixorigin/matrixone/pkg/container/vector" "github.com/matrixorigin/matrixone/pkg/defines" "github.com/matrixorigin/matrixone/pkg/pb/timestamp" "github.com/matrixorigin/matrixone/pkg/txn/client" @@ -30,6 +38,244 @@ import ( "github.com/stretchr/testify/require" ) +type setOffsetStore struct { + IncrValueStore + + t *testing.T + forceCalled bool + tableID uint64 + colName string + offset uint64 +} + +type failingGetColumnsStore struct { + IncrValueStore + + mu sync.Mutex + err error +} + +type blockingGetColumnsStore struct { + IncrValueStore + + mu sync.Mutex + block bool + started chan struct{} + release chan struct{} +} + +type blockingAllocateStore struct { + IncrValueStore + + mu sync.Mutex + block bool + started chan struct{} + release chan struct{} +} + +type deadlineCheckingForceSetOffsetStore struct { + IncrValueStore + deadline chan time.Time +} + +func (s *deadlineCheckingForceSetOffsetStore) ForceSetOffset( + ctx context.Context, + tableID uint64, + colName string, + offset uint64, + txnOp client.TxnOperator, +) error { + deadline, ok := ctx.Deadline() + if !ok { + return errors.New("ForceSetOffset context has no deadline") + } + s.deadline <- deadline + return s.IncrValueStore.ForceSetOffset(ctx, tableID, colName, offset, txnOp) +} + +func (s *blockingAllocateStore) blockNext() (<-chan struct{}, func()) { + s.mu.Lock() + defer s.mu.Unlock() + s.block = true + s.started = make(chan struct{}) + s.release = make(chan struct{}) + var once sync.Once + return s.started, func() { once.Do(func() { close(s.release) }) } +} + +func (s *blockingAllocateStore) Allocate( + ctx context.Context, + tableID uint64, + col string, + count int, + txnOp client.TxnOperator, +) (uint64, uint64, timestamp.Timestamp, error) { + s.mu.Lock() + block := s.block + started := s.started + release := s.release + if block { + s.block = false + } + s.mu.Unlock() + if block { + close(started) + select { + case <-release: + case <-ctx.Done(): + return 0, 0, timestamp.Timestamp{}, context.Cause(ctx) + } + } + return s.IncrValueStore.Allocate(ctx, tableID, col, count, txnOp) +} + +type countingAllocator struct { + asyncCalls atomic.Int64 + asyncErr error +} + +type observedDoneContext struct { + context.Context + observed chan struct{} + once sync.Once +} + +func (c *observedDoneContext) Done() <-chan struct{} { + c.once.Do(func() { close(c.observed) }) + return c.Context.Done() +} + +type countingIncrTableCache struct { + tableID uint64 + acquires atomic.Int64 + releases atomic.Int64 + retires atomic.Int64 + closes atomic.Int64 + retired atomic.Bool +} + +func (c *countingIncrTableCache) table() uint64 { return c.tableID } +func (c *countingIncrTableCache) epoch() uint32 { return 0 } +func (c *countingIncrTableCache) acquire() { c.acquires.Add(1) } +func (c *countingIncrTableCache) release() { c.releases.Add(1) } +func (c *countingIncrTableCache) retire() { + if c.retired.CompareAndSwap(false, true) { + c.retires.Add(1) + _ = c.close() + } +} +func (c *countingIncrTableCache) commit() {} +func (c *countingIncrTableCache) columns() []AutoColumn { return nil } +func (c *countingIncrTableCache) insertAutoValues(context.Context, uint64, []*vector.Vector, int, int64) (uint64, error) { + return 0, nil +} +func (c *countingIncrTableCache) currentValue(context.Context, uint64, string) (uint64, error) { + return 0, nil +} +func (c *countingIncrTableCache) getLastAllocateTS(context.Context, string) (timestamp.Timestamp, error) { + return timestamp.Timestamp{}, nil +} +func (c *countingIncrTableCache) adjust(context.Context, []AutoColumn) error { return nil } +func (c *countingIncrTableCache) close() error { + c.closes.Add(1) + return nil +} + +func (a *countingAllocator) allocate(context.Context, uint64, string, int, client.TxnOperator) (uint64, uint64, timestamp.Timestamp, error) { + return 0, 0, timestamp.Timestamp{}, nil +} + +func (a *countingAllocator) asyncAllocate(context.Context, uint64, string, int, client.TxnOperator, func(uint64, uint64, timestamp.Timestamp, error)) error { + a.asyncCalls.Add(1) + return a.asyncErr +} + +func (a *countingAllocator) updateMinValue(context.Context, uint64, string, uint64, client.TxnOperator) error { + return nil +} + +func (a *countingAllocator) forceSetOffset(context.Context, uint64, string, uint64, client.TxnOperator) error { + return nil +} + +func (a *countingAllocator) close() {} + +func (s *blockingGetColumnsStore) blockNext() (<-chan struct{}, chan<- struct{}) { + s.mu.Lock() + defer s.mu.Unlock() + s.block = true + s.started = make(chan struct{}) + s.release = make(chan struct{}) + return s.started, s.release +} + +func (s *blockingGetColumnsStore) GetColumns( + ctx context.Context, + tableID uint64, + txnOp client.TxnOperator, +) ([]AutoColumn, error) { + s.mu.Lock() + block := s.block + started := s.started + release := s.release + if block { + s.block = false + } + s.mu.Unlock() + if block { + close(started) + select { + case <-release: + case <-ctx.Done(): + return nil, context.Cause(ctx) + } + } + return s.IncrValueStore.GetColumns(ctx, tableID, txnOp) +} + +func (s *failingGetColumnsStore) GetColumns( + ctx context.Context, + tableID uint64, + txnOp client.TxnOperator, +) ([]AutoColumn, error) { + s.mu.Lock() + err := s.err + s.mu.Unlock() + if err != nil { + return nil, err + } + return s.IncrValueStore.GetColumns(ctx, tableID, txnOp) +} + +func (s *failingGetColumnsStore) failWith(err error) { + s.mu.Lock() + defer s.mu.Unlock() + s.err = err +} + +func (s *setOffsetStore) GetColumns( + ctx context.Context, + tableID uint64, + txnOp client.TxnOperator, +) ([]AutoColumn, error) { + s.t.Fatalf("SetOffset should not read committed columns") + return nil, nil +} + +func (s *setOffsetStore) ForceSetOffset( + ctx context.Context, + tableID uint64, + colName string, + offset uint64, + txnOp client.TxnOperator, +) error { + s.forceCalled = true + s.tableID = tableID + s.colName = colName + s.offset = offset + return nil +} + func TestCreate(t *testing.T) { runServiceTests( t, @@ -139,6 +385,181 @@ func TestReloadIncrCache(t *testing.T) { }) } +func TestReloadDoesNotRetainGenerationWithoutActiveBuilder(t *testing.T) { + s := NewIncrService("", NewMemStore(), Config{}).(*service) + defer s.Close() + for tableID := uint64(1); tableID <= 1000; tableID++ { + require.NoError(t, s.Reload(context.Background(), tableID)) + } + s.mu.Lock() + generations := len(s.mu.generation) + builds := len(s.mu.generationBuilds) + s.mu.Unlock() + require.Zero(t, generations) + require.Zero(t, builds) +} + +func TestSetOffset(t *testing.T) { + runServiceTests( + t, + 1, + func( + ctx context.Context, + ss []*service, + ops []client.TxnOperator, + ) { + s := ss[0] + op := ops[0] + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, op)) + require.NoError(t, op.Commit(ctx)) + + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 42, nil)) + + store := s.store.(*memStore) + store.Lock() + require.Equal(t, uint64(42), store.caches[0][0].Offset) + store.Unlock() + }) +} + +func TestSetOffsetPreservesMaxUint64TerminalValue(t *testing.T) { + runServiceTests( + t, + 1, + func( + ctx context.Context, + ss []*service, + ops []client.TxnOperator, + ) { + s := ss[0] + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, ops[0])) + require.NoError(t, ops[0].Commit(ctx)) + + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, math.MaxUint64-1, nil)) + + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + last, err := s.InsertValues(ctx, 0, 0, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(math.MaxUint64), last) + require.Equal(t, uint64(math.MaxUint64), vector.MustFixedColWithTypeCheck[uint64](input)[0]) + + input = newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + _, err = s.InsertValues(ctx, 0, 0, nil, []*vector.Vector{input}, 1, 0) + require.Error(t, err) + require.True(t, moerr.IsMoErrCode(err, moerr.ErrOutOfRange)) + }) +} + +func TestSetOffsetDoesNotReadCommittedColumns(t *testing.T) { + ctx := defines.AttachAccountId(context.Background(), catalog.System_Account) + store := &setOffsetStore{t: t} + allocator := newValueAllocator("", store) + defer allocator.close() + s := &service{store: store, allocator: allocator} + + require.NoError(t, s.SetOffset(ctx, 10, "auto_col", 99, nil)) + require.True(t, store.forceCalled) + require.Equal(t, uint64(10), store.tableID) + require.Equal(t, "auto_col", store.colName) + require.Equal(t, uint64(99), store.offset) +} + +func TestSetOffsetReturnsStoreError(t *testing.T) { + runServiceTests( + t, + 1, + func( + ctx context.Context, + ss []*service, + ops []client.TxnOperator, + ) { + s := ss[0] + err := s.SetOffset(ctx, 42, "auto_0", 42, ops[0]) + require.Error(t, err) + }) +} + +func TestMemStoreSetOffset(t *testing.T) { + runServiceTests( + t, + 1, + func( + ctx context.Context, + ss []*service, + ops []client.TxnOperator, + ) { + store := ss[0].store.(*memStore) + op := ops[0] + def := newTestTableDef(1) + require.NoError(t, store.Create(ctx, 0, def, op)) + + require.NoError(t, store.SetOffset(ctx, 0, def[0].ColName, 77, op)) + require.NoError(t, store.SetOffset(ctx, 0, def[0].ColName, math.MaxUint64, op)) + + store.Lock() + require.Equal(t, uint64(math.MaxUint64), store.uncommitted[string(op.Txn().ID)][0][0].Offset) + store.Unlock() + }) +} + +func TestMemStoreSetOffsetReturnsError(t *testing.T) { + runServiceTests( + t, + 1, + func( + ctx context.Context, + ss []*service, + ops []client.TxnOperator, + ) { + store := ss[0].store.(*memStore) + def := newTestTableDef(1) + require.NoError(t, store.Create(ctx, 0, def, nil)) + + require.Error(t, store.SetOffset(ctx, 1, def[0].ColName, 77, nil)) + require.Error(t, store.SetOffset(ctx, 0, "missing_col", 77, nil)) + + op := ops[0] + require.Error(t, store.SetOffset(ctx, 1, def[0].ColName, 77, op)) + store.Lock() + _, exists := store.uncommitted[string(op.Txn().ID)] + store.Unlock() + require.False(t, exists) + + require.NoError(t, store.SetOffset(ctx, 0, def[0].ColName, 88, op)) + store.Lock() + require.Equal(t, uint64(0), store.caches[0][0].Offset) + require.Equal(t, uint64(88), store.uncommitted[string(op.Txn().ID)][0][0].Offset) + store.Unlock() + }) +} + +func TestMemStoreSetOffsetLowerThanPreAllocated(t *testing.T) { + runServiceTests( + t, + 1, + func( + ctx context.Context, + ss []*service, + ops []client.TxnOperator, + ) { + store := ss[0].store.(*memStore) + def := newTestTableDef(1) + // Simulate pre-allocation by advancing the offset to a high value + def[0].Offset = 10000 + require.NoError(t, store.Create(ctx, 0, def, nil)) + + // ForceSetOffset to a value LOWER than the current offset bypasses + // the monotonic guard. Regular SetOffset would reject the decrease. + require.NoError(t, store.ForceSetOffset(ctx, 0, def[0].ColName, 99, nil)) + + store.Lock() + require.Equal(t, uint64(99), store.caches[0][0].Offset) + store.Unlock() + }) +} + func TestCreateWithTxnAborted(t *testing.T) { runServiceTests( t, @@ -244,6 +665,1266 @@ func TestDeleteOnOtherService(t *testing.T) { }) } +func TestForceSetOffset(t *testing.T) { + runServiceTests( + t, + 1, + func( + ctx context.Context, + ss []*service, + ops []client.TxnOperator, + ) { + s := ss[0] + op := ops[0] + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, op)) + require.NoError(t, op.Commit(ctx)) + + // Simulate pre-allocation by advancing the store offset past the + // desired value. CountPerAllocate defaults to 10000. + store := s.store.(*memStore) + store.Lock() + store.caches[0][0].Offset = 10000 + store.Unlock() + + // SetOffset should detect the pre-allocation gap and use + // ForceSetOffset to bypass the store-level monotonic guard. + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 100, nil)) + + store.Lock() + require.Equal(t, uint64(100), store.caches[0][0].Offset) + store.Unlock() + }) +} + +func TestReloadAfterSetOffsetDropsStalePreAllocatedRange(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + defer leaktest.AfterTest(t)() + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := NewMemStore() + cn1 := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + cn2 := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + defer cn1.Close() + defer cn2.Close() + + op, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + def := newTestTableDef(1) + require.NoError(t, cn1.Create(ctx, 0, def, op)) + require.NoError(t, op.Commit(ctx)) + + vecType := types.New(types.T_uint64, 0, 0) + input := newTestVector[uint64](1, vecType, nil, nil) + last, err := cn1.InsertValues(ctx, 0, 0, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(1), last) + + require.NoError(t, cn2.SetOffset(ctx, 0, def[0].ColName, 100, nil)) + require.NoError(t, cn1.Reload(ctx, 0)) + + input = newTestVector[uint64](1, vecType, nil, nil) + last, err = cn1.InsertValues(ctx, 0, 0, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(101), last) + }) +} + +func TestSetOffsetRollbackKeepsCommittedOffsetAndSafelyRebuildsCache(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + defer leaktest.AfterTest(t)() + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := NewMemStore().(*memStore) + s := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + defer s.Close() + + createTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, createTxn)) + require.NoError(t, createTxn.Commit(ctx)) + + const autoIncrEpoch = 7 + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + lastBeforeAlter, err := s.InsertValues(ctx, 0, autoIncrEpoch, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + + // Drain any background pre-allocation before recording the durable + // high-water mark that rollback must preserve. + require.NoError(t, s.allocator.updateMinValue(ctx, 0, def[0].ColName, 0, nil)) + store.Lock() + committedCols := append([]AutoColumn(nil), store.caches[0]...) + store.Unlock() + committedOffset := committedCols[0].Offset + require.Greater(t, committedOffset, lastBeforeAlter) + + alterTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 50, alterTxn)) + + store.Lock() + uncommittedCols := append([]AutoColumn(nil), store.uncommitted[string(alterTxn.Txn().ID)][0]...) + store.Unlock() + // The private cache is lazy, so ALTER without subsequent DML leaves the + // staged offset exactly at the requested reset value. + require.Equal(t, uint64(50), uncommittedCols[0].Offset) + require.NoError(t, alterTxn.Rollback(ctx)) + + store.Lock() + committedCols = append([]AutoColumn(nil), store.caches[0]...) + store.Unlock() + require.Equal(t, committedOffset, committedCols[0].Offset) + + input = newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + lastAfterRollback, err := s.InsertValues(ctx, 0, autoIncrEpoch, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Greater(t, lastAfterRollback, committedOffset) + require.Greater(t, lastAfterRollback, lastBeforeAlter) + }) +} + +func TestSetOffsetTransactionUsesPendingOffsetForInsert(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + defer leaktest.AfterTest(t)() + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := NewMemStore().(*memStore) + s := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + defer s.Close() + + createTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, createTxn)) + require.NoError(t, createTxn.Commit(ctx)) + + alterTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 999, alterTxn)) + + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + last, err := s.InsertValues(ctx, 0, 1, alterTxn, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(1000), last) + }) +} + +func TestSetOffsetOnNewTablePublishesFreshCacheAfterCommit(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout( + defines.AttachAccountId(context.Background(), catalog.System_Account), + 10*time.Second, + ) + defer cancel() + + s := NewIncrService("", NewMemStore(), Config{CountPerAllocate: 100}).(*service) + defer s.Close() + createTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, createTxn)) + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 999, createTxn)) + key := privateResetKey{txnID: string(createTxn.Txn().ID), tableID: 0} + require.NoError(t, createTxn.Commit(ctx)) + s.mu.Lock() + _, resetExists := s.mu.createdResets[key] + s.mu.Unlock() + require.False(t, resetExists) + + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + last, err := s.InsertValues(ctx, 0, 0, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(1000), last) + }) +} + +func TestDiscardOffsetResetRestoresCreatedTableCache(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout( + defines.AttachAccountId(context.Background(), catalog.System_Account), + 10*time.Second, + ) + defer cancel() + + s := NewIncrService("", NewMemStore(), Config{CountPerAllocate: 100}).(*service) + defer s.Close() + createTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, createTxn)) + original := s.getTableCache(0) + + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 999, createTxn)) + replacement := s.getTableCache(0) + require.NotSame(t, original, replacement) + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + last, err := s.InsertValues(ctx, 0, 0, createTxn, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(1000), last) + key := privateResetKey{txnID: string(createTxn.Txn().ID), tableID: 0} + s.mu.Lock() + previous := s.mu.createdResets[key] + s.mu.Unlock() + require.Same(t, original, previous) + + require.NoError(t, s.DiscardOffsetReset(ctx, 0, createTxn)) + require.NoError(t, s.DiscardOffsetReset(ctx, 0, createTxn)) + require.Same(t, original, s.getTableCache(0)) + s.mu.Lock() + _, exists := s.mu.createdResets[key] + s.mu.Unlock() + require.False(t, exists) + cache := replacement.(*tableCache) + cache.lifecycle.Lock() + require.True(t, cache.lifecycle.retired) + require.True(t, cache.lifecycle.closed) + cache.lifecycle.Unlock() + + require.NoError(t, createTxn.Rollback(ctx)) + originalCache := original.(*tableCache) + originalCache.lifecycle.Lock() + require.True(t, originalCache.lifecycle.retired) + require.True(t, originalCache.lifecycle.closed) + originalCache.lifecycle.Unlock() + }) +} + +func TestSetOffsetWithoutInsertDoesNotReservePrivateRange(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := NewMemStore().(*memStore) + s := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + defer s.Close() + def := newTestTableDef(1) + require.NoError(t, store.Create(ctx, 0, def, nil)) + + alterTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 999, alterTxn)) + + store.Lock() + staged := append([]AutoColumn(nil), store.uncommitted[string(alterTxn.Txn().ID)][0]...) + store.Unlock() + require.Equal(t, uint64(999), staged[0].Offset, + "ALTER without DML must not reserve a range in its private transaction") + require.NoError(t, alterTxn.Commit(ctx)) + + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + last, err := s.InsertValues(ctx, 0, 1, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(1000), last) + }) +} + +type lazyLoadResult struct { + cache incrTableCache + err error +} + +func TestLazyPrivateTableCacheCanceledWaiterDoesNotStealBuild(t *testing.T) { + buildStarted := make(chan struct{}) + releaseBuild := make(chan struct{}) + var buildCalls atomic.Int64 + candidate := &countingIncrTableCache{tableID: 1} + lazy := newLazyPrivateTableCache(1, nil, func(context.Context) (incrTableCache, error) { + buildCalls.Add(1) + close(buildStarted) + <-releaseBuild + return candidate, nil + }).(*lazyPrivateTableCache) + + builderResult := make(chan lazyLoadResult, 1) + go func() { + cache, err := lazy.load(context.Background()) + builderResult <- lazyLoadResult{cache: cache, err: err} + }() + <-buildStarted + + waiterBase, cancelWaiter := context.WithCancel(context.Background()) + waiterCtx := &observedDoneContext{Context: waiterBase, observed: make(chan struct{})} + waiterResult := make(chan lazyLoadResult, 1) + go func() { + cache, err := lazy.load(waiterCtx) + waiterResult <- lazyLoadResult{cache: cache, err: err} + }() + <-waiterCtx.observed + cancelWaiter() + + waiter := <-waiterResult + require.Nil(t, waiter.cache) + require.ErrorIs(t, waiter.err, context.Canceled) + require.Equal(t, int64(1), buildCalls.Load()) + select { + case result := <-builderResult: + require.Failf(t, "builder returned before release", "result: %+v", result) + default: + } + + close(releaseBuild) + builder := <-builderResult + require.NoError(t, builder.err) + require.Same(t, candidate, builder.cache) + builder.cache.release() + lazy.retire() + require.Equal(t, int64(1), candidate.acquires.Load()) + require.Equal(t, int64(1), candidate.releases.Load()) + require.Equal(t, int64(1), candidate.retires.Load()) + require.Equal(t, int64(1), candidate.closes.Load()) +} + +func TestLazyPrivateTableCacheBuildErrorWakesGenerationAndLaterRetries(t *testing.T) { + firstBuildStarted := make(chan struct{}) + releaseFirstBuild := make(chan struct{}) + buildErr := errors.New("first build failed") + var buildCalls atomic.Int64 + candidate := &countingIncrTableCache{tableID: 1} + lazy := newLazyPrivateTableCache(1, nil, func(context.Context) (incrTableCache, error) { + if buildCalls.Add(1) == 1 { + close(firstBuildStarted) + <-releaseFirstBuild + return nil, buildErr + } + return candidate, nil + }).(*lazyPrivateTableCache) + + firstResult := make(chan lazyLoadResult, 1) + go func() { + cache, err := lazy.load(context.Background()) + firstResult <- lazyLoadResult{cache: cache, err: err} + }() + <-firstBuildStarted + + waiterCtx := &observedDoneContext{Context: context.Background(), observed: make(chan struct{})} + waiterResult := make(chan lazyLoadResult, 1) + go func() { + cache, err := lazy.load(waiterCtx) + waiterResult <- lazyLoadResult{cache: cache, err: err} + }() + <-waiterCtx.observed + close(releaseFirstBuild) + + for _, result := range []lazyLoadResult{<-firstResult, <-waiterResult} { + require.Nil(t, result.cache) + require.ErrorIs(t, result.err, buildErr) + } + require.Equal(t, int64(1), buildCalls.Load(), "same-generation waiter must not retry") + + cache, err := lazy.load(context.Background()) + require.NoError(t, err) + require.Same(t, candidate, cache) + cache.release() + require.Equal(t, int64(2), buildCalls.Load()) + lazy.retire() + require.Equal(t, int64(1), candidate.acquires.Load()) + require.Equal(t, int64(1), candidate.releases.Load()) + require.Equal(t, int64(1), candidate.retires.Load()) + require.Equal(t, int64(1), candidate.closes.Load()) +} + +func TestLazyPrivateTableCacheRetireRejectsBlockedCandidate(t *testing.T) { + buildStarted := make(chan struct{}) + releaseBuild := make(chan struct{}) + candidate := &countingIncrTableCache{tableID: 1} + lazy := newLazyPrivateTableCache(1, nil, func(context.Context) (incrTableCache, error) { + close(buildStarted) + <-releaseBuild + return candidate, nil + }).(*lazyPrivateTableCache) + + resultC := make(chan lazyLoadResult, 1) + go func() { + cache, err := lazy.load(context.Background()) + resultC <- lazyLoadResult{cache: cache, err: err} + }() + <-buildStarted + lazy.retire() + close(releaseBuild) + + result := <-resultC + require.Nil(t, result.cache) + require.True(t, moerr.IsMoErrCode(result.err, moerr.ErrTxnNeedRetryWithDefChanged)) + require.Nil(t, lazy.mu.cache) + lazy.retire() + require.Zero(t, candidate.acquires.Load()) + require.Equal(t, int64(1), candidate.retires.Load()) + require.Equal(t, int64(1), candidate.closes.Load()) +} + +func TestServiceCloseWaitsForAdmittedLazyBuilder(t *testing.T) { + s := NewIncrService("", NewMemStore(), Config{CountPerAllocate: 100}).(*service) + buildAdmitted := make(chan struct{}) + releaseBuild := make(chan struct{}) + candidate := &countingIncrTableCache{tableID: 1} + lazy := newLazyPrivateTableCache(1, nil, func(ctx context.Context) (incrTableCache, error) { + return s.buildPrivateTableCache(ctx, func() (incrTableCache, error) { + close(buildAdmitted) + <-releaseBuild + return candidate, nil + }) + }).(*lazyPrivateTableCache) + key := privateResetKey{txnID: "close-gate", tableID: 1} + s.mu.Lock() + s.mu.private[key] = lazy + s.mu.Unlock() + + loadResult := make(chan lazyLoadResult, 1) + go func() { + cache, err := lazy.load(context.Background()) + loadResult <- lazyLoadResult{cache: cache, err: err} + }() + <-buildAdmitted + + closeResult := make(chan struct{}) + go func() { + s.Close() + close(closeResult) + }() + closedObserved := make(chan struct{}) + go func() { + for { + s.mu.Lock() + closed := s.mu.closed + s.mu.Unlock() + if closed { + close(closedObserved) + return + } + runtime.Gosched() + } + }() + <-closedObserved + select { + case <-closeResult: + require.Fail(t, "Close returned while an admitted builder was blocked") + default: + } + + close(releaseBuild) + result := <-loadResult + require.Nil(t, result.cache) + require.True(t, moerr.IsMoErrCode(result.err, moerr.ErrTxnNeedRetryWithDefChanged)) + <-closeResult + require.Nil(t, lazy.mu.cache) + require.Zero(t, candidate.acquires.Load()) + require.Equal(t, int64(1), candidate.retires.Load()) + require.Equal(t, int64(1), candidate.closes.Load()) + s.mu.Lock() + _, exists := s.mu.private[key] + s.mu.Unlock() + require.False(t, exists) +} + +func TestPrivateOffsetResetBuildsOneCacheForConcurrentUsers(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := NewMemStore().(*memStore) + s := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + defer s.Close() + def := newTestTableDef(1) + require.NoError(t, store.Create(ctx, 0, def, nil)) + txnOp, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, store.Create(ctx, 0, def, txnOp)) + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 999, txnOp)) + + key := privateResetKey{txnID: string(txnOp.Txn().ID), tableID: 0} + s.mu.Lock() + lazy := s.mu.private[key].(*lazyPrivateTableCache) + s.mu.Unlock() + + const users = 8 + caches := make(chan incrTableCache, users) + errs := make(chan error, users) + var wg sync.WaitGroup + for range users { + wg.Add(1) + go func() { + defer wg.Done() + cache, err := lazy.load(ctx) + if err == nil { + cache.release() + } + caches <- cache + errs <- err + }() + } + wg.Wait() + close(caches) + close(errs) + for err := range errs { + require.NoError(t, err) + } + var first incrTableCache + for cache := range caches { + if first == nil { + first = cache + } else { + require.Same(t, first, cache) + } + } + + store.Lock() + staged := append([]AutoColumn(nil), store.uncommitted[string(txnOp.Txn().ID)][0]...) + store.Unlock() + require.Equal(t, uint64(1099), staged[0].Offset, + "concurrent users must reserve exactly one 100-value range") + }) +} + +func TestOffsetResetCacheRetiredOnTransactionClose(t *testing.T) { + for _, commit := range []bool{false, true} { + name := "rollback" + if commit { + name = "commit" + } + t.Run(name, func(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := NewMemStore().(*memStore) + s := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + defer s.Close() + def := newTestTableDef(1) + require.NoError(t, store.Create(ctx, 0, def, nil)) + + txnOp, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, store.Create(ctx, 0, def, txnOp)) + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 999, txnOp)) + + key := privateResetKey{txnID: string(txnOp.Txn().ID), tableID: 0} + s.mu.Lock() + private := s.mu.private[key] + s.mu.Unlock() + require.NotNil(t, private) + + if commit { + require.NoError(t, txnOp.Commit(ctx)) + } else { + require.NoError(t, txnOp.Rollback(ctx)) + } + + s.mu.Lock() + _, exists := s.mu.private[key] + s.mu.Unlock() + require.False(t, exists) + cache := private.(*lazyPrivateTableCache) + cache.lifecycle.Lock() + require.True(t, cache.lifecycle.retired) + require.True(t, cache.lifecycle.closed) + cache.lifecycle.Unlock() + }) + }) + } +} + +func TestDiscardOffsetResetRetiresPrivateCache(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := NewMemStore().(*memStore) + s := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + defer s.Close() + def := newTestTableDef(1) + require.NoError(t, store.Create(ctx, 0, def, nil)) + + txnOp, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, store.Create(ctx, 0, def, txnOp)) + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 999, txnOp)) + + key := privateResetKey{txnID: string(txnOp.Txn().ID), tableID: 0} + s.mu.Lock() + private := s.mu.private[key] + s.mu.Unlock() + require.NotNil(t, private) + + require.NoError(t, s.DiscardOffsetReset(ctx, 0, txnOp)) + require.NoError(t, s.DiscardOffsetReset(ctx, 0, txnOp)) + + s.mu.Lock() + _, exists := s.mu.private[key] + s.mu.Unlock() + require.False(t, exists) + cache := private.(*lazyPrivateTableCache) + cache.lifecycle.Lock() + require.True(t, cache.lifecycle.retired) + require.True(t, cache.lifecycle.closed) + cache.lifecycle.Unlock() + }) +} + +func TestServiceCloseRetiresPrivateOffsetResetCache(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := NewMemStore().(*memStore) + s := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + def := newTestTableDef(1) + require.NoError(t, store.Create(ctx, 0, def, nil)) + + txnOp, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, store.Create(ctx, 0, def, txnOp)) + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 999, txnOp)) + + key := privateResetKey{txnID: string(txnOp.Txn().ID), tableID: 0} + s.mu.Lock() + private := s.mu.private[key] + s.mu.Unlock() + require.NotNil(t, private) + + s.Close() + cache := private.(*lazyPrivateTableCache) + cache.lifecycle.Lock() + require.True(t, cache.lifecycle.retired) + require.True(t, cache.lifecycle.closed) + cache.lifecycle.Unlock() + }) +} + +func TestPrivateOffsetResetIsVisibleOnlyToOwningTransaction(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := NewMemStore().(*memStore) + s := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + defer s.Close() + def := newTestTableDef(1) + require.NoError(t, store.Create(ctx, 0, def, nil)) + + owner, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, store.Create(ctx, 0, def, owner)) + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 999, owner)) + other, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + + otherInput := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + otherLast, err := s.InsertValues(ctx, 0, 1, other, []*vector.Vector{otherInput}, 1, 0) + require.NoError(t, err) + require.Less(t, otherLast, uint64(1000)) + + key := privateResetKey{txnID: string(owner.Txn().ID), tableID: 0} + s.mu.Lock() + lazy := s.mu.private[key].(*lazyPrivateTableCache) + s.mu.Unlock() + loaded, err := lazy.load(ctx) + require.NoError(t, err) + private := loaded.(*tableCache) + loaded.release() + privateTS := timestamp.Timestamp{PhysicalTime: 42, LogicalTime: 7} + private.mu.Lock() + privateCol := private.mu.cols[def[0].ColName] + private.mu.Unlock() + privateCol.Lock() + privateCol.ranges.allocatedAt[0] = privateTS + privateCol.Unlock() + gotTS, err := s.GetLastAllocateTS(ctx, 0, 1, owner, def[0].ColName) + require.NoError(t, err) + require.Equal(t, privateTS, gotTS) + + ownerInput := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + ownerLast, err := s.InsertValues(ctx, 0, 1, owner, []*vector.Vector{ownerInput}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(1000), ownerLast) + + privateCol.Lock() + privateCol.ranges.values = nil + privateCol.ranges.allocatedAt = nil + privateCol.overflow = true + privateCol.Unlock() + ownerInput = newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + _, err = s.InsertValues(ctx, 0, 1, owner, []*vector.Vector{ownerInput}, 1, 0) + require.Error(t, err, "a present private cache must never fall back to the committed cache") + }) +} + +func TestFailedPrivateOffsetResetInstallRetiresCache(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := NewMemStore().(*memStore) + s := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + def := newTestTableDef(1) + require.NoError(t, store.Create(ctx, 0, def, nil)) + txnOp, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, store.Create(ctx, 0, def, txnOp)) + + private, err := newTableCache(ctx, s.sid, 0, 0, def, s.cfg, s.allocator, txnOp, false) + require.NoError(t, err) + s.Close() + require.Error(t, s.installPrivateReset(ctx, 0, txnOp, private)) + + cache := private.(*tableCache) + cache.lifecycle.Lock() + require.True(t, cache.lifecycle.retired) + require.True(t, cache.lifecycle.closed) + cache.lifecycle.Unlock() + }) +} + +func TestInsertValuesReplacesCacheOnAutoIncrementEpochChange(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + defer leaktest.AfterTest(t)() + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := NewMemStore() + s := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + defer s.Close() + + op, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, op)) + require.NoError(t, op.Commit(ctx)) + + vecType := types.New(types.T_uint64, 0, 0) + input := newTestVector[uint64](1, vecType, nil, nil) + last, err := s.InsertValues(ctx, 0, 7, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(101), last) + + require.NoError(t, store.ForceSetOffset(ctx, 0, def[0].ColName, 1000, nil)) + input = newTestVector[uint64](1, vecType, nil, nil) + last, err = s.InsertValues(ctx, 0, 8, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(1001), last) + + s.mu.Lock() + cached := s.mu.tables[0] + s.mu.Unlock() + require.Equal(t, uint32(8), cached.epoch()) + }) +} + +func TestTwoServicesKeepStaleRangesAcrossTransactionalReset(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + defer leaktest.AfterTest(t)() + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := NewMemStore().(*memStore) + cn1 := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + cn2 := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + defer cn1.Close() + defer cn2.Close() + + createTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + def := newTestTableDef(1) + require.NoError(t, cn1.Create(ctx, 0, def, createTxn)) + require.NoError(t, createTxn.Commit(ctx)) + + const ( + oldEpoch = uint32(7) + newEpoch = uint32(8) + effectiveOffset = uint64(999) + ) + + vecType := types.New(types.T_uint64, 0, 0) + input1 := newTestVector[uint64](1, vecType, nil, nil) + last1, err := cn1.InsertValues(ctx, 0, oldEpoch, nil, []*vector.Vector{input1}, 1, 0) + require.NoError(t, err) + input2 := newTestVector[uint64](1, vecType, nil, nil) + last2, err := cn2.InsertValues(ctx, 0, oldEpoch, nil, []*vector.Vector{input2}, 1, 0) + require.NoError(t, err) + // Create reserves the first range in its transaction; the first epoch-7 + // committed caches therefore start at 101 and 201 respectively. + require.Equal(t, uint64(101), last1) + require.Equal(t, uint64(201), last2) + require.LessOrEqual(t, last1, effectiveOffset) + require.LessOrEqual(t, last2, effectiveOffset) + + alterTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, cn1.SetOffset(ctx, 0, def[0].ColName, effectiveOffset, alterTxn)) + require.NoError(t, alterTxn.Commit(ctx)) + + // CN2 is deliberately not reloaded. Its known epoch-7 cache can still generate + // a value below the effective offset; the TN fence test consumes this same + // stale-value/epoch shape and proves that it cannot commit after ALTER. + input2 = newTestVector[uint64](1, vecType, nil, nil) + staleGenerated, err := cn2.InsertValues(ctx, 0, oldEpoch, nil, []*vector.Vector{input2}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(202), staleGenerated) + require.LessOrEqual(t, staleGenerated, effectiveOffset) + + // A retry planned at epoch 8 replaces CN2's stale cache from durable metadata. + input2 = newTestVector[uint64](1, vecType, nil, nil) + retryGenerated, err := cn2.InsertValues(ctx, 0, newEpoch, nil, []*vector.Vector{input2}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(1000), retryGenerated) + require.Greater(t, retryGenerated, effectiveOffset) + }) +} + +func TestInsertValuesFailedEpochReplacementPreservesOldCache(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + defer leaktest.AfterTest(t)() + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + + store := &failingGetColumnsStore{IncrValueStore: NewMemStore()} + s := NewIncrService("", store, Config{CountPerAllocate: 100}).(*service) + defer s.Close() + + op, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, op)) + require.NoError(t, op.Commit(ctx)) + + vecType := types.New(types.T_uint64, 0, 0) + input := newTestVector[uint64](1, vecType, nil, nil) + last, err := s.InsertValues(ctx, 0, 7, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + + loadErr := errors.New("load version 8") + store.failWith(loadErr) + input = newTestVector[uint64](1, vecType, nil, nil) + _, err = s.InsertValues(ctx, 0, 8, nil, []*vector.Vector{input}, 1, 0) + require.ErrorIs(t, err, loadErr) + + store.failWith(nil) + input = newTestVector[uint64](1, vecType, nil, nil) + next, err := s.InsertValues(ctx, 0, 7, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, last+1, next) + }) +} + +func TestTableCacheRetireWaitsForActiveUsers(t *testing.T) { + c := &tableCache{} + c.acquire() + c.retire() + + c.lifecycle.Lock() + require.False(t, c.lifecycle.closed) + c.lifecycle.Unlock() + + c.release() + c.lifecycle.Lock() + require.True(t, c.lifecycle.closed) + c.lifecycle.Unlock() +} + +func TestInsertValuesRejectsOlderEpoch(t *testing.T) { + runServiceTests(t, 1, func(ctx context.Context, ss []*service, ops []client.TxnOperator) { + s := ss[0] + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, ops[0])) + require.NoError(t, ops[0].Commit(ctx)) + vecType := types.New(types.T_uint64, 0, 0) + input := newTestVector[uint64](1, vecType, nil, nil) + _, err := s.InsertValues(ctx, 0, 8, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + + input = newTestVector[uint64](1, vecType, nil, nil) + _, err = s.InsertValues(ctx, 0, 7, nil, []*vector.Vector{input}, 1, 0) + require.True(t, moerr.IsMoErrCode(err, moerr.ErrTxnNeedRetryWithDefChanged)) + require.Equal(t, uint32(8), s.getTableCache(0).epoch()) + }) +} + +func TestInsertValuesRollbackDiscardsTxnInstalledNewerEpoch(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + s := NewIncrService("", NewMemStore(), Config{CountPerAllocate: 10}).(*service) + defer s.Close() + + createTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, s.Create(ctx, 0, newTestTableDef(1), createTxn)) + require.NoError(t, createTxn.Commit(ctx)) + + vecType := types.New(types.T_uint64, 0, 0) + input := newTestVector[uint64](1, vecType, nil, nil) + _, err = s.InsertValues(ctx, 0, 7, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + + alterTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + input = newTestVector[uint64](1, vecType, nil, nil) + _, err = s.InsertValues(ctx, 0, 8, alterTxn, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint32(8), s.getTableCache(0).epoch()) + require.NoError(t, alterTxn.Rollback(ctx)) + + input = newTestVector[uint64](1, vecType, nil, nil) + _, err = s.InsertValues(ctx, 0, 7, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint32(7), s.getTableCache(0).epoch()) + }) +} + +func TestInsertValuesCommitKeepsTxnInstalledNewerEpoch(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + s := NewIncrService("", NewMemStore(), Config{CountPerAllocate: 10}).(*service) + defer s.Close() + + createTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, s.Create(ctx, 0, newTestTableDef(1), createTxn)) + require.NoError(t, createTxn.Commit(ctx)) + + alterTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + _, err = s.InsertValues(ctx, 0, 8, alterTxn, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.NoError(t, alterTxn.Commit(ctx)) + + input = newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + _, err = s.InsertValues(ctx, 0, 8, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint32(8), s.getTableCache(0).epoch()) + }) +} + +func TestInsertValuesRejectsOlderBuilderFinishingAfterNewerEpoch(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + store := &blockingGetColumnsStore{IncrValueStore: NewMemStore()} + s := NewIncrService("", store, Config{CountPerAllocate: 10}).(*service) + defer s.Close() + op, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, s.Create(ctx, 0, newTestTableDef(1), op)) + require.NoError(t, op.Commit(ctx)) + + started, release := store.blockNext() + oldErr := make(chan error, 1) + go func() { + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + _, err := s.InsertValues(ctx, 0, 7, nil, []*vector.Vector{input}, 1, 0) + oldErr <- err + }() + <-started + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + _, err = s.InsertValues(ctx, 0, 8, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + close(release) + require.True(t, moerr.IsMoErrCode(<-oldErr, moerr.ErrTxnNeedRetryWithDefChanged)) + require.Equal(t, uint32(8), s.getTableCache(0).epoch()) + }) +} + +func TestInsertValuesBuilderCannotReviveCacheAfterReload(t *testing.T) { + testBlockedBuilderInvalidation(t, false) +} + +func TestInsertValuesBuilderCannotReviveCacheAfterClose(t *testing.T) { + testBlockedBuilderInvalidation(t, true) +} + +func TestSetOffsetWaitsForQueuedOldAllocation(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + store := &blockingAllocateStore{IncrValueStore: NewMemStore()} + s := NewIncrService("", store, Config{CountPerAllocate: 2, LowCapacity: 1}).(*service) + defer s.Close() + op, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, op)) + require.NoError(t, op.Commit(ctx)) + + started, release := store.blockNext() + defer release() + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + last, err := s.InsertValues(ctx, 0, 0, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(1), last) + <-started + + setResult := make(chan error, 1) + go func() { setResult <- s.SetOffset(ctx, 0, def[0].ColName, 99, nil) }() + require.Eventually(t, func() bool { + return len(s.allocator.(*allocator).c) == 1 + }, time.Second, time.Millisecond) + release() + require.NoError(t, <-setResult) + + input = newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + last, err = s.InsertValues(ctx, 0, 1, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(100), last) + }) +} + +func TestGetLastAllocateTSUsesRequestedEpochCache(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + store := NewMemStore() + s := NewIncrService("", store, Config{CountPerAllocate: 2, LowCapacity: 1}).(*service) + defer s.Close() + op, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, op)) + require.NoError(t, op.Commit(ctx)) + require.NoError(t, s.SetOffset(ctx, 0, def[0].ColName, 99, nil)) + + _, err = s.GetLastAllocateTS(ctx, 0, 1, nil, def[0].ColName) + require.NoError(t, err) + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + last, err := s.InsertValues(ctx, 0, 1, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + require.Equal(t, uint64(100), last) + }) +} + +func TestCanceledSetOffsetDoesNotRunQueuedForceUpdate(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + mem := NewMemStore().(*memStore) + store := &blockingAllocateStore{IncrValueStore: mem} + s := NewIncrService("", store, Config{CountPerAllocate: 2, LowCapacity: 1}).(*service) + defer s.Close() + op, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + def := newTestTableDef(1) + require.NoError(t, s.Create(ctx, 0, def, op)) + require.NoError(t, op.Commit(ctx)) + + started, release := store.blockNext() + defer release() + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + _, err = s.InsertValues(ctx, 0, 0, nil, []*vector.Vector{input}, 1, 0) + require.NoError(t, err) + <-started + + setCtx, cancelSet := context.WithCancel(ctx) + setResult := make(chan error, 1) + go func() { setResult <- s.SetOffset(setCtx, 0, def[0].ColName, 99, nil) }() + require.Eventually(t, func() bool { + return len(s.allocator.(*allocator).c) == 1 + }, time.Second, time.Millisecond) + cancelSet() + require.ErrorIs(t, <-setResult, context.Canceled) + release() + + require.NoError(t, s.allocator.updateMinValue(ctx, 0, def[0].ColName, 0, nil)) + mem.Lock() + offset := mem.caches[0][0].Offset + mem.Unlock() + require.Equal(t, uint64(4), offset) + }) +} + +func TestForceSetOffsetUsesBoundedContext(t *testing.T) { + ctx := defines.AttachAccountId(context.Background(), catalog.System_Account) + store := &deadlineCheckingForceSetOffsetStore{ + IncrValueStore: NewMemStore(), + deadline: make(chan time.Time, 1), + } + require.NoError(t, store.Create(ctx, 0, newTestTableDef(1), nil)) + allocator := newValueAllocator("", store).(*allocator) + defer allocator.close() + + require.NoError(t, allocator.forceSetOffset(ctx, 0, "auto_0", 99, nil)) + deadline := <-store.deadline + remaining := time.Until(deadline) + require.Positive(t, remaining) + require.LessOrEqual(t, remaining, defaultForceSetOffsetTimeout) +} + +func TestRetiredTableCacheCannotQueueAllocation(t *testing.T) { + allocator := &countingAllocator{} + col := &columnCache{ + col: AutoColumn{TableID: 1, ColName: "auto", Step: 1}, + cfg: Config{CountPerAllocate: 2, LowCapacity: 1}, + ranges: &ranges{step: 1}, + allocator: allocator, + committed: true, + } + c := &tableCache{} + c.mu.cols = map[string]*columnCache{"auto": col} + c.retire() + col.preAllocate(context.Background(), 1, 2, nil) + require.Zero(t, allocator.asyncCalls.Load()) +} + +func TestPreAllocateClearsStateWhenEnqueueFails(t *testing.T) { + allocator := &countingAllocator{asyncErr: context.Canceled} + col := &columnCache{ + col: AutoColumn{TableID: 1, ColName: "auto", Step: 1}, + cfg: Config{CountPerAllocate: 2, LowCapacity: 1}, + ranges: &ranges{step: 1}, + allocator: allocator, + committed: true, + } + col.preAllocate(context.Background(), 1, 2, nil) + col.Lock() + allocating := col.allocating + col.Unlock() + require.False(t, allocating) +} + +func testBlockedBuilderInvalidation(t *testing.T, closeService bool) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + store := &blockingGetColumnsStore{IncrValueStore: NewMemStore()} + s := NewIncrService("", store, Config{CountPerAllocate: 10}).(*service) + if !closeService { + defer s.Close() + } + op, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, s.Create(ctx, 0, newTestTableDef(1), op)) + require.NoError(t, op.Commit(ctx)) + + started, release := store.blockNext() + result := make(chan error, 1) + go func() { + input := newTestVector[uint64](1, types.New(types.T_uint64, 0, 0), nil, nil) + _, err := s.InsertValues(ctx, 0, 8, nil, []*vector.Vector{input}, 1, 0) + result <- err + }() + <-started + if closeService { + closed := make(chan struct{}) + go func() { + s.Close() + close(closed) + }() + require.Eventually(t, func() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.mu.closed + }, time.Second, time.Millisecond) + close(release) + <-closed + } else { + require.NoError(t, s.Reload(ctx, 0)) + close(release) + } + require.True(t, moerr.IsMoErrCode(<-result, moerr.ErrTxnNeedRetryWithDefChanged)) + s.mu.Lock() + cache, installed := s.mu.tables[0] + _, generationExists := s.mu.generation[0] + _, generationBuildExists := s.mu.generationBuilds[0] + s.mu.Unlock() + require.False(t, generationExists) + require.False(t, generationBuildExists) + if closeService { + require.True(t, installed) + require.Equal(t, uint32(0), cache.epoch()) + } else { + require.False(t, installed) + } + }) +} + +func TestMemStoreForceSetOffset(t *testing.T) { + runServiceTests( + t, + 1, + func( + ctx context.Context, + ss []*service, + ops []client.TxnOperator, + ) { + store := ss[0].store.(*memStore) + op := ops[0] + def := newTestTableDef(1) + require.NoError(t, store.Create(ctx, 0, def, op)) + + // ForceSetOffset bypasses the monotonic guard, allowing any value. + require.NoError(t, store.ForceSetOffset(ctx, 0, def[0].ColName, 50, op)) + + store.Lock() + require.Equal(t, uint64(50), store.uncommitted[string(op.Txn().ID)][0][0].Offset) + store.Unlock() + }) +} + +func TestMemStoreForceSetOffsetLowerThanCurrent(t *testing.T) { + runServiceTests( + t, + 1, + func( + ctx context.Context, + ss []*service, + ops []client.TxnOperator, + ) { + store := ss[0].store.(*memStore) + op := ops[0] + def := newTestTableDef(1) + require.NoError(t, store.Create(ctx, 0, def, op)) + + // Raise offset first with monotonic SetOffset. + require.NoError(t, store.SetOffset(ctx, 0, def[0].ColName, 1000, op)) + + // ForceSetOffset can lower it below the current value. + require.NoError(t, store.ForceSetOffset(ctx, 0, def[0].ColName, 100, op)) + + store.Lock() + require.Equal(t, uint64(100), store.uncommitted[string(op.Txn().ID)][0][0].Offset) + store.Unlock() + }) +} + +func TestMemStoreForceSetOffsetCreatesTransactionPrivateState(t *testing.T) { + client.RunTxnTests(func(tc client.TxnClient, _ rpc.TxnSender) { + ctx, cancel := context.WithTimeout(defines.AttachAccountId(context.Background(), catalog.System_Account), 10*time.Second) + defer cancel() + store := NewMemStore().(*memStore) + def := newTestTableDef(1) + createTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, store.Create(ctx, 0, def, createTxn)) + require.NoError(t, createTxn.Commit(ctx)) + require.NoError(t, store.ForceSetOffset(ctx, 0, def[0].ColName, 10, nil)) + + rollbackTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, store.ForceSetOffset(ctx, 0, def[0].ColName, 99, rollbackTxn)) + store.Lock() + require.Equal(t, uint64(10), store.caches[0][0].Offset) + require.Equal(t, uint64(99), store.uncommitted[string(rollbackTxn.Txn().ID)][0][0].Offset) + store.Unlock() + require.NoError(t, rollbackTxn.Rollback(ctx)) + store.Lock() + require.Equal(t, uint64(10), store.caches[0][0].Offset) + _, exists := store.uncommitted[string(rollbackTxn.Txn().ID)] + store.Unlock() + require.False(t, exists) + + commitTxn, err := tc.New(ctx, timestamp.Timestamp{}) + require.NoError(t, err) + require.NoError(t, store.ForceSetOffset(ctx, 0, def[0].ColName, 77, commitTxn)) + require.NoError(t, commitTxn.Commit(ctx)) + store.Lock() + require.Equal(t, uint64(77), store.caches[0][0].Offset) + store.Unlock() + }) +} + func runServiceTests( t *testing.T, n int, diff --git a/pkg/incrservice/store_mem.go b/pkg/incrservice/store_mem.go index 19a2ab1d2c681..ad5aec16ca894 100644 --- a/pkg/incrservice/store_mem.go +++ b/pkg/incrservice/store_mem.go @@ -20,6 +20,7 @@ import ( "sync" "time" + "github.com/matrixorigin/matrixone/pkg/common/moerr" "github.com/matrixorigin/matrixone/pkg/pb/timestamp" "github.com/matrixorigin/matrixone/pkg/pb/txn" "github.com/matrixorigin/matrixone/pkg/txn/client" @@ -56,23 +57,7 @@ func (s *memStore) Create( defer s.Unlock() m := s.caches if txnOp != nil { - m = make(map[uint64][]AutoColumn) - s.uncommitted[string(txnOp.Txn().ID)] = m - txnOp.AppendEventCallback( - client.ClosedEvent, - client.NewTxnEventCallback( - func(ctx context.Context, txnOp client.TxnOperator, event client.TxnEvent, v any) error { - txnMeta := event.Txn - s.Lock() - defer s.Unlock() - delete(s.uncommitted, string(txnMeta.ID)) - if txnMeta.Status == txn.TxnStatus_Committed { - for k, v := range m { - s.caches[k] = v - } - } - return nil - })) + m = s.getOrCreateTxnMapLocked(txnOp) } caches := m[tableID] @@ -92,6 +77,50 @@ func (s *memStore) Create( return nil } +func (s *memStore) getOrCreateTxnMapLocked(txnOp client.TxnOperator) map[uint64][]AutoColumn { + txnKey := string(txnOp.Txn().ID) + if m, ok := s.uncommitted[txnKey]; ok { + return m + } + m := make(map[uint64][]AutoColumn) + s.uncommitted[txnKey] = m + txnOp.AppendEventCallback( + client.ClosedEvent, + client.NewTxnEventCallback( + func(ctx context.Context, txnOp client.TxnOperator, event client.TxnEvent, v any) error { + txnMeta := event.Txn + s.Lock() + defer s.Unlock() + delete(s.uncommitted, string(txnMeta.ID)) + if txnMeta.Status == txn.TxnStatus_Committed { + for k, v := range m { + s.caches[k] = v + } + } + return nil + })) + return m +} + +func (s *memStore) getOrCloneTxnTableLocked( + ctx context.Context, + tableID uint64, + txnOp client.TxnOperator, +) (map[uint64][]AutoColumn, error) { + if m, ok := s.uncommitted[string(txnOp.Txn().ID)]; ok { + if _, exists := m[tableID]; exists { + return m, nil + } + } + cols, ok := s.caches[tableID] + if !ok { + return nil, moerr.NewInternalErrorf(ctx, "incrservice: table %d not found in memStore", tableID) + } + m := s.getOrCreateTxnMapLocked(txnOp) + m[tableID] = append([]AutoColumn(nil), cols...) + return m, nil +} + func (s *memStore) GetColumns( ctx context.Context, tableID uint64, @@ -129,7 +158,7 @@ func (s *memStore) Allocate( c = &cols[i] } } - if !ok { + if c == nil { panic("missing incr column record") } @@ -165,16 +194,81 @@ func (s *memStore) UpdateMinValue( c = &cols[i] } } - if !ok { + if c == nil { panic("missing incr column record") } - if c != nil && c.Offset < minValue { + if c.Offset < minValue { c.Offset = minValue } return nil } +func (s *memStore) SetOffset( + ctx context.Context, + tableID uint64, + colName string, + offset uint64, + txnOp client.TxnOperator, +) error { + s.Lock() + defer s.Unlock() + m := s.caches + if txnOp != nil { + var err error + m, err = s.getOrCloneTxnTableLocked(ctx, tableID, txnOp) + if err != nil { + return err + } + } + cols, ok := m[tableID] + if !ok { + return moerr.NewInternalErrorf(ctx, "incrservice: table %d not found in memStore", tableID) + } + for i := range cols { + if cols[i].ColName == colName { + if cols[i].Offset < offset { + cols[i].Offset = offset + } + return nil + } + } + return moerr.NewInternalErrorf(ctx, "incrservice: column %s not found for table %d in memStore", colName, tableID) +} + +// ForceSetOffset sets the offset of an auto-increment column to any value, +// bypassing the monotonic guard. Only called from service.SetOffset during +// ALTER TABLE AUTO_INCREMENT, which holds an exclusive DDL lock. +func (s *memStore) ForceSetOffset( + ctx context.Context, + tableID uint64, + colName string, + offset uint64, + txnOp client.TxnOperator, +) error { + s.Lock() + defer s.Unlock() + m := s.caches + if txnOp != nil { + var err error + m, err = s.getOrCloneTxnTableLocked(ctx, tableID, txnOp) + if err != nil { + return err + } + } + cols, ok := m[tableID] + if !ok { + return moerr.NewInternalErrorf(ctx, "incrservice: table %d not found in memStore", tableID) + } + for i := range cols { + if cols[i].ColName == colName { + cols[i].Offset = offset + return nil + } + } + return moerr.NewInternalErrorf(ctx, "incrservice: column %s not found for table %d in memStore", colName, tableID) +} + func (s *memStore) Delete( ctx context.Context, tableID uint64) error { diff --git a/pkg/incrservice/store_sql.go b/pkg/incrservice/store_sql.go index 503267f3e6826..eef179417f475 100644 --- a/pkg/incrservice/store_sql.go +++ b/pkg/incrservice/store_sql.go @@ -23,6 +23,7 @@ import ( "github.com/matrixorigin/matrixone/pkg/catalog" "github.com/matrixorigin/matrixone/pkg/common/moerr" + "github.com/matrixorigin/matrixone/pkg/common/sqlquote" "github.com/matrixorigin/matrixone/pkg/container/vector" "github.com/matrixorigin/matrixone/pkg/defines" "github.com/matrixorigin/matrixone/pkg/lockservice" @@ -42,7 +43,7 @@ func (c AutoColumn) getInsertSQL() string { values(%d, '%s', %d, %d, %d)`, incrTableName, c.TableID, - c.ColName, + sqlquote.EscapeString(c.ColName), c.ColIndex, c.Offset, c.Step) @@ -106,7 +107,7 @@ func (s *sqlStore) Allocate( fetchSQL := fmt.Sprintf(`select offset, step from %s where table_id = %d and col_name = '%s' for update`, incrTableName, tableID, - colName) + sqlquote.EscapeString(colName)) opts := executor.Options{}. WithDatabase(database). WithTxn(txnOp). @@ -177,7 +178,7 @@ func (s *sqlStore) Allocate( incrTableName, next, tableID, - colName, + sqlquote.EscapeString(colName), current) start = time.Now() res, err = te.Exec(sql, executor.StatementOption{}.WithDisableLog()) @@ -296,7 +297,7 @@ func (s *sqlStore) UpdateMinValue( incrTableName, minValue, tableID, - col, + sqlquote.EscapeString(col), minValue), opts) if err != nil { @@ -306,6 +307,77 @@ func (s *sqlStore) UpdateMinValue( return nil } +func (s *sqlStore) SetOffset( + ctx context.Context, + tableID uint64, + colName string, + offset uint64, + txnOp client.TxnOperator, +) error { + opts := executor.Options{}. + WithDatabase(database). + WithTxn(txnOp) + if txnOp == nil { + opts = opts. + WithWaitCommittedLogApplied(). + WithEnableTrace(). + WithDisableWaitPaused(). + WithStatementOption(executor.StatementOption{}.WithDisableLog()) + } else { + opts = opts.WithDisableIncrStatement() + } + res, err := s.exec.Exec( + ctx, + fmt.Sprintf( + "update %s set offset = %d where table_id = %d and col_name = '%s' and offset < %d", + incrTableName, offset, tableID, sqlquote.EscapeString(colName), offset, + ), + opts, + ) + if err != nil { + return err + } + defer res.Close() + return nil +} + +// ForceSetOffset sets the offset of an auto-increment column to any value, +// bypassing the monotonic guard. Only called from service.SetOffset during +// ALTER TABLE AUTO_INCREMENT, which holds an exclusive DDL lock. +func (s *sqlStore) ForceSetOffset( + ctx context.Context, + tableID uint64, + colName string, + offset uint64, + txnOp client.TxnOperator, +) error { + opts := executor.Options{}. + WithDatabase(database). + WithTxn(txnOp) + if txnOp == nil { + opts = opts. + WithWaitCommittedLogApplied(). + WithEnableTrace(). + WithDisableWaitPaused(). + WithStatementOption(executor.StatementOption{}.WithDisableLog()) + } else { + opts = opts.WithDisableIncrStatement() + } + res, err := s.exec.Exec( + ctx, + fmt.Sprintf( + "update %s set offset = %d where table_id = %d and col_name = '%s'", + incrTableName, offset, tableID, sqlquote.EscapeString(colName), + ), + opts, + ) + if err != nil { + return err + } + defer res.Close() + return nil +} + func (s *sqlStore) Delete(ctx context.Context, tableID uint64) error { opts := executor.Options{}. WithDatabase(database). diff --git a/pkg/incrservice/store_sql_test.go b/pkg/incrservice/store_sql_test.go index 57325d02278bc..9c5c24e92e5b3 100644 --- a/pkg/incrservice/store_sql_test.go +++ b/pkg/incrservice/store_sql_test.go @@ -15,6 +15,7 @@ package incrservice import ( "context" + "math" "strings" "sync/atomic" "testing" @@ -656,3 +657,77 @@ func Test_Allocate_Retry_When_AffectedRows_Invalid(t *testing.T) { require.NoError(t, err) require.Equal(t, int32(2), updateCnt.Load()) } + +func TestSQLStoreSetOffset(t *testing.T) { + ctx := context.TODO() + ctx = defines.AttachAccountId(ctx, 12) + + txnOp := &testTxnOperator{} + var executedSQLs []string + sqlExecutor := executor.NewMemExecutor2( + func(sql string) (executor.Result, error) { + executedSQLs = append(executedSQLs, sql) + return executor.Result{}, nil + }, + txnOp, + ) + + s := &sqlStore{ + exec: sqlExecutor, + } + + require.NoError(t, s.SetOffset(ctx, 10, "auto_col", 99, nil)) + require.NoError(t, s.SetOffset(ctx, 10, "auto_col", 100, txnOp)) + require.NoError(t, s.ForceSetOffset(ctx, 10, "auto_col", math.MaxUint64, txnOp)) + require.Len(t, executedSQLs, 3) + require.Contains(t, executedSQLs[0], "update mo_increment_columns set offset = 99") + require.Contains(t, executedSQLs[0], "table_id = 10") + require.Contains(t, executedSQLs[0], "col_name = 'auto_col'") + require.Contains(t, executedSQLs[1], "update mo_increment_columns set offset = 100") + require.Contains(t, executedSQLs[2], "update mo_increment_columns set offset = 18446744073709551615") +} + +func TestSQLStoreSetOffsetEscapesColumnNameLiteral(t *testing.T) { + ctx := context.TODO() + ctx = defines.AttachAccountId(ctx, 12) + + var executedSQLs []string + sqlExecutor := executor.NewMemExecutor2( + func(sql string) (executor.Result, error) { + executedSQLs = append(executedSQLs, sql) + return executor.Result{}, nil + }, + nil, + ) + + s := &sqlStore{ + exec: sqlExecutor, + } + + require.NoError(t, s.SetOffset(ctx, 10, "1id", 99, nil)) + require.NoError(t, s.SetOffset(ctx, 10, "auto'col\\x", 100, nil)) + require.NoError(t, s.ForceSetOffset(ctx, 10, "auto'col\\x", 101, nil)) + require.Len(t, executedSQLs, 3) + require.Contains(t, executedSQLs[0], "col_name = '1id'") + require.Contains(t, executedSQLs[1], `col_name = 'auto''col\\x'`) + require.Contains(t, executedSQLs[2], `col_name = 'auto''col\\x'`) +} + +func TestSQLStoreSetOffsetReturnsExecError(t *testing.T) { + ctx := context.TODO() + ctx = defines.AttachAccountId(ctx, 12) + + expected := moerr.NewInternalError(ctx, "set offset failed") + sqlExecutor := executor.NewMemExecutor2( + func(sql string) (executor.Result, error) { + return executor.Result{}, expected + }, + nil, + ) + + s := &sqlStore{ + exec: sqlExecutor, + } + + require.ErrorIs(t, s.SetOffset(ctx, 10, "auto_col", 99, nil), expected) +} diff --git a/pkg/incrservice/table_cache.go b/pkg/incrservice/table_cache.go index a40a947cc5d31..10a2fb6bd8aef 100644 --- a/pkg/incrservice/table_cache.go +++ b/pkg/incrservice/table_cache.go @@ -28,8 +28,16 @@ import ( type tableCache struct { logger *log.MOLogger tableID uint64 + epochID uint32 cols []AutoColumn + lifecycle struct { + sync.Mutex + users int + retired bool + closed bool + } + mu struct { sync.RWMutex committed bool @@ -42,6 +50,7 @@ func newTableCache( ctx context.Context, sid string, tableID uint64, + epoch uint32, cols []AutoColumn, cfg Config, allocator valueAllocator, @@ -50,6 +59,7 @@ func newTableCache( c := &tableCache{ logger: getLogger(sid).Named("incrservice"), tableID: tableID, + epochID: epoch, cols: cols, } c.mu.cols = make(map[string]*columnCache, 1) @@ -67,6 +77,10 @@ func newTableCache( txnOp, ) if err != nil { + for _, created := range c.mu.cols { + created.retire() + _ = created.close() + } return nil, err } c.mu.cols[col.ColName] = cc @@ -95,13 +109,13 @@ func (c *tableCache) getTxn() client.TxnOperator { return c.mu.txnOp } -func (c *tableCache) getLastAllocateTS(colName string) (timestamp.Timestamp, error) { +func (c *tableCache) getLastAllocateTS(_ context.Context, colName string) (timestamp.Timestamp, error) { cc := c.getColumnCache(colName) if cc == nil { panic("column cache should not be nil, " + colName) } cc.RLock() - ts := cc.ranges.oldestAllocateAt() + ts := cc.oldestAllocateAtLocked() cc.RUnlock() // Log a warning if the allocation timestamp is empty, which may cause PrimaryKeysMayBeUpserted // to scan a very large time range and impact performance. @@ -162,6 +176,48 @@ func (c *tableCache) table() uint64 { return c.tableID } +func (c *tableCache) epoch() uint32 { + return c.epochID +} + +func (c *tableCache) acquire() { + c.lifecycle.Lock() + defer c.lifecycle.Unlock() + c.lifecycle.users++ +} + +func (c *tableCache) release() { + c.lifecycle.Lock() + c.lifecycle.users-- + closeNow := c.lifecycle.retired && c.lifecycle.users == 0 && !c.lifecycle.closed + if closeNow { + c.lifecycle.closed = true + } + c.lifecycle.Unlock() + if closeNow { + _ = c.close() + } +} + +func (c *tableCache) retire() { + c.mu.RLock() + for _, col := range c.mu.cols { + col.retire() + } + c.mu.RUnlock() + + c.lifecycle.Lock() + c.lifecycle.retired = true + closeNow := c.lifecycle.users == 0 && !c.lifecycle.closed + if closeNow { + c.lifecycle.closed = true + } + c.lifecycle.Unlock() + if closeNow { + _ = c.close() + } +} + func (c *tableCache) columns() []AutoColumn { return c.cols } diff --git a/pkg/incrservice/types.go b/pkg/incrservice/types.go index edc2c847d7207..12883ec12bf45 100644 --- a/pkg/incrservice/types.go +++ b/pkg/incrservice/types.go @@ -62,17 +62,21 @@ type AutoIncrementService interface { // records to be deleted are recorded. When the delete table transaction is committed, the // delete operation is triggered. Delete(ctx context.Context, tableID uint64, txn client.TxnOperator) error - // InsertValues insert auto columns values into bat. - InsertValues(ctx context.Context, tableID uint64, vecs []*vector.Vector, rows int, estimate int64) (uint64, error) + // InsertValues inserts auto-column values, using a reset cache owned by txn when present. + InsertValues(ctx context.Context, tableID uint64, autoIncrEpoch uint32, txn client.TxnOperator, vecs []*vector.Vector, rows int, estimate int64) (uint64, error) // CurrentValue return current incr column value. CurrentValue(ctx context.Context, tableID uint64, col string) (uint64, error) // Reload reload auto increment cache. Reload(ctx context.Context, tableID uint64) error + // SetOffset sets the offset of an auto-increment column and refreshes local cache. + SetOffset(ctx context.Context, tableID uint64, colName string, offset uint64, txn client.TxnOperator) error + // DiscardOffsetReset synchronously retires a transaction-private reset cache. + DiscardOffsetReset(ctx context.Context, tableID uint64, txn client.TxnOperator) error // Close close the auto increment service Close() // GetLastAllocateTS gets the oldest allocation timestamp that can still - // issue a value from the column cache. - GetLastAllocateTS(ctx context.Context, tableID uint64, colName string) (timestamp.Timestamp, error) + // issue a value from the transaction-private or committed column cache. + GetLastAllocateTS(ctx context.Context, tableID uint64, autoIncrEpoch uint32, txn client.TxnOperator, colName string) (timestamp.Timestamp, error) } // incrTableCache a cache containing auto-incremented columns of a table, an incrCache may @@ -108,11 +112,15 @@ type AutoIncrementService interface { // allocations for one write. type incrTableCache interface { table() uint64 + epoch() uint32 + acquire() + release() + retire() commit() columns() []AutoColumn insertAutoValues(ctx context.Context, tableID uint64, vecs []*vector.Vector, rows int, estimate int64) (uint64, error) currentValue(ctx context.Context, tableID uint64, col string) (uint64, error) - getLastAllocateTS(colName string) (timestamp.Timestamp, error) + getLastAllocateTS(ctx context.Context, colName string) (timestamp.Timestamp, error) adjust(ctx context.Context, cols []AutoColumn) error close() error } @@ -121,6 +129,7 @@ type valueAllocator interface { allocate(ctx context.Context, tableID uint64, col string, count int, txnOp client.TxnOperator) (uint64, uint64, timestamp.Timestamp, error) asyncAllocate(ctx context.Context, tableID uint64, col string, count int, txnOp client.TxnOperator, cb func(uint64, uint64, timestamp.Timestamp, error)) error updateMinValue(ctx context.Context, tableID uint64, col string, minValue uint64, txnOp client.TxnOperator) error + forceSetOffset(ctx context.Context, tableID uint64, col string, offset uint64, txnOp client.TxnOperator) error close() } @@ -134,6 +143,13 @@ type IncrValueStore interface { Allocate(ctx context.Context, tableID uint64, col string, count int, txnOp client.TxnOperator) (uint64, uint64, timestamp.Timestamp, error) // UpdateMinValue update auto column min value to specified value. UpdateMinValue(ctx context.Context, tableID uint64, col string, minValue uint64, txnOp client.TxnOperator) error + // SetOffset updates the offset of an auto-increment column, only raising it when the new + // value exceeds the current. If the current offset is already >= the new offset, this is a no-op. + SetOffset(ctx context.Context, tableID uint64, colName string, offset uint64, txnOp client.TxnOperator) error + // ForceSetOffset sets the offset of an auto-increment column to any value, bypassing + // the monotonic guard. Only called during ALTER TABLE AUTO_INCREMENT which holds an + // exclusive DDL lock, ensuring no concurrent inserts. + ForceSetOffset(ctx context.Context, tableID uint64, colName string, offset uint64, txnOp client.TxnOperator) error // Delete remove metadata records from catalog.AutoIncrTableName. Delete(ctx context.Context, tableID uint64) error // Close the store diff --git a/pkg/sql/colexec/preinsert/preinsert.go b/pkg/sql/colexec/preinsert/preinsert.go index ebd7f1ced6ac0..2d1f6963d97ae 100644 --- a/pkg/sql/colexec/preinsert/preinsert.go +++ b/pkg/sql/colexec/preinsert/preinsert.go @@ -384,7 +384,7 @@ retryInsertValues: // but conflict detection must still cover every value generated for this batch. lastAllocateTSMap := make(map[string]timestamp.Timestamp) for col := range needReCheck { - ts, err := proc.GetIncrService().GetLastAllocateTS(proc.Ctx, tableID, col) + ts, err := proc.GetIncrService().GetLastAllocateTS(proc.Ctx, tableID, preInsert.TableDef.AutoIncrEpoch, currentTxn, col) if err != nil { return err } @@ -394,6 +394,8 @@ retryInsertValues: lastInsertValue, err := proc.GetIncrService().InsertValues( proc.Ctx, tableID, + preInsert.TableDef.AutoIncrEpoch, + currentTxn, bat.Vecs[preInsert.ColOffset:int(preInsert.ColOffset)+len(preInsert.Attrs)], bat.RowCount(), preInsert.EstimatedRowCount, diff --git a/pkg/sql/colexec/preinsert/preinsert_test.go b/pkg/sql/colexec/preinsert/preinsert_test.go index c4b73b286bf9a..596ee47d99c97 100644 --- a/pkg/sql/colexec/preinsert/preinsert_test.go +++ b/pkg/sql/colexec/preinsert/preinsert_test.go @@ -228,7 +228,7 @@ func TestPreInsertHasAutoCol(t *testing.T) { }).AnyTimes() incrService := mock_frontend.NewMockAutoIncrementService(ctrl) - incrService.EXPECT().InsertValues(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(uint64(111111), nil).AnyTimes() + incrService.EXPECT().InsertValues(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(uint64(111111), nil).AnyTimes() proc := testutil.NewProc(t) proc.Base.TxnClient = txnClient @@ -339,7 +339,7 @@ func TestPreInsertIsUpdate(t *testing.T) { }).AnyTimes() incrService := mock_frontend.NewMockAutoIncrementService(ctrl) - incrService.EXPECT().InsertValues(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(uint64(111111), nil).AnyTimes() + incrService.EXPECT().InsertValues(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(uint64(111111), nil).AnyTimes() proc := testutil.NewProc(t) proc.Base.TxnClient = txnClient @@ -513,9 +513,9 @@ func TestGenAutoIncrColRefreshesStaleTableID(t *testing.T) { incrService := mock_frontend.NewMockAutoIncrementService(ctrl) gomock.InOrder( - incrService.EXPECT().InsertValues(gomock.Any(), uint64(100), gomock.Any(), 1, int64(1)). + incrService.EXPECT().InsertValues(gomock.Any(), uint64(100), uint32(5), txnOperator, gomock.Any(), 1, int64(1)). Return(uint64(0), moerr.NewNoSuchTableNoCtx("", "100")), - incrService.EXPECT().InsertValues(gomock.Any(), uint64(200), gomock.Any(), 1, int64(1)). + incrService.EXPECT().InsertValues(gomock.Any(), uint64(200), uint32(5), txnOperator, gomock.Any(), 1, int64(1)). Return(uint64(111111), nil), ) @@ -530,8 +530,10 @@ func TestGenAutoIncrColRefreshesStaleTableID(t *testing.T) { HasAutoCol: true, SchemaName: "testDb", TableDef: &plan.TableDef{ - Name: "idx_tbl", - TblId: 100, + Name: "idx_tbl", + TblId: 100, + Version: 17, + AutoIncrEpoch: 5, Cols: []*plan.ColDef{ {Name: catalog.FakePrimaryKeyColName, Typ: i32typ}, }, @@ -574,7 +576,7 @@ func TestGenAutoIncrColReturnsRetryWhenDefinitionStillChanged(t *testing.T) { rel.EXPECT().GetTableID(gomock.Any()).Return(uint64(100)) incrService := mock_frontend.NewMockAutoIncrementService(ctrl) - incrService.EXPECT().InsertValues(gomock.Any(), uint64(100), gomock.Any(), 1, int64(1)). + incrService.EXPECT().InsertValues(gomock.Any(), uint64(100), gomock.Any(), txnOperator, gomock.Any(), 1, int64(1)). Return(uint64(0), moerr.NewNoSuchTableNoCtx("", "100")) proc := testutil.NewProc(t) @@ -622,7 +624,7 @@ func TestGenAutoIncrColKeepsTemporaryTableBehavior(t *testing.T) { eng := mock_frontend.NewMockEngine(ctrl) incrService := mock_frontend.NewMockAutoIncrementService(ctrl) - incrService.EXPECT().InsertValues(gomock.Any(), uint64(100), gomock.Any(), 1, int64(1)). + incrService.EXPECT().InsertValues(gomock.Any(), uint64(100), gomock.Any(), txnOperator, gomock.Any(), 1, int64(1)). Return(uint64(0), moerr.NewNoSuchTableNoCtx("", "100")) proc := testutil.NewProc(t) diff --git a/pkg/sql/colexec/table_clone/table_clone.go b/pkg/sql/colexec/table_clone/table_clone.go index 18102fcd251bd..e5ada6c710ac6 100644 --- a/pkg/sql/colexec/table_clone/table_clone.go +++ b/pkg/sql/colexec/table_clone/table_clone.go @@ -536,7 +536,13 @@ func (tc *TableClone) updateDstAutoIncrColumns( } if _, err = proc.GetIncrService().InsertValues( - dstCtx, tc.dstMasterRel.GetTableID(dstCtx), vecs, rows, int64(rows), + dstCtx, + tc.dstMasterRel.GetTableID(dstCtx), + dstTblDef.AutoIncrEpoch, + proc.GetTxnOperator(), + vecs, + rows, + int64(rows), ); err != nil { return err } diff --git a/pkg/vm/engine/test/mock_increament_service.go b/pkg/vm/engine/test/mock_increament_service.go index 448479e78e219..f489272d0ee89 100644 --- a/pkg/vm/engine/test/mock_increament_service.go +++ b/pkg/vm/engine/test/mock_increament_service.go @@ -18,6 +18,7 @@ import ( "context" "sync" + "github.com/matrixorigin/matrixone/pkg/common/moerr" "github.com/matrixorigin/matrixone/pkg/container/types" "github.com/matrixorigin/matrixone/pkg/container/vector" "github.com/matrixorigin/matrixone/pkg/incrservice" @@ -113,6 +114,8 @@ func (m *MockAutoIncrementService) Delete( func (m *MockAutoIncrementService) InsertValues( ctx context.Context, tableID uint64, + tableVersion uint32, + txnOp client.TxnOperator, vecs []*vector.Vector, rows int, estimate int64, @@ -197,6 +200,35 @@ func (m *MockAutoIncrementService) Reload(ctx context.Context, tableID uint64) e return nil } +// SetOffset sets the offset of an auto-increment column +func (m *MockAutoIncrementService) SetOffset( + ctx context.Context, + tableID uint64, + colName string, + offset uint64, + txn client.TxnOperator, +) error { + m.mu.Lock() + defer m.mu.Unlock() + counters, ok := m.counters[tableID] + if !ok { + return moerr.NewInternalErrorf(ctx, "table %d not found in mock auto-increment counters", tableID) + } + if current, ok := counters[colName]; !ok || current < offset { + counters[colName] = offset + } + return nil +} + +// DiscardOffsetReset discards a transaction-private reset in the mock service. +func (m *MockAutoIncrementService) DiscardOffsetReset( + ctx context.Context, + tableID uint64, + txn client.TxnOperator, +) error { + return nil +} + // Close closes the service func (m *MockAutoIncrementService) Close() { m.mu.Lock() @@ -210,6 +242,8 @@ func (m *MockAutoIncrementService) Close() { func (m *MockAutoIncrementService) GetLastAllocateTS( ctx context.Context, tableID uint64, + tableVersion uint32, + txnOp client.TxnOperator, colName string, ) (timestamp.Timestamp, error) { return timestamp.Timestamp{}, nil