diff options
author | Quang-Minh Nguyen <qmnguyen@gitlab.com> | 2023-11-27 19:15:16 +0300 |
---|---|---|
committer | Quang-Minh Nguyen <qmnguyen@gitlab.com> | 2023-12-06 10:33:19 +0300 |
commit | 615032a1b3a92c8369148cb27c6ea7d1d81c682f (patch) | |
tree | de9695df7c82e38c8df24d9453cc89935c1bccf3 | |
parent | 7235df79904dce2f733f15f42a307109865629b5 (diff) |
Split tests related to repo modification out of TestTransactionManager
-rw-r--r-- | internal/gitaly/storage/storagemgr/transaction_manager_repo_test.go | 1023 | ||||
-rw-r--r-- | internal/gitaly/storage/storagemgr/transaction_manager_test.go | 1001 |
2 files changed, 1025 insertions, 999 deletions
diff --git a/internal/gitaly/storage/storagemgr/transaction_manager_repo_test.go b/internal/gitaly/storage/storagemgr/transaction_manager_repo_test.go new file mode 100644 index 000000000..a28c3241b --- /dev/null +++ b/internal/gitaly/storage/storagemgr/transaction_manager_repo_test.go @@ -0,0 +1,1023 @@ +package storagemgr + +import ( + "io/fs" + "testing" + + "gitlab.com/gitlab-org/gitaly/v16/internal/git" + "gitlab.com/gitlab-org/gitaly/v16/internal/helper/perm" + "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper" +) + +func generateCreateRepositoryTests(t *testing.T, setup testTransactionSetup) []transactionTestCase { + umask := testhelper.Umask() + + return []transactionTestCase{ + { + desc: "create repository when it doesn't exist", + steps: steps{ + RemoveRepository{}, + StartManager{}, + Begin{ + RelativePath: setup.RelativePath, + }, + CreateRepository{}, + Commit{}, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(1).toProto(), + }, + Repositories: RepositoryStates{ + setup.RelativePath: { + Objects: []git.ObjectID{}, + }, + }, + }, + }, + { + desc: "create repository when it already exists", + steps: steps{ + RemoveRepository{}, + StartManager{}, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + }, + CreateRepository{ + TransactionID: 1, + }, + CreateRepository{ + TransactionID: 2, + }, + Commit{ + TransactionID: 1, + }, + Commit{ + TransactionID: 2, + ExpectedError: ErrRepositoryAlreadyExists, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(1).toProto(), + }, + Repositories: RepositoryStates{ + setup.RelativePath: { + Objects: []git.ObjectID{}, + }, + }, + }, + }, + { + desc: "create repository with full state", + steps: steps{ + RemoveRepository{}, + StartManager{}, + Begin{ + RelativePath: setup.RelativePath, + }, + CreateRepository{ + DefaultBranch: "refs/heads/branch", + References: map[git.ReferenceName]git.ObjectID{ + "refs/heads/main": setup.Commits.First.OID, + "refs/heads/branch": setup.Commits.Second.OID, + }, + Packs: [][]byte{setup.Commits.First.Pack, setup.Commits.Second.Pack}, + CustomHooks: validCustomHooks(t), + }, + Commit{}, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(1).toProto(), + }, + Directory: testhelper.DirectoryState{ + "/": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/1": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/1/objects.idx": indexFileDirectoryEntry(setup.Config), + "/wal/1/objects.pack": packFileDirectoryEntry( + setup.Config, + []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + setup.Commits.Second.OID, + }, + ), + "/wal/1/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), + }, + Repositories: RepositoryStates{ + setup.RelativePath: { + DefaultBranch: "refs/heads/branch", + References: []git.Reference{ + {Name: "refs/heads/branch", Target: setup.Commits.Second.OID.String()}, + {Name: "refs/heads/main", Target: setup.Commits.First.OID.String()}, + }, + Objects: []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + setup.Commits.Second.OID, + }, + CustomHooks: testhelper.DirectoryState{ + "/": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, + "/pre-receive": { + Mode: umask.Mask(fs.ModePerm), + Content: []byte("hook content"), + }, + "/private-dir": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, + "/private-dir/private-file": {Mode: umask.Mask(perm.PrivateFile), Content: []byte("private content")}, + }, + }, + }, + }, + }, + { + desc: "transactions are snapshot isolated from concurrent creations", + steps: steps{ + RemoveRepository{}, + StartManager{}, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + CreateRepository{ + TransactionID: 1, + References: map[git.ReferenceName]git.ObjectID{ + "refs/heads/main": setup.Commits.First.OID, + }, + Packs: [][]byte{setup.Commits.First.Pack}, + CustomHooks: validCustomHooks(t), + }, + Commit{ + TransactionID: 1, + }, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 1, + }, + Begin{ + TransactionID: 3, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 1, + }, + Commit{ + TransactionID: 3, + DeleteRepository: true, + }, + Begin{ + TransactionID: 4, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 2, + }, + CreateRepository{ + TransactionID: 4, + DefaultBranch: "refs/heads/other", + References: map[git.ReferenceName]git.ObjectID{ + "refs/heads/other": setup.Commits.Second.OID, + }, + Packs: [][]byte{setup.Commits.First.Pack, setup.Commits.Second.Pack}, + }, + Commit{ + TransactionID: 4, + }, + // Transaction 2 has been open through out the repository deletion and creation. It should + // still see the original state of the repository before the deletion. + RepositoryAssertion{ + TransactionID: 2, + Repositories: RepositoryStates{ + setup.RelativePath: { + DefaultBranch: "refs/heads/main", + References: []git.Reference{ + {Name: "refs/heads/main", Target: setup.Commits.First.OID.String()}, + }, + Objects: []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + }, + CustomHooks: testhelper.DirectoryState{ + "/": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, + "/pre-receive": { + Mode: umask.Mask(fs.ModePerm), + Content: []byte("hook content"), + }, + "/private-dir": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, + "/private-dir/private-file": {Mode: umask.Mask(perm.PrivateFile), Content: []byte("private content")}, + }, + }, + }, + }, + Rollback{ + TransactionID: 2, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(3).toProto(), + }, + Directory: testhelper.DirectoryState{ + "/": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/1": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/1/objects.idx": indexFileDirectoryEntry(setup.Config), + "/wal/1/objects.pack": packFileDirectoryEntry( + setup.Config, + []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + }, + ), + "/wal/1/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), + "/wal/3": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/3/objects.idx": indexFileDirectoryEntry(setup.Config), + "/wal/3/objects.pack": packFileDirectoryEntry( + setup.Config, + []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + setup.Commits.Second.OID, + }, + ), + "/wal/3/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), + }, + Repositories: RepositoryStates{ + setup.RelativePath: { + DefaultBranch: "refs/heads/other", + References: []git.Reference{ + {Name: "refs/heads/other", Target: setup.Commits.Second.OID.String()}, + }, + Objects: []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + setup.Commits.Second.OID, + }, + }, + }, + }, + }, + { + desc: "logged repository creation is respected", + steps: steps{ + RemoveRepository{}, + StartManager{ + Hooks: testTransactionHooks{ + BeforeApplyLogEntry: func(hookContext) { + panic(errSimulatedCrash) + }, + }, + ExpectedError: errSimulatedCrash, + }, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + CreateRepository{ + TransactionID: 1, + }, + Commit{ + TransactionID: 1, + ExpectedError: ErrTransactionProcessingStopped, + }, + AssertManager{ + ExpectedError: errSimulatedCrash, + }, + StartManager{}, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 1, + }, + Commit{ + TransactionID: 2, + DeleteRepository: true, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(2).toProto(), + }, + Repositories: RepositoryStates{}, + }, + }, + { + desc: "reapplying repository creation works", + steps: steps{ + RemoveRepository{}, + StartManager{ + Hooks: testTransactionHooks{ + BeforeStoreAppliedLSN: func(hookContext) { + panic(errSimulatedCrash) + }, + }, + ExpectedError: errSimulatedCrash, + }, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + CreateRepository{ + TransactionID: 1, + DefaultBranch: "refs/heads/branch", + Packs: [][]byte{setup.Commits.First.Pack}, + References: map[git.ReferenceName]git.ObjectID{ + "refs/heads/main": setup.Commits.First.OID, + }, + CustomHooks: validCustomHooks(t), + }, + Commit{ + TransactionID: 1, + ExpectedError: ErrTransactionProcessingStopped, + }, + AssertManager{ + ExpectedError: errSimulatedCrash, + }, + StartManager{}, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(1).toProto(), + }, + Directory: testhelper.DirectoryState{ + "/": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/1": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/1/objects.idx": indexFileDirectoryEntry(setup.Config), + "/wal/1/objects.pack": packFileDirectoryEntry( + setup.Config, + []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + }, + ), + "/wal/1/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), + }, + Repositories: RepositoryStates{ + setup.RelativePath: { + DefaultBranch: "refs/heads/branch", + References: []git.Reference{ + {Name: "refs/heads/main", Target: setup.Commits.First.OID.String()}, + }, + CustomHooks: testhelper.DirectoryState{ + "/": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, + "/pre-receive": { + Mode: umask.Mask(fs.ModePerm), + Content: []byte("hook content"), + }, + "/private-dir": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, + "/private-dir/private-file": {Mode: umask.Mask(perm.PrivateFile), Content: []byte("private content")}, + }, + Objects: []git.ObjectID{ + setup.Commits.First.OID, + setup.ObjectHash.EmptyTreeOID, + }, + }, + }, + }, + }, + { + desc: "commit without creating a repository", + steps: steps{ + RemoveRepository{}, + StartManager{}, + Begin{ + RelativePath: setup.RelativePath, + }, + Commit{}, + }, + expectedState: StateAssertion{ + Repositories: RepositoryStates{}, + }, + }, + { + desc: "two repositories created in different transactions", + steps: steps{ + RemoveRepository{}, + StartManager{}, + Begin{ + TransactionID: 1, + RelativePath: "repository-1", + }, + Begin{ + TransactionID: 2, + RelativePath: "repository-2", + }, + CreateRepository{ + TransactionID: 1, + References: map[git.ReferenceName]git.ObjectID{ + "refs/heads/main": setup.Commits.First.OID, + }, + Packs: [][]byte{setup.Commits.First.Pack}, + CustomHooks: validCustomHooks(t), + }, + CreateRepository{ + TransactionID: 2, + References: map[git.ReferenceName]git.ObjectID{ + "refs/heads/branch": setup.Commits.Third.OID, + }, + DefaultBranch: "refs/heads/branch", + Packs: [][]byte{ + setup.Commits.First.Pack, + setup.Commits.Second.Pack, + setup.Commits.Third.Pack, + }, + }, + Commit{ + TransactionID: 1, + }, + Commit{ + TransactionID: 2, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(2).toProto(), + }, + Repositories: RepositoryStates{ + "repository-1": { + References: []git.Reference{ + {Name: "refs/heads/main", Target: setup.Commits.First.OID.String()}, + }, + Objects: []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + }, + CustomHooks: testhelper.DirectoryState{ + "/": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, + "/pre-receive": { + Mode: umask.Mask(fs.ModePerm), + Content: []byte("hook content"), + }, + "/private-dir": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, + "/private-dir/private-file": {Mode: umask.Mask(perm.PrivateFile), Content: []byte("private content")}, + }, + }, + "repository-2": { + DefaultBranch: "refs/heads/branch", + References: []git.Reference{ + {Name: "refs/heads/branch", Target: setup.Commits.Third.OID.String()}, + }, + Objects: []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + setup.Commits.Second.OID, + setup.Commits.Third.OID, + }, + }, + }, + Directory: testhelper.DirectoryState{ + "/": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/1": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/1/objects.idx": indexFileDirectoryEntry(setup.Config), + "/wal/1/objects.pack": packFileDirectoryEntry( + setup.Config, + []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + }, + ), + "/wal/1/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), + "/wal/2": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/2/objects.idx": indexFileDirectoryEntry(setup.Config), + "/wal/2/objects.pack": packFileDirectoryEntry( + setup.Config, + []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + setup.Commits.Second.OID, + setup.Commits.Third.OID, + }, + ), + "/wal/2/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), + }, + }, + }, + } +} + +func generateDeleteRepositoryTests(t *testing.T, setup testTransactionSetup) []transactionTestCase { + umask := testhelper.Umask() + + return []transactionTestCase{ + { + desc: "repository is successfully deleted", + steps: steps{ + StartManager{}, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + Commit{ + TransactionID: 1, + DeleteRepository: true, + }, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 1, + }, + RepositoryAssertion{ + TransactionID: 2, + Repositories: RepositoryStates{}, + }, + Rollback{ + TransactionID: 2, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(1).toProto(), + }, + Repositories: RepositoryStates{}, + }, + }, + { + desc: "repository deletion fails if repository is deleted", + steps: steps{ + StartManager{}, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + }, + Commit{ + TransactionID: 1, + DeleteRepository: true, + }, + Commit{ + TransactionID: 2, + DeleteRepository: true, + ExpectedError: ErrRepositoryNotFound, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(1).toProto(), + }, + Repositories: RepositoryStates{}, + }, + }, + { + desc: "custom hooks update fails if repository is deleted", + steps: steps{ + StartManager{}, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + }, + Commit{ + TransactionID: 1, + DeleteRepository: true, + }, + Commit{ + TransactionID: 2, + CustomHooksUpdate: &CustomHooksUpdate{validCustomHooks(t)}, + ExpectedError: ErrRepositoryNotFound, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(1).toProto(), + }, + Repositories: RepositoryStates{}, + }, + }, + { + desc: "reference updates fail if repository is deleted", + steps: steps{ + StartManager{}, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + }, + Commit{ + TransactionID: 1, + DeleteRepository: true, + }, + Commit{ + TransactionID: 2, + ReferenceUpdates: ReferenceUpdates{ + "refs/heads/main": {OldOID: setup.ObjectHash.ZeroOID, NewOID: setup.Commits.First.OID}, + }, + ExpectedError: ErrRepositoryNotFound, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(1).toProto(), + }, + Repositories: RepositoryStates{}, + }, + }, + { + desc: "default branch update fails if repository is deleted", + steps: steps{ + StartManager{}, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + }, + Commit{ + TransactionID: 1, + DeleteRepository: true, + }, + Commit{ + TransactionID: 2, + DefaultBranchUpdate: &DefaultBranchUpdate{ + Reference: "refs/heads/new-default", + }, + ExpectedError: ErrRepositoryNotFound, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(1).toProto(), + }, + Repositories: RepositoryStates{}, + }, + }, + { + desc: "logged repository deletions are considered after restart", + steps: steps{ + StartManager{ + Hooks: testTransactionHooks{ + BeforeApplyLogEntry: func(hookContext) { + panic(errSimulatedCrash) + }, + }, + ExpectedError: errSimulatedCrash, + }, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + Commit{ + TransactionID: 1, + DeleteRepository: true, + ExpectedError: ErrTransactionProcessingStopped, + }, + AssertManager{ + ExpectedError: errSimulatedCrash, + }, + StartManager{}, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 1, + }, + RepositoryAssertion{ + TransactionID: 2, + Repositories: RepositoryStates{}, + }, + Rollback{ + TransactionID: 2, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(1).toProto(), + }, + Repositories: RepositoryStates{}, + }, + }, + { + desc: "reapplying repository deletion works", + steps: steps{ + StartManager{ + Hooks: testTransactionHooks{ + BeforeStoreAppliedLSN: func(hookContext) { + panic(errSimulatedCrash) + }, + }, + ExpectedError: errSimulatedCrash, + }, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + Commit{ + TransactionID: 1, + DeleteRepository: true, + ExpectedError: ErrTransactionProcessingStopped, + }, + AssertManager{ + ExpectedError: errSimulatedCrash, + }, + StartManager{}, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 1, + }, + RepositoryAssertion{ + TransactionID: 2, + Repositories: RepositoryStates{}, + }, + Rollback{ + TransactionID: 2, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(1).toProto(), + }, + Repositories: RepositoryStates{}, + }, + }, + { + // This is a serialization violation as the outcome would be different + // if the transactions were applied in different order. + desc: "deletion succeeds with concurrent writes to repository", + steps: steps{ + StartManager{}, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + }, + Commit{ + TransactionID: 1, + DefaultBranchUpdate: &DefaultBranchUpdate{ + Reference: "refs/heads/branch", + }, + ReferenceUpdates: ReferenceUpdates{ + "refs/heads/branch": {OldOID: setup.ObjectHash.ZeroOID, NewOID: setup.Commits.First.OID}, + }, + CustomHooksUpdate: &CustomHooksUpdate{ + CustomHooksTAR: validCustomHooks(t), + }, + }, + Commit{ + TransactionID: 2, + DeleteRepository: true, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(2).toProto(), + }, + Directory: testhelper.DirectoryState{ + "/": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, + }, + Repositories: RepositoryStates{}, + }, + }, + { + desc: "deletion waits until other transactions are done", + steps: steps{ + StartManager{}, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + }, + Commit{ + TransactionID: 1, + DeleteRepository: true, + }, + // The concurrent transaction should be able to read the + // repository despite the committed deletion. + RepositoryAssertion{ + TransactionID: 2, + Repositories: RepositoryStates{ + setup.RelativePath: { + DefaultBranch: "refs/heads/main", + Objects: []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + setup.Commits.Second.OID, + setup.Commits.Third.OID, + setup.Commits.Diverging.OID, + }, + }, + }, + }, + Rollback{ + TransactionID: 2, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(1).toProto(), + }, + Repositories: RepositoryStates{}, + }, + }, + { + desc: "read-only transaction fails with repository deletion staged", + steps: steps{ + StartManager{}, + Begin{ + RelativePath: setup.RelativePath, + ReadOnly: true, + }, + Commit{ + DeleteRepository: true, + ExpectedError: errReadOnlyRepositoryDeletion, + }, + }, + }, + { + desc: "transactions are snapshot isolated from concurrent deletions", + steps: steps{ + Prune{}, + StartManager{}, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + Commit{ + TransactionID: 1, + DefaultBranchUpdate: &DefaultBranchUpdate{ + Reference: "refs/heads/new-head", + }, + ReferenceUpdates: ReferenceUpdates{ + "refs/heads/main": {OldOID: setup.ObjectHash.ZeroOID, NewOID: setup.Commits.First.OID}, + }, + QuarantinedPacks: [][]byte{setup.Commits.First.Pack}, + CustomHooksUpdate: &CustomHooksUpdate{ + CustomHooksTAR: validCustomHooks(t), + }, + }, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 1, + }, + Begin{ + TransactionID: 3, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 1, + }, + Commit{ + TransactionID: 2, + DeleteRepository: true, + }, + // This transaction was started before the deletion, so it should see the old state regardless + // of the repository being deleted. + RepositoryAssertion{ + TransactionID: 3, + Repositories: RepositoryStates{ + setup.RelativePath: { + DefaultBranch: "refs/heads/new-head", + References: []git.Reference{ + {Name: "refs/heads/main", Target: setup.Commits.First.OID.String()}, + }, + Objects: []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + }, + CustomHooks: testhelper.DirectoryState{ + "/": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, + "/pre-receive": { + Mode: umask.Mask(fs.ModePerm), + Content: []byte("hook content"), + }, + "/private-dir": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, + "/private-dir/private-file": {Mode: umask.Mask(perm.PrivateFile), Content: []byte("private content")}, + }, + }, + }, + }, + Rollback{ + TransactionID: 3, + }, + Begin{ + TransactionID: 4, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 2, + }, + RepositoryAssertion{ + TransactionID: 4, + Repositories: RepositoryStates{}, + }, + Rollback{ + TransactionID: 4, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(2).toProto(), + }, + Directory: testhelper.DirectoryState{ + "/": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/1": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/1/objects.idx": indexFileDirectoryEntry(setup.Config), + "/wal/1/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), + "/wal/1/objects.pack": packFileDirectoryEntry( + setup.Config, + []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + }, + ), + }, + Repositories: RepositoryStates{}, + }, + }, + { + desc: "create repository again after deletion", + steps: steps{ + RemoveRepository{}, + StartManager{}, + Begin{ + TransactionID: 1, + RelativePath: setup.RelativePath, + }, + CreateRepository{ + TransactionID: 1, + }, + Commit{ + TransactionID: 1, + }, + Begin{ + TransactionID: 2, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 1, + }, + Commit{ + TransactionID: 2, + ReferenceUpdates: ReferenceUpdates{ + "refs/heads/main": {OldOID: setup.ObjectHash.ZeroOID, NewOID: setup.Commits.First.OID}, + }, + QuarantinedPacks: [][]byte{setup.Commits.First.Pack}, + CustomHooksUpdate: &CustomHooksUpdate{ + CustomHooksTAR: validCustomHooks(t), + }, + }, + Begin{ + TransactionID: 3, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 2, + }, + Commit{ + TransactionID: 3, + DeleteRepository: true, + }, + Begin{ + TransactionID: 4, + RelativePath: setup.RelativePath, + ExpectedSnapshotLSN: 3, + }, + CreateRepository{ + TransactionID: 4, + }, + Commit{ + TransactionID: 4, + }, + }, + expectedState: StateAssertion{ + Database: DatabaseState{ + string(keyAppliedLSN(setup.PartitionID)): LSN(4).toProto(), + }, + Directory: testhelper.DirectoryState{ + "/": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/2": {Mode: fs.ModeDir | perm.PrivateDir}, + "/wal/2/objects.idx": indexFileDirectoryEntry(setup.Config), + "/wal/2/objects.pack": packFileDirectoryEntry( + setup.Config, + []git.ObjectID{ + setup.ObjectHash.EmptyTreeOID, + setup.Commits.First.OID, + }, + ), + "/wal/2/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), + }, + Repositories: RepositoryStates{ + setup.RelativePath: { + Objects: []git.ObjectID{}, + }, + }, + }, + }, + } +} diff --git a/internal/gitaly/storage/storagemgr/transaction_manager_test.go b/internal/gitaly/storage/storagemgr/transaction_manager_test.go index b6f47ccc1..fe07b32c8 100644 --- a/internal/gitaly/storage/storagemgr/transaction_manager_test.go +++ b/internal/gitaly/storage/storagemgr/transaction_manager_test.go @@ -1919,246 +1919,6 @@ func TestTransactionManager(t *testing.T) { }, }, { - desc: "repository is successfully deleted", - steps: steps{ - StartManager{}, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - Commit{ - TransactionID: 1, - DeleteRepository: true, - }, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - ExpectedSnapshotLSN: 1, - }, - RepositoryAssertion{ - TransactionID: 2, - Repositories: RepositoryStates{}, - }, - Rollback{ - TransactionID: 2, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(1).toProto(), - }, - Repositories: RepositoryStates{}, - }, - }, - { - desc: "repository deletion fails if repository is deleted", - steps: steps{ - StartManager{}, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - }, - Commit{ - TransactionID: 1, - DeleteRepository: true, - }, - Commit{ - TransactionID: 2, - DeleteRepository: true, - ExpectedError: ErrRepositoryNotFound, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(1).toProto(), - }, - Repositories: RepositoryStates{}, - }, - }, - { - desc: "custom hooks update fails if repository is deleted", - steps: steps{ - StartManager{}, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - }, - Commit{ - TransactionID: 1, - DeleteRepository: true, - }, - Commit{ - TransactionID: 2, - CustomHooksUpdate: &CustomHooksUpdate{validCustomHooks(t)}, - ExpectedError: ErrRepositoryNotFound, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(1).toProto(), - }, - Repositories: RepositoryStates{}, - }, - }, - { - desc: "reference updates fail if repository is deleted", - steps: steps{ - StartManager{}, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - }, - Commit{ - TransactionID: 1, - DeleteRepository: true, - }, - Commit{ - TransactionID: 2, - ReferenceUpdates: ReferenceUpdates{ - "refs/heads/main": {OldOID: setup.ObjectHash.ZeroOID, NewOID: setup.Commits.First.OID}, - }, - ExpectedError: ErrRepositoryNotFound, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(1).toProto(), - }, - Repositories: RepositoryStates{}, - }, - }, - { - desc: "default branch update fails if repository is deleted", - steps: steps{ - StartManager{}, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - }, - Commit{ - TransactionID: 1, - DeleteRepository: true, - }, - Commit{ - TransactionID: 2, - DefaultBranchUpdate: &DefaultBranchUpdate{ - Reference: "refs/heads/new-default", - }, - ExpectedError: ErrRepositoryNotFound, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(1).toProto(), - }, - Repositories: RepositoryStates{}, - }, - }, - { - desc: "logged repository deletions are considered after restart", - steps: steps{ - StartManager{ - Hooks: testTransactionHooks{ - BeforeApplyLogEntry: func(hookContext) { - panic(errSimulatedCrash) - }, - }, - ExpectedError: errSimulatedCrash, - }, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - Commit{ - TransactionID: 1, - DeleteRepository: true, - ExpectedError: ErrTransactionProcessingStopped, - }, - AssertManager{ - ExpectedError: errSimulatedCrash, - }, - StartManager{}, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - ExpectedSnapshotLSN: 1, - }, - RepositoryAssertion{ - TransactionID: 2, - Repositories: RepositoryStates{}, - }, - Rollback{ - TransactionID: 2, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(1).toProto(), - }, - Repositories: RepositoryStates{}, - }, - }, - { - desc: "reapplying repository deletion works", - steps: steps{ - StartManager{ - Hooks: testTransactionHooks{ - BeforeStoreAppliedLSN: func(hookContext) { - panic(errSimulatedCrash) - }, - }, - ExpectedError: errSimulatedCrash, - }, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - Commit{ - TransactionID: 1, - DeleteRepository: true, - ExpectedError: ErrTransactionProcessingStopped, - }, - AssertManager{ - ExpectedError: errSimulatedCrash, - }, - StartManager{}, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - ExpectedSnapshotLSN: 1, - }, - RepositoryAssertion{ - TransactionID: 2, - Repositories: RepositoryStates{}, - }, - Rollback{ - TransactionID: 2, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(1).toProto(), - }, - Repositories: RepositoryStates{}, - }, - }, - { desc: "non-existent repository is correctly handled", steps: steps{ RemoveRepository{}, @@ -2180,92 +1940,6 @@ func TestTransactionManager(t *testing.T) { }, }, { - // This is a serialization violation as the outcome would be different - // if the transactions were applied in different order. - desc: "deletion succeeds with concurrent writes to repository", - steps: steps{ - StartManager{}, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - }, - Commit{ - TransactionID: 1, - DefaultBranchUpdate: &DefaultBranchUpdate{ - Reference: "refs/heads/branch", - }, - ReferenceUpdates: ReferenceUpdates{ - "refs/heads/branch": {OldOID: setup.ObjectHash.ZeroOID, NewOID: setup.Commits.First.OID}, - }, - CustomHooksUpdate: &CustomHooksUpdate{ - CustomHooksTAR: validCustomHooks(t), - }, - }, - Commit{ - TransactionID: 2, - DeleteRepository: true, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(2).toProto(), - }, - Directory: testhelper.DirectoryState{ - "/": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, - }, - Repositories: RepositoryStates{}, - }, - }, - { - desc: "deletion waits until other transactions are done", - steps: steps{ - StartManager{}, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - }, - Commit{ - TransactionID: 1, - DeleteRepository: true, - }, - // The concurrent transaction should be able to read the - // repository despite the committed deletion. - RepositoryAssertion{ - TransactionID: 2, - Repositories: RepositoryStates{ - relativePath: { - DefaultBranch: "refs/heads/main", - Objects: []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - setup.Commits.Second.OID, - setup.Commits.Third.OID, - setup.Commits.Diverging.OID, - }, - }, - }, - }, - Rollback{ - TransactionID: 2, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(1).toProto(), - }, - Repositories: RepositoryStates{}, - }, - }, - { desc: "failing initialization prevents transaction beginning", steps: steps{ StartManager{ @@ -2415,20 +2089,6 @@ func TestTransactionManager(t *testing.T) { }, }, { - desc: "read-only transaction fails with repository deletion staged", - steps: steps{ - StartManager{}, - Begin{ - RelativePath: relativePath, - ReadOnly: true, - }, - Commit{ - DeleteRepository: true, - ExpectedError: errReadOnlyRepositoryDeletion, - }, - }, - }, - { desc: "read-only transaction fails with objects staged", steps: steps{ StartManager{}, @@ -2554,665 +2214,6 @@ func TestTransactionManager(t *testing.T) { }, }, { - desc: "transactions are snapshot isolated from concurrent deletions", - steps: steps{ - Prune{}, - StartManager{}, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - Commit{ - TransactionID: 1, - DefaultBranchUpdate: &DefaultBranchUpdate{ - Reference: "refs/heads/new-head", - }, - ReferenceUpdates: ReferenceUpdates{ - "refs/heads/main": {OldOID: setup.ObjectHash.ZeroOID, NewOID: setup.Commits.First.OID}, - }, - QuarantinedPacks: [][]byte{setup.Commits.First.Pack}, - CustomHooksUpdate: &CustomHooksUpdate{ - CustomHooksTAR: validCustomHooks(t), - }, - }, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - ExpectedSnapshotLSN: 1, - }, - Begin{ - TransactionID: 3, - RelativePath: relativePath, - ExpectedSnapshotLSN: 1, - }, - Commit{ - TransactionID: 2, - DeleteRepository: true, - }, - // This transaction was started before the deletion, so it should see the old state regardless - // of the repository being deleted. - RepositoryAssertion{ - TransactionID: 3, - Repositories: RepositoryStates{ - relativePath: { - DefaultBranch: "refs/heads/new-head", - References: []git.Reference{ - {Name: "refs/heads/main", Target: setup.Commits.First.OID.String()}, - }, - Objects: []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - }, - CustomHooks: testhelper.DirectoryState{ - "/": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, - "/pre-receive": { - Mode: umask.Mask(fs.ModePerm), - Content: []byte("hook content"), - }, - "/private-dir": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, - "/private-dir/private-file": {Mode: umask.Mask(perm.PrivateFile), Content: []byte("private content")}, - }, - }, - }, - }, - Rollback{ - TransactionID: 3, - }, - Begin{ - TransactionID: 4, - RelativePath: relativePath, - ExpectedSnapshotLSN: 2, - }, - RepositoryAssertion{ - TransactionID: 4, - Repositories: RepositoryStates{}, - }, - Rollback{ - TransactionID: 4, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(2).toProto(), - }, - Directory: testhelper.DirectoryState{ - "/": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/1": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/1/objects.idx": indexFileDirectoryEntry(setup.Config), - "/wal/1/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), - "/wal/1/objects.pack": packFileDirectoryEntry( - setup.Config, - []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - }, - ), - }, - Repositories: RepositoryStates{}, - }, - }, - { - desc: "create repository when it doesn't exist", - steps: steps{ - RemoveRepository{}, - StartManager{}, - Begin{ - RelativePath: relativePath, - }, - CreateRepository{}, - Commit{}, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(1).toProto(), - }, - Repositories: RepositoryStates{ - relativePath: { - Objects: []git.ObjectID{}, - }, - }, - }, - }, - { - desc: "create repository when it already exists", - steps: steps{ - RemoveRepository{}, - StartManager{}, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - }, - CreateRepository{ - TransactionID: 1, - }, - CreateRepository{ - TransactionID: 2, - }, - Commit{ - TransactionID: 1, - }, - Commit{ - TransactionID: 2, - ExpectedError: ErrRepositoryAlreadyExists, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(1).toProto(), - }, - Repositories: RepositoryStates{ - relativePath: { - Objects: []git.ObjectID{}, - }, - }, - }, - }, - { - desc: "create repository again after deletion", - steps: steps{ - RemoveRepository{}, - StartManager{}, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - CreateRepository{ - TransactionID: 1, - }, - Commit{ - TransactionID: 1, - }, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - ExpectedSnapshotLSN: 1, - }, - Commit{ - TransactionID: 2, - ReferenceUpdates: ReferenceUpdates{ - "refs/heads/main": {OldOID: setup.ObjectHash.ZeroOID, NewOID: setup.Commits.First.OID}, - }, - QuarantinedPacks: [][]byte{setup.Commits.First.Pack}, - CustomHooksUpdate: &CustomHooksUpdate{ - CustomHooksTAR: validCustomHooks(t), - }, - }, - Begin{ - TransactionID: 3, - RelativePath: relativePath, - ExpectedSnapshotLSN: 2, - }, - Commit{ - TransactionID: 3, - DeleteRepository: true, - }, - Begin{ - TransactionID: 4, - RelativePath: relativePath, - ExpectedSnapshotLSN: 3, - }, - CreateRepository{ - TransactionID: 4, - }, - Commit{ - TransactionID: 4, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(4).toProto(), - }, - Directory: testhelper.DirectoryState{ - "/": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/2": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/2/objects.idx": indexFileDirectoryEntry(setup.Config), - "/wal/2/objects.pack": packFileDirectoryEntry( - setup.Config, - []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - }, - ), - "/wal/2/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), - }, - Repositories: RepositoryStates{ - relativePath: { - Objects: []git.ObjectID{}, - }, - }, - }, - }, - { - desc: "create repository with full state", - steps: steps{ - RemoveRepository{}, - StartManager{}, - Begin{ - RelativePath: relativePath, - }, - CreateRepository{ - DefaultBranch: "refs/heads/branch", - References: map[git.ReferenceName]git.ObjectID{ - "refs/heads/main": setup.Commits.First.OID, - "refs/heads/branch": setup.Commits.Second.OID, - }, - Packs: [][]byte{setup.Commits.First.Pack, setup.Commits.Second.Pack}, - CustomHooks: validCustomHooks(t), - }, - Commit{}, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(1).toProto(), - }, - Directory: testhelper.DirectoryState{ - "/": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/1": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/1/objects.idx": indexFileDirectoryEntry(setup.Config), - "/wal/1/objects.pack": packFileDirectoryEntry( - setup.Config, - []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - setup.Commits.Second.OID, - }, - ), - "/wal/1/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), - }, - Repositories: RepositoryStates{ - relativePath: { - DefaultBranch: "refs/heads/branch", - References: []git.Reference{ - {Name: "refs/heads/branch", Target: setup.Commits.Second.OID.String()}, - {Name: "refs/heads/main", Target: setup.Commits.First.OID.String()}, - }, - Objects: []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - setup.Commits.Second.OID, - }, - CustomHooks: testhelper.DirectoryState{ - "/": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, - "/pre-receive": { - Mode: umask.Mask(fs.ModePerm), - Content: []byte("hook content"), - }, - "/private-dir": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, - "/private-dir/private-file": {Mode: umask.Mask(perm.PrivateFile), Content: []byte("private content")}, - }, - }, - }, - }, - }, - { - desc: "transactions are snapshot isolated from concurrent creations", - steps: steps{ - RemoveRepository{}, - StartManager{}, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - CreateRepository{ - TransactionID: 1, - References: map[git.ReferenceName]git.ObjectID{ - "refs/heads/main": setup.Commits.First.OID, - }, - Packs: [][]byte{setup.Commits.First.Pack}, - CustomHooks: validCustomHooks(t), - }, - Commit{ - TransactionID: 1, - }, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - ExpectedSnapshotLSN: 1, - }, - Begin{ - TransactionID: 3, - RelativePath: relativePath, - ExpectedSnapshotLSN: 1, - }, - Commit{ - TransactionID: 3, - DeleteRepository: true, - }, - Begin{ - TransactionID: 4, - RelativePath: relativePath, - ExpectedSnapshotLSN: 2, - }, - CreateRepository{ - TransactionID: 4, - DefaultBranch: "refs/heads/other", - References: map[git.ReferenceName]git.ObjectID{ - "refs/heads/other": setup.Commits.Second.OID, - }, - Packs: [][]byte{setup.Commits.First.Pack, setup.Commits.Second.Pack}, - }, - Commit{ - TransactionID: 4, - }, - // Transaction 2 has been open through out the repository deletion and creation. It should - // still see the original state of the repository before the deletion. - RepositoryAssertion{ - TransactionID: 2, - Repositories: RepositoryStates{ - relativePath: { - DefaultBranch: "refs/heads/main", - References: []git.Reference{ - {Name: "refs/heads/main", Target: setup.Commits.First.OID.String()}, - }, - Objects: []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - }, - CustomHooks: testhelper.DirectoryState{ - "/": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, - "/pre-receive": { - Mode: umask.Mask(fs.ModePerm), - Content: []byte("hook content"), - }, - "/private-dir": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, - "/private-dir/private-file": {Mode: umask.Mask(perm.PrivateFile), Content: []byte("private content")}, - }, - }, - }, - }, - Rollback{ - TransactionID: 2, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(3).toProto(), - }, - Directory: testhelper.DirectoryState{ - "/": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/1": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/1/objects.idx": indexFileDirectoryEntry(setup.Config), - "/wal/1/objects.pack": packFileDirectoryEntry( - setup.Config, - []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - }, - ), - "/wal/1/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), - "/wal/3": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/3/objects.idx": indexFileDirectoryEntry(setup.Config), - "/wal/3/objects.pack": packFileDirectoryEntry( - setup.Config, - []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - setup.Commits.Second.OID, - }, - ), - "/wal/3/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), - }, - Repositories: RepositoryStates{ - relativePath: { - DefaultBranch: "refs/heads/other", - References: []git.Reference{ - {Name: "refs/heads/other", Target: setup.Commits.Second.OID.String()}, - }, - Objects: []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - setup.Commits.Second.OID, - }, - }, - }, - }, - }, - { - desc: "logged repository creation is respected", - steps: steps{ - RemoveRepository{}, - StartManager{ - Hooks: testTransactionHooks{ - BeforeApplyLogEntry: func(hookContext) { - panic(errSimulatedCrash) - }, - }, - ExpectedError: errSimulatedCrash, - }, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - CreateRepository{ - TransactionID: 1, - }, - Commit{ - TransactionID: 1, - ExpectedError: ErrTransactionProcessingStopped, - }, - AssertManager{ - ExpectedError: errSimulatedCrash, - }, - StartManager{}, - Begin{ - TransactionID: 2, - RelativePath: relativePath, - ExpectedSnapshotLSN: 1, - }, - Commit{ - TransactionID: 2, - DeleteRepository: true, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(2).toProto(), - }, - Repositories: RepositoryStates{}, - }, - }, - { - desc: "reapplying repository creation works", - steps: steps{ - RemoveRepository{}, - StartManager{ - Hooks: testTransactionHooks{ - BeforeStoreAppliedLSN: func(hookContext) { - panic(errSimulatedCrash) - }, - }, - ExpectedError: errSimulatedCrash, - }, - Begin{ - TransactionID: 1, - RelativePath: relativePath, - }, - CreateRepository{ - TransactionID: 1, - DefaultBranch: "refs/heads/branch", - Packs: [][]byte{setup.Commits.First.Pack}, - References: map[git.ReferenceName]git.ObjectID{ - "refs/heads/main": setup.Commits.First.OID, - }, - CustomHooks: validCustomHooks(t), - }, - Commit{ - TransactionID: 1, - ExpectedError: ErrTransactionProcessingStopped, - }, - AssertManager{ - ExpectedError: errSimulatedCrash, - }, - StartManager{}, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(1).toProto(), - }, - Directory: testhelper.DirectoryState{ - "/": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/1": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/1/objects.idx": indexFileDirectoryEntry(setup.Config), - "/wal/1/objects.pack": packFileDirectoryEntry( - setup.Config, - []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - }, - ), - "/wal/1/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), - }, - Repositories: RepositoryStates{ - relativePath: { - DefaultBranch: "refs/heads/branch", - References: []git.Reference{ - {Name: "refs/heads/main", Target: setup.Commits.First.OID.String()}, - }, - CustomHooks: testhelper.DirectoryState{ - "/": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, - "/pre-receive": { - Mode: umask.Mask(fs.ModePerm), - Content: []byte("hook content"), - }, - "/private-dir": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, - "/private-dir/private-file": {Mode: umask.Mask(perm.PrivateFile), Content: []byte("private content")}, - }, - Objects: []git.ObjectID{ - setup.Commits.First.OID, - setup.ObjectHash.EmptyTreeOID, - }, - }, - }, - }, - }, - { - desc: "commit without creating a repository", - steps: steps{ - RemoveRepository{}, - StartManager{}, - Begin{ - RelativePath: relativePath, - }, - Commit{}, - }, - expectedState: StateAssertion{ - Repositories: RepositoryStates{}, - }, - }, - { - desc: "two repositories created in different transactions", - steps: steps{ - RemoveRepository{}, - StartManager{}, - Begin{ - TransactionID: 1, - RelativePath: "repository-1", - }, - Begin{ - TransactionID: 2, - RelativePath: "repository-2", - }, - CreateRepository{ - TransactionID: 1, - References: map[git.ReferenceName]git.ObjectID{ - "refs/heads/main": setup.Commits.First.OID, - }, - Packs: [][]byte{setup.Commits.First.Pack}, - CustomHooks: validCustomHooks(t), - }, - CreateRepository{ - TransactionID: 2, - References: map[git.ReferenceName]git.ObjectID{ - "refs/heads/branch": setup.Commits.Third.OID, - }, - DefaultBranch: "refs/heads/branch", - Packs: [][]byte{ - setup.Commits.First.Pack, - setup.Commits.Second.Pack, - setup.Commits.Third.Pack, - }, - }, - Commit{ - TransactionID: 1, - }, - Commit{ - TransactionID: 2, - }, - }, - expectedState: StateAssertion{ - Database: DatabaseState{ - string(keyAppliedLSN(partitionID)): LSN(2).toProto(), - }, - Repositories: RepositoryStates{ - "repository-1": { - References: []git.Reference{ - {Name: "refs/heads/main", Target: setup.Commits.First.OID.String()}, - }, - Objects: []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - }, - CustomHooks: testhelper.DirectoryState{ - "/": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, - "/pre-receive": { - Mode: umask.Mask(fs.ModePerm), - Content: []byte("hook content"), - }, - "/private-dir": {Mode: umask.Mask(fs.ModeDir | perm.PrivateDir)}, - "/private-dir/private-file": {Mode: umask.Mask(perm.PrivateFile), Content: []byte("private content")}, - }, - }, - "repository-2": { - DefaultBranch: "refs/heads/branch", - References: []git.Reference{ - {Name: "refs/heads/branch", Target: setup.Commits.Third.OID.String()}, - }, - Objects: []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - setup.Commits.Second.OID, - setup.Commits.Third.OID, - }, - }, - }, - Directory: testhelper.DirectoryState{ - "/": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/1": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/1/objects.idx": indexFileDirectoryEntry(setup.Config), - "/wal/1/objects.pack": packFileDirectoryEntry( - setup.Config, - []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - }, - ), - "/wal/1/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), - "/wal/2": {Mode: fs.ModeDir | perm.PrivateDir}, - "/wal/2/objects.idx": indexFileDirectoryEntry(setup.Config), - "/wal/2/objects.pack": packFileDirectoryEntry( - setup.Config, - []git.ObjectID{ - setup.ObjectHash.EmptyTreeOID, - setup.Commits.First.OID, - setup.Commits.Second.OID, - setup.Commits.Third.OID, - }, - ), - "/wal/2/objects.rev": reverseIndexFileDirectoryEntry(setup.Config), - }, - }, - }, - { desc: "start transaction with empty relative path", steps: steps{ StartManager{}, @@ -4699,6 +3700,8 @@ func TestTransactionManager(t *testing.T) { subTests := [][]transactionTestCase{ generateInvalidReferencesTests(t, setup), generateModifyReferencesTests(t, setup), + generateCreateRepositoryTests(t, setup), + generateDeleteRepositoryTests(t, setup), } for _, subCases := range subTests { testCases = append(testCases, subCases...) |