Repository navigation
fix(dgraph): commit the transaction Mutate opens #4158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
3cb0903
fix(dgraph): commit the transaction Mutate opens (#4157)
aryanmehrotra ea8b1df
Merge branch 'development' into fix/dgraph-mutate-commit
aryanmehrotra 8503937
fix(dgraph): keep the deferred discard out of the caller's cancellation
aryanmehrotra e25d673
chore: drop the go.work.sum line a local toolchain run added
aryanmehrotra 3dcbaba
Merge remote-tracking branch 'origin/development' into audit4158
aryanmehrotra 475ac4e
Merge branch 'development' into fix/dgraph-mutate-commit
Umang01-hash 88d64be
fix(dgraph): cite dgo and gomock by method, not by line number
aryanmehrotra 461df30
Merge branch 'development' into fix/dgraph-mutate-commit
Umang01-hash 4f58e22
Merge branch 'development' into fix/dgraph-mutate-commit
aryanmehrotra e0db632
docs(dgraph): say why the deferred discard costs no round trip
aryanmehrotra 89bd537
docs(container): state the Dgraph Mutate commit guarantee on the inte…
aryanmehrotra a33664e
Merge branch 'development' into fix/dgraph-mutate-commit
Umang01-hash File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| package dgraph | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "github.com/dgraph-io/dgo/v210/protos/api" | ||
| "github.com/stretchr/testify/require" | ||
| "go.opentelemetry.io/otel" | ||
| "go.uber.org/mock/gomock" | ||
| ) | ||
|
|
||
| var ( | ||
| errCommitFailed = errors.New("commit failed") | ||
| errDiscardFailed = errors.New("discard failed") | ||
| ) | ||
|
|
||
| // recordingTxn counts what Mutate does to the transaction it opens. | ||
| // | ||
| // A gomock Txn cannot answer "was Commit called?" in this package: setupDB runs ctrl.Finish() | ||
| // when it returns, which marks the controller finished before the test body starts, so the | ||
| // t.Cleanup verification gomock.NewController installs short-circuits on Controller.finish's | ||
| // already-finished branch, and an unmet expectation is never reported. Counting here asserts on | ||
| // what happened rather than on the mock library's bookkeeping. | ||
| type recordingTxn struct { | ||
| mutateErr error | ||
| commitErr error | ||
| discardErr error | ||
|
|
||
| mutations int | ||
| commits int | ||
| discards int | ||
|
|
||
| // discardCtxErr is whatever the context handed to Discard reported. Mutate | ||
| // detaches cancellation for the discard alone, so this stays nil even when | ||
| // the caller's context is already done. | ||
| discardCtxErr error | ||
| } | ||
|
|
||
| func (r *recordingTxn) Mutate(_ context.Context, _ *api.Mutation) (*api.Response, error) { | ||
| r.mutations++ | ||
|
|
||
| if r.mutateErr != nil { | ||
| return nil, r.mutateErr | ||
| } | ||
|
|
||
| return &api.Response{Json: []byte(`{}`)}, nil | ||
| } | ||
|
|
||
| func (r *recordingTxn) Commit(context.Context) error { | ||
| r.commits++ | ||
|
|
||
| return r.commitErr | ||
| } | ||
|
|
||
| func (r *recordingTxn) Discard(ctx context.Context) error { | ||
| r.discards++ | ||
| r.discardCtxErr = ctx.Err() | ||
|
|
||
| return r.discardErr | ||
| } | ||
|
|
||
| func (r *recordingTxn) BestEffort() Txn { return r } | ||
|
|
||
| func (*recordingTxn) Query(context.Context, string) (*api.Response, error) { return nil, nil } | ||
|
|
||
| func (*recordingTxn) QueryRDF(context.Context, string) (*api.Response, error) { return nil, nil } | ||
|
|
||
| func (*recordingTxn) QueryWithVars(context.Context, string, map[string]string) (*api.Response, error) { | ||
| return nil, nil | ||
| } | ||
|
|
||
| func (*recordingTxn) QueryRDFWithVars(context.Context, string, | ||
| map[string]string) (*api.Response, error) { | ||
| return nil, nil | ||
| } | ||
|
|
||
| func (*recordingTxn) Do(context.Context, *api.Request) (*api.Response, error) { return nil, nil } | ||
|
|
||
| func setupWithTxn(t *testing.T, txn Txn) *Client { | ||
| t.Helper() | ||
|
|
||
| ctrl := gomock.NewController(t) | ||
|
|
||
| logger := NewMockLogger(ctrl) | ||
| logger.EXPECT().Debug(gomock.Any()).AnyTimes() | ||
| logger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes() | ||
| logger.EXPECT().Log(gomock.Any()).AnyTimes() | ||
| logger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() | ||
| logger.EXPECT().Error(gomock.Any(), gomock.Any()).AnyTimes() | ||
|
|
||
| metrics := NewMockMetrics(ctrl) | ||
| metrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() | ||
|
|
||
| client := New(Config{Host: "localhost", Port: "9080"}) | ||
| client.UseLogger(logger) | ||
| client.UseMetrics(metrics) | ||
| client.UseTracer(otel.GetTracerProvider().Tracer("gofr-dgraph")) | ||
|
|
||
| dgraphClient := NewMockDgraphClient(ctrl) | ||
| dgraphClient.EXPECT().NewTxn().Return(txn).AnyTimes() | ||
| client.client = dgraphClient | ||
|
|
||
| return client | ||
| } | ||
|
|
||
| func Test_Mutate_TransactionHandling(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| commitNow bool | ||
| txn recordingTxn | ||
| wantErr error | ||
| wantResp bool | ||
| wantCommits int | ||
| wantDiscards int | ||
| }{ | ||
| { | ||
| // The defect: dgo only finishes the transaction when CommitNow is set, so without | ||
| // an explicit Commit the write was staged and abandoned, silently. | ||
| name: "commits when CommitNow is not set", | ||
| wantResp: true, | ||
| wantCommits: 1, | ||
| wantDiscards: 1, | ||
| }, | ||
| { | ||
| // dgo has already finished the transaction; Commit again returns ErrFinished. | ||
| name: "does not commit again when CommitNow is set", | ||
| commitNow: true, | ||
| wantResp: true, | ||
| wantCommits: 0, | ||
| wantDiscards: 1, | ||
| }, | ||
| { | ||
| name: "commit failure is returned", | ||
| txn: recordingTxn{commitErr: errCommitFailed}, | ||
| wantErr: errCommitFailed, | ||
| wantCommits: 1, | ||
| wantDiscards: 1, | ||
| }, | ||
| { | ||
| name: "mutate failure is returned without committing", | ||
| txn: recordingTxn{mutateErr: errMutationFailed}, | ||
| wantErr: errMutationFailed, | ||
| wantCommits: 0, | ||
| wantDiscards: 1, | ||
| }, | ||
| { | ||
| // A failing Discard must not turn a committed write into an error. | ||
| name: "discard failure does not fail a committed write", | ||
| txn: recordingTxn{discardErr: errDiscardFailed}, | ||
| wantResp: true, | ||
| wantCommits: 1, | ||
| wantDiscards: 1, | ||
| }, | ||
| } | ||
|
|
||
| for _, tc := range tests { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| txn := tc.txn | ||
| client := setupWithTxn(t, &txn) | ||
|
|
||
| resp, err := client.Mutate(t.Context(), &api.Mutation{ | ||
| SetJson: []byte(`{"name":"GoFr"}`), | ||
| CommitNow: tc.commitNow, | ||
| }) | ||
|
|
||
| if tc.wantErr != nil { | ||
| require.ErrorIs(t, err, tc.wantErr) | ||
| require.Nil(t, resp) | ||
| } else { | ||
| require.NoError(t, err) | ||
| } | ||
|
|
||
| require.Equal(t, tc.wantResp, resp != nil, "response") | ||
| require.Equal(t, 1, txn.mutations, "Mutate calls") | ||
| require.Equal(t, tc.wantCommits, txn.commits, "Commit calls") | ||
| require.Equal(t, tc.wantDiscards, txn.discards, "Discard calls") | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // Test_Mutate_DiscardSurvivesCallerCancellation pins that the deferred discard does not | ||
| // inherit the caller's cancellation. | ||
| // | ||
| // Discard runs after the commit has already returned. If it took the caller's context, a | ||
| // request canceled in that window would log "discard failed" for a write that was persisted | ||
| // -- an error line describing a success, which is the kind of log that sends someone looking | ||
| // for a data-loss bug that is not there. | ||
| func Test_Mutate_DiscardSurvivesCallerCancellation(t *testing.T) { | ||
| txn := recordingTxn{} | ||
| client := setupWithTxn(t, &txn) | ||
|
|
||
| ctx, cancel := context.WithCancel(t.Context()) | ||
| cancel() | ||
|
|
||
| _, _ = client.Mutate(ctx, &api.Mutation{SetJson: []byte(`{"name":"GoFr"}`)}) | ||
|
|
||
| require.Equal(t, 1, txn.discards, "the transaction is still discarded") | ||
| require.NoError(t, txn.discardCtxErr, "the discard must not inherit the caller's cancellation") | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.