diff options
author | Patrick Steinhardt <psteinhardt@gitlab.com> | 2021-12-13 10:43:15 +0300 |
---|---|---|
committer | Patrick Steinhardt <psteinhardt@gitlab.com> | 2021-12-13 10:43:15 +0300 |
commit | 3e8831389d6c15793b6f640af4572531af37ca53 (patch) | |
tree | e06302d78fc93f3065e50fbc2e7c4a1f34004089 | |
parent | 27dddad834d99e9901b4a9b137748b850e71849a (diff) | |
parent | cfd0fd048b46f4d783880f0774ffbdb7abcc8cb4 (diff) |
Merge branch 'pks-tx-voting-phases' into 'master'
proto: Introduce transactional voting phases
See merge request gitlab-org/gitaly!4180
33 files changed, 487 insertions, 299 deletions
diff --git a/internal/git/housekeeping/housekeeping_test.go b/internal/git/housekeeping/housekeeping_test.go index c70143080..2daae8b96 100644 --- a/internal/git/housekeeping/housekeeping_test.go +++ b/internal/git/housekeeping/housekeeping_test.go @@ -2,7 +2,6 @@ package housekeeping import ( "bytes" - "context" "fmt" "os" "path/filepath" @@ -20,7 +19,6 @@ import ( "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testcfg" "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo" - "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting" "google.golang.org/grpc/peer" ) @@ -706,13 +704,7 @@ func TestPerform_UnsetConfiguration_transactional(t *testing.T) { gittest.Exec(t, cfg, "-C", repoPath, "config", "http.some.extraHeader", "value") - votes := 0 - txManager := &transaction.MockManager{ - VoteFn: func(context.Context, txinfo.Transaction, voting.Vote) error { - votes++ - return nil - }, - } + txManager := transaction.NewTrackingManager() ctx, err := txinfo.InjectTransaction(ctx, 1, "node", true) require.NoError(t, err) @@ -721,8 +713,7 @@ func TestPerform_UnsetConfiguration_transactional(t *testing.T) { }) require.NoError(t, Perform(ctx, repo, txManager)) - - require.Equal(t, 2, votes) + require.Equal(t, 2, len(txManager.Votes())) configKeys := gittest.Exec(t, cfg, "-C", repoPath, "config", "--list", "--local", "--name-only") diff --git a/internal/git/localrepo/config_test.go b/internal/git/localrepo/config_test.go index e0d188cf4..2a31977a7 100644 --- a/internal/git/localrepo/config_test.go +++ b/internal/git/localrepo/config_test.go @@ -1,7 +1,6 @@ package localrepo import ( - "context" "fmt" "path/filepath" "runtime" @@ -18,7 +17,6 @@ import ( "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testcfg" "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo" - "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting" "google.golang.org/grpc/peer" ) @@ -121,17 +119,12 @@ func TestRepo_SetConfig(t *testing.T) { ctx, err := txinfo.InjectTransaction(ctx, 1, "node", true) ctx = peer.NewContext(ctx, backchannelPeer) - votes := 0 + txManager := transaction.NewTrackingManager() require.NoError(t, err) - require.NoError(t, repo.SetConfig(ctx, "some.key", "value", &transaction.MockManager{ - VoteFn: func(context.Context, txinfo.Transaction, voting.Vote) error { - votes++ - return nil - }, - })) + require.NoError(t, repo.SetConfig(ctx, "some.key", "value", txManager)) - require.Equal(t, 2, votes) + require.Equal(t, 2, len(txManager.Votes())) }) } @@ -262,16 +255,11 @@ func TestRepo_UnsetMatchingConfig(t *testing.T) { ctx, err := txinfo.InjectTransaction(ctx, 1, "node", true) ctx = peer.NewContext(ctx, backchannelPeer) - votes := 0 + txManager := transaction.NewTrackingManager() require.NoError(t, err) - require.NoError(t, repo.UnsetMatchingConfig(ctx, "some.key", &transaction.MockManager{ - VoteFn: func(context.Context, txinfo.Transaction, voting.Vote) error { - votes++ - return nil - }, - })) + require.NoError(t, repo.UnsetMatchingConfig(ctx, "some.key", txManager)) - require.Equal(t, 2, votes) + require.Equal(t, 2, len(txManager.Votes())) }) } diff --git a/internal/git/objectpool/link_test.go b/internal/git/objectpool/link_test.go index 3a45adb37..c960f48db 100644 --- a/internal/git/objectpool/link_test.go +++ b/internal/git/objectpool/link_test.go @@ -1,7 +1,6 @@ package objectpool import ( - "context" "os" "path/filepath" "strings" @@ -13,7 +12,6 @@ import ( "gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/transaction" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo" - "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting" "google.golang.org/grpc/peer" ) @@ -54,13 +52,8 @@ func TestLink_transactional(t *testing.T) { pool, poolMember := setupObjectPool(t) require.NoError(t, pool.Create(ctx, poolMember)) - votes := 0 - pool.txManager = &transaction.MockManager{ - VoteFn: func(context.Context, txinfo.Transaction, voting.Vote) error { - votes++ - return nil - }, - } + txManager := transaction.NewTrackingManager() + pool.txManager = txManager alternatesPath, err := pool.locator.InfoAlternatesPath(poolMember) require.NoError(t, err) @@ -74,7 +67,7 @@ func TestLink_transactional(t *testing.T) { require.NoError(t, pool.Link(ctx, poolMember)) - require.Equal(t, 2, votes) + require.Equal(t, 2, len(txManager.Votes())) } func TestLinkRemoveBitmap(t *testing.T) { diff --git a/internal/gitaly/hook/referencetransaction.go b/internal/gitaly/hook/referencetransaction.go index f391876e0..bba8eaabe 100644 --- a/internal/gitaly/hook/referencetransaction.go +++ b/internal/gitaly/hook/referencetransaction.go @@ -9,6 +9,7 @@ import ( "io" "gitlab.com/gitlab-org/gitaly/v14/internal/git" + "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting" ) // forceDeletionPrefix is the prefix of a queued reference transaction which deletes a @@ -27,13 +28,19 @@ func (m *GitLabHookManager) ReferenceTransactionHook(ctx context.Context, state return fmt.Errorf("reading stdin from request: %w", err) } + var phase voting.Phase + switch state { // We're voting in prepared state as this is the only stage in Git's reference transaction - // which allows us to abort the transaction. We're also voting in committed state to tell - // Praefect we've actually persisted the changes. This is necessary as some RPCs fail return - // errors in the response body rather than as an error code. Praefect can't tell if these RPCs - // have failed. Voting on committed ensure Praefect sees either a missing vote or that the RPC did - // commit the changes. - if state != ReferenceTransactionPrepared && state != ReferenceTransactionCommitted { + // which allows us to abort the transaction. + case ReferenceTransactionPrepared: + phase = voting.Prepared + // We're also voting in committed state to tell Praefect we've actually persisted the + // changes. This is necessary as some RPCs fail return errors in the response body rather + // than as an error code. Praefect can't tell if these RPCs have failed. Voting on committed + // ensure Praefect sees either a missing vote or that the RPC did commit the changes. + case ReferenceTransactionCommitted: + phase = voting.Committed + default: return nil } @@ -61,7 +68,7 @@ func (m *GitLabHookManager) ReferenceTransactionHook(ctx context.Context, state hash := sha1.Sum(changes) - if err := m.voteOnTransaction(ctx, hash, payload); err != nil { + if err := m.voteOnTransaction(ctx, hash, phase, payload); err != nil { return fmt.Errorf("error voting on transaction: %w", err) } diff --git a/internal/gitaly/hook/transactions.go b/internal/gitaly/hook/transactions.go index 78cd82c4c..33097b551 100644 --- a/internal/gitaly/hook/transactions.go +++ b/internal/gitaly/hook/transactions.go @@ -32,9 +32,14 @@ func (m *GitLabHookManager) runWithTransaction(ctx context.Context, payload git. return nil } -func (m *GitLabHookManager) voteOnTransaction(ctx context.Context, vote voting.Vote, payload git.HooksPayload) error { +func (m *GitLabHookManager) voteOnTransaction( + ctx context.Context, + vote voting.Vote, + phase voting.Phase, + payload git.HooksPayload, +) error { return m.runWithTransaction(ctx, payload, func(ctx context.Context, tx txinfo.Transaction) error { - return m.txManager.Vote(ctx, tx, vote) + return m.txManager.Vote(ctx, tx, vote, phase) }) } diff --git a/internal/gitaly/hook/transactions_test.go b/internal/gitaly/hook/transactions_test.go index 0f1759b95..de9f7d7cb 100644 --- a/internal/gitaly/hook/transactions_test.go +++ b/internal/gitaly/hook/transactions_test.go @@ -124,7 +124,7 @@ func TestHookManager_contextCancellationCancelsVote(t *testing.T) { cfg, repo, _ := testcfg.BuildWithRepo(t) mockTxMgr := transaction.MockManager{ - VoteFn: func(ctx context.Context, tx txinfo.Transaction, vote voting.Vote) error { + VoteFn: func(ctx context.Context, _ txinfo.Transaction, _ voting.Vote, _ voting.Phase) error { <-ctx.Done() return fmt.Errorf("mock error: %s", ctx.Err()) }, diff --git a/internal/gitaly/service/operations/apply_patch_test.go b/internal/gitaly/service/operations/apply_patch_test.go index 89b50f4f0..b0676804d 100644 --- a/internal/gitaly/service/operations/apply_patch_test.go +++ b/internal/gitaly/service/operations/apply_patch_test.go @@ -1,12 +1,10 @@ package operations import ( - "context" "fmt" "io" "os" "strings" - "sync/atomic" "testing" "testing/iotest" "time" @@ -26,7 +24,6 @@ import ( "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testcfg" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testserver" "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo" - "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting" "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" "gitlab.com/gitlab-org/gitaly/v14/streamio" "google.golang.org/grpc/codes" @@ -621,16 +618,7 @@ func TestUserApplyPatchTransactional(t *testing.T) { ctx, cancel := testhelper.Context() defer cancel() - var votes int32 - txManager := &transaction.MockManager{ - VoteFn: func(context.Context, txinfo.Transaction, voting.Vote) error { - // We can see a race here between adding the worktree and changing its - // config and updating refs via the reference-transaction hook. Given that - // both come in via different threads, Go perceives it as a potential race. - atomic.AddInt32(&votes, 1) - return nil - }, - } + txManager := transaction.NewTrackingManager() ctx, cfg, repoProto, repoPath, client := setupOperationsService(t, ctx, testserver.WithTransactionManager(txManager)) @@ -665,7 +653,7 @@ func TestUserApplyPatchTransactional(t *testing.T) { require.True(t, response.BranchUpdate.BranchCreated) - require.Equal(t, int32(14), votes) + require.Equal(t, 14, len(txManager.Votes())) splitIndex := gittest.Exec(t, cfg, "-C", repoPath, "config", "core.splitIndex") require.Equal(t, "false", text.ChompBytes(splitIndex)) diff --git a/internal/gitaly/service/operations/rebase_test.go b/internal/gitaly/service/operations/rebase_test.go index 555c7048c..5c05880b3 100644 --- a/internal/gitaly/service/operations/rebase_test.go +++ b/internal/gitaly/service/operations/rebase_test.go @@ -1,7 +1,6 @@ package operations import ( - "context" "fmt" "io" "strings" @@ -18,7 +17,6 @@ import ( "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testserver" "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo" - "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting" "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" @@ -93,13 +91,7 @@ func TestUserRebaseConfirmableTransaction(t *testing.T) { ctx, cancel := testhelper.Context() defer cancel() - var voteCount int - txManager := &transaction.MockManager{ - VoteFn: func(context.Context, txinfo.Transaction, voting.Vote) error { - voteCount++ - return nil - }, - } + txManager := transaction.NewTrackingManager() ctx, cfg, repoProto, repoPath, client := setupOperationsService( t, ctx, @@ -141,7 +133,7 @@ func TestUserRebaseConfirmableTransaction(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { preReceiveHookOutputPath := gittest.WriteEnvToCustomHook(t, repoPath, "pre-receive") - voteCount = 0 + txManager.Reset() ctx := ctx if tc.withTransaction { @@ -173,7 +165,7 @@ func TestUserRebaseConfirmableTransaction(t *testing.T) { require.Nil(t, response) require.Equal(t, io.EOF, err) - require.Equal(t, tc.expectedVotes, voteCount) + require.Equal(t, tc.expectedVotes, len(txManager.Votes())) if tc.expectPreReceiveHook { require.FileExists(t, preReceiveHookOutputPath) } else { diff --git a/internal/gitaly/service/ref/delete_refs.go b/internal/gitaly/service/ref/delete_refs.go index 26e6d2929..222432ed0 100644 --- a/internal/gitaly/service/ref/delete_refs.go +++ b/internal/gitaly/service/ref/delete_refs.go @@ -60,11 +60,18 @@ func (s *server) DeleteRefs(ctx context.Context, in *gitalypb.DeleteRefsRequest) return nil, helper.ErrInternalf("could not compute vote: %v", err) } + // We don't use proper two-phase voting when the feature flag is disabled, so we use + // `UnknownPhase` in that case. + preparedPhase := voting.UnknownPhase + if featureflag.TxTwoPhaseDeleteRefs.IsEnabled(ctx) { + preparedPhase = voting.Prepared + } + // All deletes we're doing in this RPC are force deletions. Because we're required to filter // out transactions which only consist of force deletions, we never do any voting via the // reference-transaction hook here. Instead, we need to resort to a manual vote which is // simply the concatenation of all reference we're about to delete. - if err := transaction.VoteOnContext(ctx, s.txManager, vote); err != nil { + if err := transaction.VoteOnContext(ctx, s.txManager, vote, preparedPhase); err != nil { return nil, helper.ErrInternalf("preparatory vote: %w", err) } @@ -77,7 +84,7 @@ func (s *server) DeleteRefs(ctx context.Context, in *gitalypb.DeleteRefsRequest) } if featureflag.TxTwoPhaseDeleteRefs.IsEnabled(ctx) { - if err := transaction.VoteOnContext(ctx, s.txManager, vote); err != nil { + if err := transaction.VoteOnContext(ctx, s.txManager, vote, voting.Committed); err != nil { return nil, helper.ErrInternalf("committing vote: %w", err) } } diff --git a/internal/gitaly/service/ref/delete_refs_test.go b/internal/gitaly/service/ref/delete_refs_test.go index b52c3f74c..b508e4308 100644 --- a/internal/gitaly/service/ref/delete_refs_test.go +++ b/internal/gitaly/service/ref/delete_refs_test.go @@ -17,7 +17,6 @@ import ( "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testcfg" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testserver" "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo" - "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting" "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -92,13 +91,7 @@ func testDeleteRefsTransaction(t *testing.T, ctx context.Context) { testcfg.BuildGitalyHooks(t, cfg) - var votes int - txManager := &transaction.MockManager{ - VoteFn: func(context.Context, txinfo.Transaction, voting.Vote) error { - votes++ - return nil - }, - } + txManager := transaction.NewTrackingManager() addr := testserver.RunGitalyServer(t, cfg, nil, func(srv *grpc.Server, deps *service.Dependencies) { gitalypb.RegisterRefServiceServer(srv, NewServer( @@ -139,7 +132,7 @@ func testDeleteRefsTransaction(t *testing.T, ctx context.Context) { }, } { t.Run(tc.desc, func(t *testing.T) { - votes = 0 + txManager.Reset() repo, _ := gittest.CloneRepo(t, cfg, cfg.Storages[0]) tc.request.Repository = repo @@ -153,7 +146,7 @@ func testDeleteRefsTransaction(t *testing.T, ctx context.Context) { expectedVotes *= 2 } - require.Equal(t, expectedVotes, votes) + require.Equal(t, expectedVotes, len(txManager.Votes())) }) } } diff --git a/internal/gitaly/service/repository/apply_gitattributes.go b/internal/gitaly/service/repository/apply_gitattributes.go index 82a08d1a1..8f188183a 100644 --- a/internal/gitaly/service/repository/apply_gitattributes.go +++ b/internal/gitaly/service/repository/apply_gitattributes.go @@ -66,7 +66,7 @@ func (s *server) applyGitattributes(ctx context.Context, repo *localrepo.Repo, o // We use the zero OID as placeholder to vote on removal of the // gitattributes file. - if err := s.vote(ctx, git.ZeroOID); err != nil { + if err := s.vote(ctx, git.ZeroOID, voting.Prepared); err != nil { return fmt.Errorf("preimage vote: %w", err) } @@ -74,7 +74,7 @@ func (s *server) applyGitattributes(ctx context.Context, repo *localrepo.Repo, o return err } - if err := s.vote(ctx, git.ZeroOID); err != nil { + if err := s.vote(ctx, git.ZeroOID, voting.Committed); err != nil { return fmt.Errorf("postimage vote: %w", err) } @@ -83,7 +83,7 @@ func (s *server) applyGitattributes(ctx context.Context, repo *localrepo.Repo, o // If there is no gitattributes file, we simply use the ZeroOID // as a placeholder to vote on the removal. - if err := s.vote(ctx, git.ZeroOID); err != nil { + if err := s.vote(ctx, git.ZeroOID, voting.UnknownPhase); err != nil { return fmt.Errorf("could not remove gitattributes: %w", err) } @@ -114,7 +114,7 @@ func (s *server) applyGitattributes(ctx context.Context, repo *localrepo.Repo, o return nil } -func (s *server) vote(ctx context.Context, oid git.ObjectID) error { +func (s *server) vote(ctx context.Context, oid git.ObjectID, phase voting.Phase) error { tx, err := txinfo.TransactionFromContext(ctx) if errors.Is(err, txinfo.ErrTransactionNotFound) { return nil @@ -130,7 +130,7 @@ func (s *server) vote(ctx context.Context, oid git.ObjectID) error { return fmt.Errorf("cannot convert OID to vote: %w", err) } - if err := s.txManager.Vote(ctx, tx, vote); err != nil { + if err := s.txManager.Vote(ctx, tx, vote, phase); err != nil { return fmt.Errorf("vote failed: %w", err) } diff --git a/internal/gitaly/service/repository/create_repository.go b/internal/gitaly/service/repository/create_repository.go index 334d8b79b..7d3243eb9 100644 --- a/internal/gitaly/service/repository/create_repository.go +++ b/internal/gitaly/service/repository/create_repository.go @@ -84,7 +84,7 @@ func (s *server) CreateRepository(ctx context.Context, req *gitalypb.CreateRepos return err } - if err := s.txManager.Vote(ctx, tx, vote); err != nil { + if err := s.txManager.Vote(ctx, tx, vote, voting.UnknownPhase); err != nil { return fmt.Errorf("casting vote: %w", err) } diff --git a/internal/gitaly/service/repository/create_repository_from_bundle_test.go b/internal/gitaly/service/repository/create_repository_from_bundle_test.go index 90ab876d9..668b135f5 100644 --- a/internal/gitaly/service/repository/create_repository_from_bundle_test.go +++ b/internal/gitaly/service/repository/create_repository_from_bundle_test.go @@ -104,16 +104,9 @@ func TestCreateRepositoryFromBundle_transactional(t *testing.T) { } func testCreateRepositoryFromBundleTransactional(t *testing.T, ctx context.Context) { - var votes []voting.Vote - txManager := &transaction.MockManager{ - VoteFn: func(ctx context.Context, tx txinfo.Transaction, vote voting.Vote) error { - votes = append(votes, vote) - return nil - }, - } + txManager := transaction.NewTrackingManager() - cfg, repoProto, repoPath, client := setupRepositoryService(t, - testserver.WithTransactionManager(txManager)) + cfg, repoProto, repoPath, client := setupRepositoryService(t, testserver.WithTransactionManager(txManager)) masterOID := text.ChompBytes(gittest.Exec(t, cfg, "-C", repoPath, "rev-parse", "refs/heads/master")) featureOID := text.ChompBytes(gittest.Exec(t, cfg, "-C", repoPath, "rev-parse", "refs/heads/feature")) @@ -155,50 +148,43 @@ func testCreateRepositoryFromBundleTransactional(t *testing.T, ctx context.Conte require.NoError(t, err) if featureflag.TxAtomicRepositoryCreation.IsEnabled(ctx) { - var actualVotes []string - for _, vote := range votes { - actualVotes = append(actualVotes, vote.String()) + createVote := func(hash string, phase voting.Phase) transaction.PhasedVote { + vote, err := voting.VoteFromString(hash) + require.NoError(t, err) + return transaction.PhasedVote{Vote: vote, Phase: phase} } // While the following votes are opaque to us, this doesn't really matter. All we do // care about is that they're stable. - require.Equal(t, []string{ + require.Equal(t, []transaction.PhasedVote{ // These are the votes created by git-fetch(1). - "47553c06f575f757ad56ef3216c59804b72aa4a6", - "47553c06f575f757ad56ef3216c59804b72aa4a6", + createVote("47553c06f575f757ad56ef3216c59804b72aa4a6", voting.Prepared), + createVote("47553c06f575f757ad56ef3216c59804b72aa4a6", voting.Committed), // And this is the manual votes we compute by walking the repository. - "da39a3ee5e6b4b0d3255bfef95601890afd80709", - "da39a3ee5e6b4b0d3255bfef95601890afd80709", - }, actualVotes) + createVote("da39a3ee5e6b4b0d3255bfef95601890afd80709", voting.Prepared), + createVote("da39a3ee5e6b4b0d3255bfef95601890afd80709", voting.Committed), + }, txManager.Votes()) return } - var votingInput []string - // This accounts for the first two votes via git-clone(1). Given that git-clone(1) creates // all references in a single reference transaction, the hook is executed twice for all // fetched references (once for "prepare", once for "commit"). - votingInput = append(votingInput, - fmt.Sprintf("%s %s refs/heads/feature\n%s %s refs/heads/master\n", git.ZeroOID, featureOID, git.ZeroOID, masterOID), - fmt.Sprintf("%s %s refs/heads/feature\n%s %s refs/heads/master\n", git.ZeroOID, featureOID, git.ZeroOID, masterOID), - ) + voteFromCloning := []byte(fmt.Sprintf("%s %s refs/heads/feature\n%s %s refs/heads/master\n", git.ZeroOID, featureOID, git.ZeroOID, masterOID)) // Keep-around references are not fetched via git-clone(1) because non-mirror clones only // fetch branches and tags. These additional references are thus obtained via git-fetch(1). // Given that we use the `--atomic` flag for git-fetch(1), all reference updates will be in // a single transaction and we thus expect exactly two votes (once for "prepare", once for // "commit"). - votingInput = append(votingInput, - fmt.Sprintf("%s %s refs/keep-around/2\n%s %s refs/keep-around/1\n", git.ZeroOID, masterOID, git.ZeroOID, masterOID), - fmt.Sprintf("%s %s refs/keep-around/2\n%s %s refs/keep-around/1\n", git.ZeroOID, masterOID, git.ZeroOID, masterOID), - ) - - var expectedVotes []voting.Vote - for _, expectedVote := range votingInput { - expectedVotes = append(expectedVotes, voting.VoteFromData([]byte(expectedVote))) - } - - require.Equal(t, votes, expectedVotes) + voteFromFetching := []byte(fmt.Sprintf("%s %s refs/keep-around/2\n%s %s refs/keep-around/1\n", git.ZeroOID, masterOID, git.ZeroOID, masterOID)) + + require.Equal(t, []transaction.PhasedVote{ + {Vote: voting.VoteFromData(voteFromCloning), Phase: voting.Prepared}, + {Vote: voting.VoteFromData(voteFromCloning), Phase: voting.Committed}, + {Vote: voting.VoteFromData(voteFromFetching), Phase: voting.Prepared}, + {Vote: voting.VoteFromData(voteFromFetching), Phase: voting.Committed}, + }, txManager.Votes()) } func TestCreateRepositoryFromBundle_invalidBundle(t *testing.T) { diff --git a/internal/gitaly/service/repository/create_repository_test.go b/internal/gitaly/service/repository/create_repository_test.go index 813259a4f..6e2a6df7c 100644 --- a/internal/gitaly/service/repository/create_repository_test.go +++ b/internal/gitaly/service/repository/create_repository_test.go @@ -150,26 +150,16 @@ func TestCreateRepository_transactional(t *testing.T) { } func testCreateRepositoryTransactional(t *testing.T, ctx context.Context) { - var actualVote voting.Vote - var called int - - mockTxManager := transaction.MockManager{ - VoteFn: func(ctx context.Context, tx txinfo.Transaction, v voting.Vote) error { - actualVote = v - called++ - return nil - }, - } + txManager := transaction.NewTrackingManager() - cfg, client := setupRepositoryServiceWithoutRepo(t, testserver.WithTransactionManager(&mockTxManager)) + cfg, client := setupRepositoryServiceWithoutRepo(t, testserver.WithTransactionManager(txManager)) ctx, err := txinfo.InjectTransaction(ctx, 1, "node", true) require.NoError(t, err) ctx = metadata.IncomingToOutgoing(ctx) t.Run("initial creation without refs", func(t *testing.T) { - called = 0 - actualVote = voting.Vote{} + txManager.Reset() repo := &gitalypb.Repository{ StorageName: cfg.Storages[0].Name, @@ -180,16 +170,16 @@ func testCreateRepositoryTransactional(t *testing.T, ctx context.Context) { require.DirExists(t, filepath.Join(cfg.Storages[0].Path, getReplicaPath(ctx, t, client, repo))) if featureflag.TxAtomicRepositoryCreation.IsEnabled(ctx) { - require.Equal(t, 2, called, "expected transactional vote") + require.Equal(t, 2, len(txManager.Votes()), "expected transactional vote") } else { - require.Equal(t, 1, called, "expected transactional vote") - require.Equal(t, voting.VoteFromData([]byte{}), actualVote) + require.Equal(t, []transaction.PhasedVote{ + {Vote: voting.VoteFromData([]byte{}), Phase: voting.UnknownPhase}, + }, txManager.Votes()) } }) t.Run("idempotent creation with preexisting refs", func(t *testing.T) { - called = 0 - actualVote = voting.Vote{} + txManager.Reset() // The above test creates the second repository on the server. As this test can run with Praefect in front of it, // we'll use the next replica path Praefect will assign in order to ensure this repository creation conflicts even @@ -212,8 +202,9 @@ func testCreateRepositoryTransactional(t *testing.T, ctx context.Context) { refs := gittest.Exec(t, cfg, "-C", repoPath, "for-each-ref") require.NotEmpty(t, refs) - require.Equal(t, 1, called, "expected transactional vote") - require.Equal(t, voting.VoteFromData(refs), actualVote) + require.Equal(t, []transaction.PhasedVote{ + {Vote: voting.VoteFromData(refs), Phase: voting.UnknownPhase}, + }, txManager.Votes()) }) } diff --git a/internal/gitaly/service/repository/fetch_remote.go b/internal/gitaly/service/repository/fetch_remote.go index 81845a363..f7849d838 100644 --- a/internal/gitaly/service/repository/fetch_remote.go +++ b/internal/gitaly/service/repository/fetch_remote.go @@ -114,7 +114,7 @@ func (s *server) FetchRemote(ctx context.Context, req *gitalypb.FetchRemoteReque return err } - return s.txManager.Vote(ctx, tx, vote) + return s.txManager.Vote(ctx, tx, vote, voting.UnknownPhase) }); err != nil { return nil, status.Errorf(codes.Aborted, "failed vote on refs: %v", err) } diff --git a/internal/gitaly/service/repository/fetch_remote_test.go b/internal/gitaly/service/repository/fetch_remote_test.go index 56355d45e..8f86bcac7 100644 --- a/internal/gitaly/service/repository/fetch_remote_test.go +++ b/internal/gitaly/service/repository/fetch_remote_test.go @@ -1,7 +1,6 @@ package repository import ( - "context" "fmt" "net/http" "net/http/httptest" @@ -24,7 +23,6 @@ import ( "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testcfg" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testserver" "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo" - "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting" "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -184,21 +182,11 @@ func TestFetchRemote_withDefaultRefmaps(t *testing.T) { require.Equal(t, sourceRefs, targetRefs) } -type mockTxManager struct { - transaction.Manager - votes int -} - -func (m *mockTxManager) Vote(context.Context, txinfo.Transaction, voting.Vote) error { - m.votes++ - return nil -} - func TestFetchRemote_transaction(t *testing.T) { t.Parallel() sourceCfg, _, sourceRepoPath := testcfg.BuildWithRepo(t) - txManager := &mockTxManager{} + txManager := transaction.NewTrackingManager() addr := testserver.RunGitalyServer(t, sourceCfg, nil, func(srv *grpc.Server, deps *service.Dependencies) { gitalypb.RegisterRepositoryServiceServer(srv, NewServer( deps.GetCfg(), @@ -223,7 +211,7 @@ func TestFetchRemote_transaction(t *testing.T) { require.NoError(t, err) ctx = metadata.IncomingToOutgoing(ctx) - require.Equal(t, 0, txManager.votes) + require.Equal(t, 0, len(txManager.Votes())) _, err = client.FetchRemote(ctx, &gitalypb.FetchRemoteRequest{ Repository: targetRepoProto, @@ -233,7 +221,7 @@ func TestFetchRemote_transaction(t *testing.T) { }) require.NoError(t, err) - require.Equal(t, 1, txManager.votes) + require.Equal(t, 1, len(txManager.Votes())) } func TestFetchRemote_prune(t *testing.T) { diff --git a/internal/gitaly/service/repository/midx_test.go b/internal/gitaly/service/repository/midx_test.go index 03db17ff1..b160f02d1 100644 --- a/internal/gitaly/service/repository/midx_test.go +++ b/internal/gitaly/service/repository/midx_test.go @@ -20,7 +20,6 @@ import ( "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testserver" "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo" - "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting" "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" "google.golang.org/grpc/peer" ) @@ -118,13 +117,7 @@ func TestMidxRepack_transactional(t *testing.T) { ctx, cancel := testhelper.Context() defer cancel() - votes := 0 - txManager := &transaction.MockManager{ - VoteFn: func(context.Context, txinfo.Transaction, voting.Vote) error { - votes++ - return nil - }, - } + txManager := transaction.NewTrackingManager() cfg, repo, repoPath, client := setupRepositoryService(t, testserver.WithTransactionManager(txManager)) @@ -140,7 +133,7 @@ func TestMidxRepack_transactional(t *testing.T) { }) require.NoError(t, err) - require.Equal(t, 2, votes) + require.Equal(t, 2, len(txManager.Votes())) multiPackIndex := gittest.Exec(t, cfg, "-C", repoPath, "config", "core.multiPackIndex") require.Equal(t, "true", text.ChompBytes(multiPackIndex)) diff --git a/internal/gitaly/service/repository/remove.go b/internal/gitaly/service/repository/remove.go index c7befa398..e8a00efd7 100644 --- a/internal/gitaly/service/repository/remove.go +++ b/internal/gitaly/service/repository/remove.go @@ -84,7 +84,7 @@ func (s *server) RemoveRepository(ctx context.Context, in *gitalypb.RemoveReposi return nil, helper.ErrInternalf("re-statting repository: %w", err) } - if err := s.voteOnAction(ctx, repo, preRemove); err != nil { + if err := s.voteOnAction(ctx, repo, voting.Prepared); err != nil { return nil, helper.ErrInternalf("vote on rename: %v", err) } @@ -99,11 +99,11 @@ func (s *server) RemoveRepository(ctx context.Context, in *gitalypb.RemoveReposi return nil, helper.ErrInternalf("removing repository: %w", err) } - if err := s.voteOnAction(ctx, repo, postRemove); err != nil { + if err := s.voteOnAction(ctx, repo, voting.Committed); err != nil { return nil, helper.ErrInternalf("vote on finalizing: %v", err) } } else { - if err := s.voteOnAction(ctx, repo, preRemove); err != nil { + if err := s.voteOnAction(ctx, repo, voting.Prepared); err != nil { return nil, helper.ErrInternalf("vote on rename: %v", err) } @@ -121,7 +121,7 @@ func (s *server) RemoveRepository(ctx context.Context, in *gitalypb.RemoveReposi return nil, helper.ErrInternal(err) } - if err := s.voteOnAction(ctx, repo, postRemove); err != nil { + if err := s.voteOnAction(ctx, repo, voting.Committed); err != nil { return nil, helper.ErrInternalf("vote on finalizing: %v", err) } } @@ -129,26 +129,19 @@ func (s *server) RemoveRepository(ctx context.Context, in *gitalypb.RemoveReposi return &gitalypb.RemoveRepositoryResponse{}, nil } -type removalStep int - -const ( - preRemove = removalStep(iota) - postRemove -) - -func (s *server) voteOnAction(ctx context.Context, repo *gitalypb.Repository, step removalStep) error { +func (s *server) voteOnAction(ctx context.Context, repo *gitalypb.Repository, phase voting.Phase) error { return transaction.RunOnContext(ctx, func(tx txinfo.Transaction) error { var voteStep string - switch step { - case preRemove: + switch phase { + case voting.Prepared: voteStep = "pre-remove" - case postRemove: + case voting.Committed: voteStep = "post-remove" default: - return fmt.Errorf("invalid removal step: %d", step) + return fmt.Errorf("invalid removal step: %d", phase) } vote := fmt.Sprintf("%s %s", voteStep, repo.GetRelativePath()) - return s.txManager.Vote(ctx, tx, voting.VoteFromData([]byte(vote))) + return s.txManager.Vote(ctx, tx, voting.VoteFromData([]byte(vote)), phase) }) } diff --git a/internal/gitaly/service/repository/util.go b/internal/gitaly/service/repository/util.go index 50a1182b3..dda214eb1 100644 --- a/internal/gitaly/service/repository/util.go +++ b/internal/gitaly/service/repository/util.go @@ -177,7 +177,7 @@ func (s *server) createRepository( return helper.ErrAlreadyExistsf("repository exists already") } - if err := transaction.VoteOnContext(ctx, s.txManager, vote); err != nil { + if err := transaction.VoteOnContext(ctx, s.txManager, vote, voting.Prepared); err != nil { return helper.ErrFailedPreconditionf("preparatory vote: %w", err) } @@ -187,7 +187,7 @@ func (s *server) createRepository( return fmt.Errorf("moving repository into place: %w", err) } - if err := transaction.VoteOnContext(ctx, s.txManager, vote); err != nil { + if err := transaction.VoteOnContext(ctx, s.txManager, vote, voting.Committed); err != nil { return helper.ErrFailedPreconditionf("committing vote: %w", err) } diff --git a/internal/gitaly/service/repository/util_test.go b/internal/gitaly/service/repository/util_test.go index 2e4602a63..48ccebd15 100644 --- a/internal/gitaly/service/repository/util_test.go +++ b/internal/gitaly/service/repository/util_test.go @@ -44,7 +44,7 @@ func TestCreateRepository(t *testing.T) { gitCmdFactory: gitCmdFactory, } - votes := 0 + var votesByPhase map[voting.Phase]int for _, tc := range []struct { desc string @@ -136,21 +136,24 @@ func TestCreateRepository(t *testing.T) { desc: "successful transaction", transactional: true, setup: func(t *testing.T, repo *gitalypb.Repository, repoPath string) { - votes = 0 - txManager.VoteFn = func(context.Context, txinfo.Transaction, voting.Vote) error { - votes++ + votesByPhase = map[voting.Phase]int{} + txManager.VoteFn = func(_ context.Context, _ txinfo.Transaction, _ voting.Vote, phase voting.Phase) error { + votesByPhase[phase]++ return nil } }, verify: func(t *testing.T, tempRepo *gitalypb.Repository, tempRepoPath string, realRepo *gitalypb.Repository, realRepoPath string) { - require.Equal(t, 2, votes) + require.Equal(t, map[voting.Phase]int{ + voting.Prepared: 1, + voting.Committed: 1, + }, votesByPhase) }, }, { desc: "failing preparatory vote", transactional: true, setup: func(t *testing.T, repo *gitalypb.Repository, repoPath string) { - txManager.VoteFn = func(context.Context, txinfo.Transaction, voting.Vote) error { + txManager.VoteFn = func(context.Context, txinfo.Transaction, voting.Vote, voting.Phase) error { return errors.New("vote failed") } }, @@ -164,10 +167,8 @@ func TestCreateRepository(t *testing.T) { desc: "failing post-commit vote", transactional: true, setup: func(t *testing.T, repo *gitalypb.Repository, repoPath string) { - votes = 0 - txManager.VoteFn = func(context.Context, txinfo.Transaction, voting.Vote) error { - votes++ - if votes == 1 { + txManager.VoteFn = func(_ context.Context, _ txinfo.Transaction, _ voting.Vote, phase voting.Phase) error { + if phase == voting.Prepared { return nil } return errors.New("vote failed") @@ -196,7 +197,7 @@ func TestCreateRepository(t *testing.T) { require.NoError(t, err) require.NoError(t, lock.Close()) - txManager.VoteFn = func(context.Context, txinfo.Transaction, voting.Vote) error { + txManager.VoteFn = func(context.Context, txinfo.Transaction, voting.Vote, voting.Phase) error { require.FailNow(t, "no votes should have happened") return nil } diff --git a/internal/gitaly/service/ssh/receive_pack.go b/internal/gitaly/service/ssh/receive_pack.go index 4e82eddc4..80c359e03 100644 --- a/internal/gitaly/service/ssh/receive_pack.go +++ b/internal/gitaly/service/ssh/receive_pack.go @@ -100,7 +100,7 @@ func (s *server) sshReceivePack(stream gitalypb.SSHService_SSHReceivePackServer, // ensure there's always at least one vote. In case there was diverging behaviour in // git-receive-pack(1) which led to a different outcome across voters, then this final vote // would fail because the sequence of votes would be different. - if err := transaction.VoteOnContext(ctx, s.txManager, voting.Vote{}); err != nil { + if err := transaction.VoteOnContext(ctx, s.txManager, voting.Vote{}, voting.Committed); err != nil { return status.Errorf(codes.Aborted, "final transactional vote: %v", err) } diff --git a/internal/gitaly/service/ssh/receive_pack_test.go b/internal/gitaly/service/ssh/receive_pack_test.go index 6c27ae7d8..56d9ac5c3 100644 --- a/internal/gitaly/service/ssh/receive_pack_test.go +++ b/internal/gitaly/service/ssh/receive_pack_test.go @@ -2,7 +2,6 @@ package ssh import ( "bytes" - "context" "fmt" "io" "os" @@ -29,7 +28,6 @@ import ( "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testcfg" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testserver" "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo" - "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting" "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" "gitlab.com/gitlab-org/gitaly/v14/streamio" "google.golang.org/grpc/codes" @@ -282,15 +280,9 @@ func TestReceivePackTransactional(t *testing.T) { testcfg.BuildGitalyHooks(t, cfg) - var votes int - serverSocketPath := runSSHServer(t, cfg, testserver.WithTransactionManager( - &transaction.MockManager{ - VoteFn: func(context.Context, txinfo.Transaction, voting.Vote) error { - votes++ - return nil - }, - }, - )) + txManager := transaction.NewTrackingManager() + + serverSocketPath := runSSHServer(t, cfg, testserver.WithTransactionManager(txManager)) client, conn := newSSHClient(t, serverSocketPath) defer conn.Close() @@ -435,7 +427,7 @@ func TestReceivePackTransactional(t *testing.T) { }, } { t.Run(tc.desc, func(t *testing.T) { - votes = 0 + txManager.Reset() var request bytes.Buffer for i, command := range tc.commands { @@ -480,7 +472,7 @@ func TestReceivePackTransactional(t *testing.T) { require.Equal(t, expectedOID, actualOID.String()) } } - require.Equal(t, tc.expectedVotes, votes) + require.Equal(t, tc.expectedVotes, len(txManager.Votes())) }) } } diff --git a/internal/gitaly/transaction/manager.go b/internal/gitaly/transaction/manager.go index e53088115..d7e2a0993 100644 --- a/internal/gitaly/transaction/manager.go +++ b/internal/gitaly/transaction/manager.go @@ -47,7 +47,7 @@ var ( type Manager interface { // Vote casts a vote on the given transaction which is hosted by the // given Praefect server. - Vote(context.Context, txinfo.Transaction, voting.Vote) error + Vote(context.Context, txinfo.Transaction, voting.Vote, voting.Phase) error // Stop gracefully stops the given transaction which is hosted by the // given Praefect server. @@ -97,7 +97,12 @@ func (m *PoolManager) getTransactionClient(ctx context.Context, tx txinfo.Transa } // Vote connects to the given server and casts vote as a vote for the transaction identified by tx. -func (m *PoolManager) Vote(ctx context.Context, tx txinfo.Transaction, vote voting.Vote) error { +func (m *PoolManager) Vote( + ctx context.Context, + tx txinfo.Transaction, + vote voting.Vote, + phase voting.Phase, +) error { client, err := m.getTransactionClient(ctx, tx) if err != nil { return err @@ -118,6 +123,7 @@ func (m *PoolManager) Vote(ctx context.Context, tx txinfo.Transaction, vote voti TransactionId: tx.ID, Node: tx.Node, ReferenceUpdatesHash: vote.Bytes(), + Phase: phase.ToProto(), }) if err != nil { // Add some additional context to cancellation errors so that diff --git a/internal/gitaly/transaction/manager_test.go b/internal/gitaly/transaction/manager_test.go index 6239f0ab2..eb67aa769 100644 --- a/internal/gitaly/transaction/manager_test.go +++ b/internal/gitaly/transaction/manager_test.go @@ -63,21 +63,64 @@ func TestPoolManager_Vote(t *testing.T) { desc string transaction txinfo.Transaction vote voting.Vote + phase voting.Phase voteFn func(*testing.T, *gitalypb.VoteTransactionRequest) (*gitalypb.VoteTransactionResponse, error) expectedErr error }{ { - desc: "successful vote", + desc: "successful unknown vote", transaction: txinfo.Transaction{ BackchannelID: backchannelID, ID: 1, Node: "node", }, - vote: voting.VoteFromData([]byte("foobar")), + vote: voting.VoteFromData([]byte("foobar")), + phase: voting.UnknownPhase, voteFn: func(t *testing.T, request *gitalypb.VoteTransactionRequest) (*gitalypb.VoteTransactionResponse, error) { require.Equal(t, uint64(1), request.TransactionId) require.Equal(t, "node", request.Node) require.Equal(t, request.ReferenceUpdatesHash, voting.VoteFromData([]byte("foobar")).Bytes()) + require.Equal(t, gitalypb.VoteTransactionRequest_UNKNOWN_PHASE, request.Phase) + + return &gitalypb.VoteTransactionResponse{ + State: gitalypb.VoteTransactionResponse_COMMIT, + }, nil + }, + }, + { + desc: "successful prepared vote", + transaction: txinfo.Transaction{ + BackchannelID: backchannelID, + ID: 1, + Node: "node", + }, + vote: voting.VoteFromData([]byte("foobar")), + phase: voting.Prepared, + voteFn: func(t *testing.T, request *gitalypb.VoteTransactionRequest) (*gitalypb.VoteTransactionResponse, error) { + require.Equal(t, uint64(1), request.TransactionId) + require.Equal(t, "node", request.Node) + require.Equal(t, request.ReferenceUpdatesHash, voting.VoteFromData([]byte("foobar")).Bytes()) + require.Equal(t, gitalypb.VoteTransactionRequest_PREPARED_PHASE, request.Phase) + + return &gitalypb.VoteTransactionResponse{ + State: gitalypb.VoteTransactionResponse_COMMIT, + }, nil + }, + }, + { + desc: "successful committed vote", + transaction: txinfo.Transaction{ + BackchannelID: backchannelID, + ID: 1, + Node: "node", + }, + vote: voting.VoteFromData([]byte("foobar")), + phase: voting.Committed, + voteFn: func(t *testing.T, request *gitalypb.VoteTransactionRequest) (*gitalypb.VoteTransactionResponse, error) { + require.Equal(t, uint64(1), request.TransactionId) + require.Equal(t, "node", request.Node) + require.Equal(t, request.ReferenceUpdatesHash, voting.VoteFromData([]byte("foobar")).Bytes()) + require.Equal(t, gitalypb.VoteTransactionRequest_COMMITTED_PHASE, request.Phase) return &gitalypb.VoteTransactionResponse{ State: gitalypb.VoteTransactionResponse_COMMIT, @@ -124,7 +167,7 @@ func TestPoolManager_Vote(t *testing.T) { return tc.voteFn(t, request) } - err := manager.Vote(ctx, tc.transaction, tc.vote) + err := manager.Vote(ctx, tc.transaction, tc.vote, tc.phase) testhelper.RequireGrpcError(t, tc.expectedErr, err) }) } diff --git a/internal/gitaly/transaction/mock.go b/internal/gitaly/transaction/mock.go index 101d4b291..10ad6efdf 100644 --- a/internal/gitaly/transaction/mock.go +++ b/internal/gitaly/transaction/mock.go @@ -3,6 +3,7 @@ package transaction import ( "context" "errors" + "sync" "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo" "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/voting" @@ -10,16 +11,16 @@ import ( // MockManager is a mock Manager for use in tests. type MockManager struct { - VoteFn func(context.Context, txinfo.Transaction, voting.Vote) error + VoteFn func(context.Context, txinfo.Transaction, voting.Vote, voting.Phase) error StopFn func(context.Context, txinfo.Transaction) error } // Vote calls the MockManager's Vote function, if set. Otherwise, it returns an error. -func (m *MockManager) Vote(ctx context.Context, tx txinfo.Transaction, vote voting.Vote) error { +func (m *MockManager) Vote(ctx context.Context, tx txinfo.Transaction, vote voting.Vote, phase voting.Phase) error { if m.VoteFn == nil { return errors.New("mock does not implement Vote function") } - return m.VoteFn(ctx, tx, vote) + return m.VoteFn(ctx, tx, vote, phase) } // Stop calls the MockManager's Stop function, if set. Otherwise, it returns an error. @@ -29,3 +30,50 @@ func (m *MockManager) Stop(ctx context.Context, tx txinfo.Transaction) error { } return m.StopFn(ctx, tx) } + +// PhasedVote is used to keep track of votes and the phase they were cast in. +type PhasedVote struct { + Vote voting.Vote + Phase voting.Phase +} + +// TrackingManager is a transaction manager which tracks all votes. Voting functions never return +// an error. +type TrackingManager struct { + MockManager + + votesLock sync.Mutex + votes []PhasedVote +} + +// NewTrackingManager creates a new TrackingManager which is ready for use. +func NewTrackingManager() *TrackingManager { + manager := &TrackingManager{} + + manager.VoteFn = func(_ context.Context, _ txinfo.Transaction, vote voting.Vote, phase voting.Phase) error { + manager.votesLock.Lock() + defer manager.votesLock.Unlock() + manager.votes = append(manager.votes, PhasedVote{Vote: vote, Phase: phase}) + return nil + } + + return manager +} + +// Votes returns a copy of all votes which have been cast. +func (m *TrackingManager) Votes() []PhasedVote { + m.votesLock.Lock() + defer m.votesLock.Unlock() + + votes := make([]PhasedVote, len(m.votes)) + copy(votes, m.votes) + + return votes +} + +// Reset resets all votes which have been recorded up to this point. +func (m *TrackingManager) Reset() { + m.votesLock.Lock() + defer m.votesLock.Unlock() + m.votes = nil +} diff --git a/internal/gitaly/transaction/voting.go b/internal/gitaly/transaction/voting.go index 071d944a8..d5ed0683f 100644 --- a/internal/gitaly/transaction/voting.go +++ b/internal/gitaly/transaction/voting.go @@ -25,9 +25,9 @@ func RunOnContext(ctx context.Context, fn func(txinfo.Transaction) error) error } // VoteOnContext casts the vote on a transaction identified by the context, if there is any. -func VoteOnContext(ctx context.Context, m Manager, vote voting.Vote) error { +func VoteOnContext(ctx context.Context, m Manager, vote voting.Vote, phase voting.Phase) error { return RunOnContext(ctx, func(transaction txinfo.Transaction) error { - return m.Vote(ctx, transaction, vote) + return m.Vote(ctx, transaction, vote, phase) }) } @@ -56,7 +56,7 @@ func CommitLockedFile(ctx context.Context, m Manager, writer *safe.LockingFileWr return fmt.Errorf("computing vote for locked file: %w", err) } - if err := m.Vote(ctx, tx, vote); err != nil { + if err := m.Vote(ctx, tx, vote, voting.Prepared); err != nil { return fmt.Errorf("preimage vote: %w", err) } @@ -69,7 +69,7 @@ func CommitLockedFile(ctx context.Context, m Manager, writer *safe.LockingFileWr return fmt.Errorf("committing file: %w", err) } - if err := VoteOnContext(ctx, m, vote); err != nil { + if err := VoteOnContext(ctx, m, vote, voting.Committed); err != nil { return fmt.Errorf("postimage vote: %w", err) } diff --git a/internal/gitaly/transaction/voting_test.go b/internal/gitaly/transaction/voting_test.go index 434c9374e..dc195c9f4 100644 --- a/internal/gitaly/transaction/voting_test.go +++ b/internal/gitaly/transaction/voting_test.go @@ -76,10 +76,10 @@ func TestVoteOnContext(t *testing.T) { AuthInfo: backchannel.WithID(nil, 1234), } - vote := voting.VoteFromData([]byte("1")) + expectedVote := voting.VoteFromData([]byte("1")) t.Run("without transaction", func(t *testing.T) { - require.NoError(t, VoteOnContext(ctx, &MockManager{}, voting.Vote{})) + require.NoError(t, VoteOnContext(ctx, &MockManager{}, voting.Vote{}, voting.Prepared)) }) t.Run("successful vote", func(t *testing.T) { @@ -89,7 +89,7 @@ func TestVoteOnContext(t *testing.T) { callbackExecuted := false require.NoError(t, VoteOnContext(ctx, &MockManager{ - VoteFn: func(ctx context.Context, tx txinfo.Transaction, vote voting.Vote) error { + VoteFn: func(ctx context.Context, tx txinfo.Transaction, vote voting.Vote, phase voting.Phase) error { require.Equal(t, txinfo.Transaction{ ID: 5678, Node: "node", @@ -97,9 +97,11 @@ func TestVoteOnContext(t *testing.T) { BackchannelID: 1234, }, tx) callbackExecuted = true + require.Equal(t, expectedVote, vote) + require.Equal(t, voting.Prepared, phase) return nil }, - }, vote)) + }, expectedVote, voting.Prepared)) require.True(t, callbackExecuted, "callback should have been executed") }) @@ -110,10 +112,10 @@ func TestVoteOnContext(t *testing.T) { expectedErr := fmt.Errorf("any error") require.Equal(t, expectedErr, VoteOnContext(ctx, &MockManager{ - VoteFn: func(ctx context.Context, tx txinfo.Transaction, vote voting.Vote) error { + VoteFn: func(context.Context, txinfo.Transaction, voting.Vote, voting.Phase) error { return expectedErr }, - }, vote)) + }, expectedVote, voting.Prepared)) }) } @@ -151,7 +153,7 @@ func TestCommitLockedFile(t *testing.T) { calls := 0 require.NoError(t, CommitLockedFile(ctx, &MockManager{ - VoteFn: func(ctx context.Context, tx txinfo.Transaction, vote voting.Vote) error { + VoteFn: func(ctx context.Context, tx txinfo.Transaction, vote voting.Vote, phase voting.Phase) error { require.Equal(t, txinfo.Transaction{ ID: 5678, Node: "node", @@ -160,6 +162,16 @@ func TestCommitLockedFile(t *testing.T) { }, tx) require.Equal(t, voting.VoteFromData([]byte("contents")), vote) calls++ + + switch calls { + case 1: + require.Equal(t, voting.Prepared, phase) + case 2: + require.Equal(t, voting.Committed, phase) + default: + require.FailNow(t, "unexpected voting phase %q", phase) + } + return nil }, }, writer)) @@ -177,7 +189,7 @@ func TestCommitLockedFile(t *testing.T) { require.NoError(t, err) err = CommitLockedFile(ctx, &MockManager{ - VoteFn: func(context.Context, txinfo.Transaction, voting.Vote) error { + VoteFn: func(context.Context, txinfo.Transaction, voting.Vote, voting.Phase) error { return fmt.Errorf("some error") }, }, writer) @@ -195,11 +207,10 @@ func TestCommitLockedFile(t *testing.T) { require.NoError(t, err) err = CommitLockedFile(ctx, &MockManager{ - VoteFn: func(context.Context, txinfo.Transaction, voting.Vote) error { + VoteFn: func(context.Context, txinfo.Transaction, voting.Vote, voting.Phase) error { // This shouldn't typically happen given that the file is locked, // but we concurrently update the file after our first vote. - require.NoError(t, os.WriteFile(file, []byte("something"), - 0o666)) + require.NoError(t, os.WriteFile(file, []byte("something"), 0o666)) return nil }, }, writer) diff --git a/internal/transaction/voting/phase.go b/internal/transaction/voting/phase.go new file mode 100644 index 000000000..fb3922a6c --- /dev/null +++ b/internal/transaction/voting/phase.go @@ -0,0 +1,36 @@ +package voting + +import ( + "fmt" + + "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" +) + +// Phase is the transactional phase a given vote can be cast on. +type Phase int + +const ( + // UnknownPhase is the default value. It should not be used. + UnknownPhase = Phase(iota) + // Prepared is the prepratory phase. The data that is about to change is locked for + // concurrent modification, but changes have not yet been written to disk. + Prepared + // Committed is the committing phase. Data has been committed to disk and will be visible + // in all subsequent requests. + Committed +) + +// ToProto converts the phase into its Protobuf enum. This function panics if called with an +// invalid phase. +func (p Phase) ToProto() gitalypb.VoteTransactionRequest_Phase { + switch p { + case UnknownPhase: + return gitalypb.VoteTransactionRequest_UNKNOWN_PHASE + case Prepared: + return gitalypb.VoteTransactionRequest_PREPARED_PHASE + case Committed: + return gitalypb.VoteTransactionRequest_COMMITTED_PHASE + default: + panic(fmt.Sprintf("unknown phase %q", p)) + } +} diff --git a/internal/transaction/voting/vote.go b/internal/transaction/voting/vote.go index dcb0fc32e..19412f742 100644 --- a/internal/transaction/voting/vote.go +++ b/internal/transaction/voting/vote.go @@ -37,6 +37,16 @@ func VoteFromHash(bytes []byte) (Vote, error) { return vote, nil } +// VoteFromString converts the given string representation of the hash into a vote. +func VoteFromString(s string) (Vote, error) { + bytes, err := hex.DecodeString(s) + if err != nil { + return Vote{}, fmt.Errorf("invalid vote string: %w", err) + } + + return VoteFromHash(bytes) +} + // VoteFromData hashes the given data and converts it to a vote. func VoteFromData(data []byte) Vote { return sha1.Sum(data) diff --git a/internal/transaction/voting/vote_test.go b/internal/transaction/voting/vote_test.go index 564eb2d84..98acfd509 100644 --- a/internal/transaction/voting/vote_test.go +++ b/internal/transaction/voting/vote_test.go @@ -2,7 +2,10 @@ package voting import ( "bytes" + "encoding/hex" + "errors" "fmt" + "strings" "testing" "github.com/stretchr/testify/require" @@ -23,6 +26,30 @@ func TestVoteFromHash(t *testing.T) { require.Equal(t, bytes.Repeat([]byte{1}, voteSize), vote.Bytes()) } +func TestVoteFromString(t *testing.T) { + _, err := VoteFromString("") + require.Equal(t, fmt.Errorf("invalid vote length 0"), err) + + _, err = VoteFromString("x") + require.Error(t, err) + var invalidByteError hex.InvalidByteError + require.True(t, errors.As(err, &invalidByteError)) + require.Equal(t, hex.InvalidByteError('x'), invalidByteError) + + _, err = VoteFromString("1234") + require.Equal(t, fmt.Errorf("invalid vote length 2"), err) + + _, err = VoteFromString(strings.Repeat("1", (voteSize+1)*2)) + require.Equal(t, fmt.Errorf("invalid vote length 21"), err) + + vote, err := VoteFromString(strings.Repeat("1", voteSize*2)) + require.Equal(t, Vote{ + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + }, vote) + require.NoError(t, err) +} + func TestVoteFromData(t *testing.T) { require.Equal(t, Vote{ 0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, diff --git a/proto/go/gitalypb/transaction.pb.go b/proto/go/gitalypb/transaction.pb.go index 24e8aaab8..7910a01d1 100644 --- a/proto/go/gitalypb/transaction.pb.go +++ b/proto/go/gitalypb/transaction.pb.go @@ -20,6 +20,63 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type VoteTransactionRequest_Phase int32 + +const ( + // UNKNOWN_PHASE is the unknown voting phase. This value has been the + // default because phases have been introduced. Eventually, using this + // phase will become unsupported. + VoteTransactionRequest_UNKNOWN_PHASE VoteTransactionRequest_Phase = 0 + // PREPARED_PHASE is the prepratory phase. The data that is about to change + // is locked for concurrent modification, but changes have not yet been + // written to disk. + VoteTransactionRequest_PREPARED_PHASE VoteTransactionRequest_Phase = 1 + // COMMITTED_PHASE is the committing phase. Data has been committed to disk + // and will be visible in all subsequent requests. + VoteTransactionRequest_COMMITTED_PHASE VoteTransactionRequest_Phase = 2 +) + +// Enum value maps for VoteTransactionRequest_Phase. +var ( + VoteTransactionRequest_Phase_name = map[int32]string{ + 0: "UNKNOWN_PHASE", + 1: "PREPARED_PHASE", + 2: "COMMITTED_PHASE", + } + VoteTransactionRequest_Phase_value = map[string]int32{ + "UNKNOWN_PHASE": 0, + "PREPARED_PHASE": 1, + "COMMITTED_PHASE": 2, + } +) + +func (x VoteTransactionRequest_Phase) Enum() *VoteTransactionRequest_Phase { + p := new(VoteTransactionRequest_Phase) + *p = x + return p +} + +func (x VoteTransactionRequest_Phase) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (VoteTransactionRequest_Phase) Descriptor() protoreflect.EnumDescriptor { + return file_transaction_proto_enumTypes[0].Descriptor() +} + +func (VoteTransactionRequest_Phase) Type() protoreflect.EnumType { + return &file_transaction_proto_enumTypes[0] +} + +func (x VoteTransactionRequest_Phase) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use VoteTransactionRequest_Phase.Descriptor instead. +func (VoteTransactionRequest_Phase) EnumDescriptor() ([]byte, []int) { + return file_transaction_proto_rawDescGZIP(), []int{0, 0} +} + // The outcome of the given transaction telling the client whether the // transaction should be committed or rolled back. type VoteTransactionResponse_TransactionState int32 @@ -55,11 +112,11 @@ func (x VoteTransactionResponse_TransactionState) String() string { } func (VoteTransactionResponse_TransactionState) Descriptor() protoreflect.EnumDescriptor { - return file_transaction_proto_enumTypes[0].Descriptor() + return file_transaction_proto_enumTypes[1].Descriptor() } func (VoteTransactionResponse_TransactionState) Type() protoreflect.EnumType { - return &file_transaction_proto_enumTypes[0] + return &file_transaction_proto_enumTypes[1] } func (x VoteTransactionResponse_TransactionState) Number() protoreflect.EnumNumber { @@ -83,6 +140,8 @@ type VoteTransactionRequest struct { Node string `protobuf:"bytes,3,opt,name=node,proto3" json:"node,omitempty"` // SHA1 of the references that are to be updated ReferenceUpdatesHash []byte `protobuf:"bytes,4,opt,name=reference_updates_hash,json=referenceUpdatesHash,proto3" json:"reference_updates_hash,omitempty"` + // Phase is the voting phase. + Phase VoteTransactionRequest_Phase `protobuf:"varint,5,opt,name=phase,proto3,enum=gitaly.VoteTransactionRequest_Phase" json:"phase,omitempty"` } func (x *VoteTransactionRequest) Reset() { @@ -145,6 +204,13 @@ func (x *VoteTransactionRequest) GetReferenceUpdatesHash() []byte { return nil } +func (x *VoteTransactionRequest) GetPhase() VoteTransactionRequest_Phase { + if x != nil { + return x.Phase + } + return VoteTransactionRequest_UNKNOWN_PHASE +} + type VoteTransactionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -292,7 +358,7 @@ var file_transaction_proto_rawDesc = []byte{ 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x1a, 0x0a, 0x6c, 0x69, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc3, 0x01, 0x0a, 0x16, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x72, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc4, 0x02, 0x0a, 0x16, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, @@ -304,43 +370,51 @@ var file_transaction_proto_rawDesc = []byte{ 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x14, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x48, 0x61, 0x73, 0x68, 0x22, 0x96, 0x01, 0x0a, 0x17, - 0x56, 0x6f, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, - 0x56, 0x6f, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, - 0x33, 0x0a, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, 0x10, 0x00, 0x12, - 0x09, 0x0a, 0x05, 0x41, 0x42, 0x4f, 0x52, 0x54, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, - 0x4f, 0x50, 0x10, 0x02, 0x22, 0x79, 0x0a, 0x16, 0x53, 0x74, 0x6f, 0x70, 0x54, 0x72, 0x61, 0x6e, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, - 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, - 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, - 0x19, 0x0a, 0x17, 0x53, 0x74, 0x6f, 0x70, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xc8, 0x01, 0x0a, 0x0e, 0x52, - 0x65, 0x66, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5a, 0x0a, - 0x0f, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x1e, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x72, - 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x72, - 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x01, 0x12, 0x5a, 0x0a, 0x0f, 0x53, 0x74, 0x6f, - 0x70, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x67, - 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x67, - 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, 0xfa, - 0x97, 0x28, 0x02, 0x08, 0x01, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2d, 0x6f, 0x72, 0x67, 0x2f, 0x67, - 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2f, 0x76, 0x31, 0x34, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, - 0x67, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x48, 0x61, 0x73, 0x68, 0x12, 0x3a, 0x0a, 0x05, 0x70, + 0x68, 0x61, 0x73, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x67, 0x69, 0x74, + 0x61, 0x6c, 0x79, 0x2e, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x50, 0x68, 0x61, 0x73, 0x65, + 0x52, 0x05, 0x70, 0x68, 0x61, 0x73, 0x65, 0x22, 0x43, 0x0a, 0x05, 0x50, 0x68, 0x61, 0x73, 0x65, + 0x12, 0x11, 0x0a, 0x0d, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x50, 0x48, 0x41, 0x53, + 0x45, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x50, 0x52, 0x45, 0x50, 0x41, 0x52, 0x45, 0x44, 0x5f, + 0x50, 0x48, 0x41, 0x53, 0x45, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x4f, 0x4d, 0x4d, 0x49, + 0x54, 0x54, 0x45, 0x44, 0x5f, 0x50, 0x48, 0x41, 0x53, 0x45, 0x10, 0x02, 0x22, 0x96, 0x01, 0x0a, + 0x17, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, + 0x2e, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x22, 0x33, 0x0a, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, 0x10, 0x00, + 0x12, 0x09, 0x0a, 0x05, 0x41, 0x42, 0x4f, 0x52, 0x54, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x53, + 0x54, 0x4f, 0x50, 0x10, 0x02, 0x22, 0x79, 0x0a, 0x16, 0x53, 0x74, 0x6f, 0x70, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x38, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x42, 0x04, 0x98, 0xc6, 0x2c, 0x01, 0x52, 0x0a, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x22, 0x19, 0x0a, 0x17, 0x53, 0x74, 0x6f, 0x70, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xc8, 0x01, 0x0a, 0x0e, + 0x52, 0x65, 0x66, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5a, + 0x0a, 0x0f, 0x56, 0x6f, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x1e, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x56, 0x6f, 0x74, 0x65, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1f, 0x2e, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x56, 0x6f, 0x74, 0x65, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x06, 0xfa, 0x97, 0x28, 0x02, 0x08, 0x01, 0x12, 0x5a, 0x0a, 0x0f, 0x53, 0x74, + 0x6f, 0x70, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, + 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, + 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x06, + 0xfa, 0x97, 0x28, 0x02, 0x08, 0x01, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2d, 0x6f, 0x72, 0x67, 0x2f, + 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x2f, 0x76, 0x31, 0x34, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x67, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x79, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -355,29 +429,31 @@ func file_transaction_proto_rawDescGZIP() []byte { return file_transaction_proto_rawDescData } -var file_transaction_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_transaction_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_transaction_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_transaction_proto_goTypes = []interface{}{ - (VoteTransactionResponse_TransactionState)(0), // 0: gitaly.VoteTransactionResponse.TransactionState - (*VoteTransactionRequest)(nil), // 1: gitaly.VoteTransactionRequest - (*VoteTransactionResponse)(nil), // 2: gitaly.VoteTransactionResponse - (*StopTransactionRequest)(nil), // 3: gitaly.StopTransactionRequest - (*StopTransactionResponse)(nil), // 4: gitaly.StopTransactionResponse - (*Repository)(nil), // 5: gitaly.Repository + (VoteTransactionRequest_Phase)(0), // 0: gitaly.VoteTransactionRequest.Phase + (VoteTransactionResponse_TransactionState)(0), // 1: gitaly.VoteTransactionResponse.TransactionState + (*VoteTransactionRequest)(nil), // 2: gitaly.VoteTransactionRequest + (*VoteTransactionResponse)(nil), // 3: gitaly.VoteTransactionResponse + (*StopTransactionRequest)(nil), // 4: gitaly.StopTransactionRequest + (*StopTransactionResponse)(nil), // 5: gitaly.StopTransactionResponse + (*Repository)(nil), // 6: gitaly.Repository } var file_transaction_proto_depIdxs = []int32{ - 5, // 0: gitaly.VoteTransactionRequest.repository:type_name -> gitaly.Repository - 0, // 1: gitaly.VoteTransactionResponse.state:type_name -> gitaly.VoteTransactionResponse.TransactionState - 5, // 2: gitaly.StopTransactionRequest.repository:type_name -> gitaly.Repository - 1, // 3: gitaly.RefTransaction.VoteTransaction:input_type -> gitaly.VoteTransactionRequest - 3, // 4: gitaly.RefTransaction.StopTransaction:input_type -> gitaly.StopTransactionRequest - 2, // 5: gitaly.RefTransaction.VoteTransaction:output_type -> gitaly.VoteTransactionResponse - 4, // 6: gitaly.RefTransaction.StopTransaction:output_type -> gitaly.StopTransactionResponse - 5, // [5:7] is the sub-list for method output_type - 3, // [3:5] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 6, // 0: gitaly.VoteTransactionRequest.repository:type_name -> gitaly.Repository + 0, // 1: gitaly.VoteTransactionRequest.phase:type_name -> gitaly.VoteTransactionRequest.Phase + 1, // 2: gitaly.VoteTransactionResponse.state:type_name -> gitaly.VoteTransactionResponse.TransactionState + 6, // 3: gitaly.StopTransactionRequest.repository:type_name -> gitaly.Repository + 2, // 4: gitaly.RefTransaction.VoteTransaction:input_type -> gitaly.VoteTransactionRequest + 4, // 5: gitaly.RefTransaction.StopTransaction:input_type -> gitaly.StopTransactionRequest + 3, // 6: gitaly.RefTransaction.VoteTransaction:output_type -> gitaly.VoteTransactionResponse + 5, // 7: gitaly.RefTransaction.StopTransaction:output_type -> gitaly.StopTransactionResponse + 6, // [6:8] is the sub-list for method output_type + 4, // [4:6] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_transaction_proto_init() } @@ -442,7 +518,7 @@ func file_transaction_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_transaction_proto_rawDesc, - NumEnums: 1, + NumEnums: 2, NumMessages: 4, NumExtensions: 0, NumServices: 1, diff --git a/proto/transaction.proto b/proto/transaction.proto index 745a764a5..3d3e48de5 100644 --- a/proto/transaction.proto +++ b/proto/transaction.proto @@ -26,6 +26,20 @@ service RefTransaction { } message VoteTransactionRequest { + enum Phase { + // UNKNOWN_PHASE is the unknown voting phase. This value has been the + // default because phases have been introduced. Eventually, using this + // phase will become unsupported. + UNKNOWN_PHASE = 0; + // PREPARED_PHASE is the prepratory phase. The data that is about to change + // is locked for concurrent modification, but changes have not yet been + // written to disk. + PREPARED_PHASE = 1; + // COMMITTED_PHASE is the committing phase. Data has been committed to disk + // and will be visible in all subsequent requests. + COMMITTED_PHASE = 2; + }; + Repository repository = 1[(target_repository)=true]; // ID of the transaction we're processing uint64 transaction_id = 2; @@ -33,6 +47,8 @@ message VoteTransactionRequest { string node = 3; // SHA1 of the references that are to be updated bytes reference_updates_hash = 4; + // Phase is the voting phase. + Phase phase = 5; } message VoteTransactionResponse { diff --git a/ruby/proto/gitaly/transaction_pb.rb b/ruby/proto/gitaly/transaction_pb.rb index 59c7f5626..01a04bf3c 100644 --- a/ruby/proto/gitaly/transaction_pb.rb +++ b/ruby/proto/gitaly/transaction_pb.rb @@ -12,6 +12,12 @@ Google::Protobuf::DescriptorPool.generated_pool.build do optional :transaction_id, :uint64, 2 optional :node, :string, 3 optional :reference_updates_hash, :bytes, 4 + optional :phase, :enum, 5, "gitaly.VoteTransactionRequest.Phase" + end + add_enum "gitaly.VoteTransactionRequest.Phase" do + value :UNKNOWN_PHASE, 0 + value :PREPARED_PHASE, 1 + value :COMMITTED_PHASE, 2 end add_message "gitaly.VoteTransactionResponse" do optional :state, :enum, 1, "gitaly.VoteTransactionResponse.TransactionState" @@ -32,6 +38,7 @@ end module Gitaly VoteTransactionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitaly.VoteTransactionRequest").msgclass + VoteTransactionRequest::Phase = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitaly.VoteTransactionRequest.Phase").enummodule VoteTransactionResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitaly.VoteTransactionResponse").msgclass VoteTransactionResponse::TransactionState = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitaly.VoteTransactionResponse.TransactionState").enummodule StopTransactionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("gitaly.StopTransactionRequest").msgclass |