Skip to content

fix: Timeout on concurrent doc-sync for the same doc - #5021

Open
islamaliev wants to merge 5 commits into
sourcenetwork:developfrom
islamaliev:fix/concurrent-doc-sync-timeout
Open

fix: Timeout on concurrent doc-sync for the same doc#5021
islamaliev wants to merge 5 commits into
sourcenetwork:developfrom
islamaliev:fix/concurrent-doc-sync-timeout

Conversation

@islamaliev

Copy link
Copy Markdown
Contributor

Relevant issue(s)

Resolves #5020

Description

Calling SyncDocuments more than once at the same time for the same document made all but one of the calls block until their deadline and return "timeout while syncing doc", even though the document actually synced. The concurrent requests were indistinguishable from each other, so they shared a single in-flight response slot: one call received the peer's reply and the rest were left waiting. Each request now carries a random nonce so concurrent syncs for the same document stay independent, matching what the encryption-key sync path already does.

How has this been tested?

Added an integration test that fires several concurrent SyncDocuments calls for the same document and asserts they all succeed (it fails without the fix). Existing P2P sync tests continue to pass.

Concurrent SyncDocuments calls for the same docID collided on the
pubsub-rpc response-channel key, which is a hash of the request bytes.
The request carried nothing unique, so identical requests shared one
in-flight slot: one call received the response and the rest blocked
until their deadline and returned "timeout while syncing doc", even
though the document actually synced.

Add a random RequestID nonce to docSyncRequest so each request marshals
to distinct bytes and gets its own response channel, mirroring the KMS
pubsub request.
@islamaliev islamaliev self-assigned this Jul 1, 2026
@islamaliev
islamaliev requested a review from a team July 1, 2026 20:52
@islamaliev islamaliev added the bug Something isn't working label Jul 1, 2026
@islamaliev islamaliev added this to the DefraDB v1.1 milestone Jul 1, 2026
@islamaliev islamaliev changed the title fix(p2p): concurrent SyncDocuments for the same doc no longer time out fix(p2p): Timeout on concurrent doc-sync for the same doc Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a random nonce to doc sync requests, updates integration helpers for concurrent same-document calls, and adds an integration test that verifies four simultaneous requests succeed.

Changes

Nonce fix and concurrency test support

Layer / File(s) Summary
Add RequestID nonce to docSyncRequest
internal/db/p2p/sync_doc.go
Adds a RequestID field and generates a 16-byte random nonce before marshalling and publishing each request.
Add concurrent SyncDocs test support
tests/integration/test_case.go, tests/integration/p2p.go
Adds Concurrency and runs parallel SyncDocuments calls with per-call error assertions while preserving the single-call retry path.
Add concurrent same-document integration test
tests/integration/net/sync/documents_test.go
Runs four concurrent SyncDocs calls for the same document ID and verifies success.
Update test license header
tests/integration/index/list_collection_name_test.go
Replaces the previous license notice with the test suite licensing statement.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Test as Integration test
  participant syncDocs
  participant Node
  participant syncDocuments
  participant Pubsub

  Test->>syncDocs: SyncDocs(Concurrency=4, same DocID)
  loop 4 concurrent calls
    syncDocs->>Node: SyncDocuments(docIDs)
    Node->>syncDocuments: Build docSyncRequest
    syncDocuments->>syncDocuments: Generate RequestID
    syncDocuments->>Pubsub: Publish unique request
    Pubsub-->>syncDocuments: Route response
    syncDocuments-->>Node: Return result
    Node-->>syncDocs: Return result
  end
  syncDocs->>syncDocs: Assert each result
Loading

Assessment against linked issues

Objective Addressed Explanation
Concurrent SyncDocuments calls for the same document do not time out because of response-channel collisions [#5020]
Requests contain unique identity data for response routing [#5020]
The concurrent same-document case has integration coverage [#5020]

Out-of-scope changes

Code Change Explanation
Update test license header (tests/integration/index/list_collection_name_test.go:3-10) This change does not relate to concurrent document synchronization or response-channel routing.

Suggested labels: area/p2p, area/testing

Suggested reviewers: shahzadlone


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/integration/p2p.go`:
- Around line 288-294: The per-call failure message in the concurrent
SyncDocuments loop is unreachable because AssertError already calls
require.NoError when action.ExpectedError is empty, so the later require.NoError
never runs with context. Update the loop in the p2p integration test to avoid
double fatal checks: let AssertError handle expected-error validation, and use a
non-fatal assertion such as assert.NoError for the contextual "concurrent
SyncDocuments call %d/%d failed" message so all errs entries are evaluated
before failing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8d08e9ba-d5e1-4cd3-a9a0-2d3d1775b64d

📥 Commits

Reviewing files that changed from the base of the PR and between 96bb671 and 6dcfb53.

📒 Files selected for processing (4)
  • internal/db/p2p/sync_doc.go
  • tests/integration/net/sync/documents_test.go
  • tests/integration/p2p.go
  • tests/integration/test_case.go
📜 Review details
⏰ Context from checks skipped due to timeout. (51)
  • GitHub Check: Test Limited Resource job
  • GitHub Check: Build and test Linux C shared library
  • GitHub Check: Start binary job
  • GitHub Check: Test coverage job (c, file, collection-named)
  • GitHub Check: Test coverage job (cli, file, collection-save)
  • GitHub Check: Test coverage job (c, memory, collection-save)
  • GitHub Check: Test coverage job (cli, file, collection-named)
  • GitHub Check: Test coverage job (c, file, gql)
  • GitHub Check: Test coverage job (c, memory, collection-named)
  • GitHub Check: Test coverage job (c, file, collection-save)
  • GitHub Check: Test coverage job (cli, memory, collection-named)
  • GitHub Check: Test coverage job (c, memory, gql)
  • GitHub Check: Test coverage job (cli, file, gql)
  • GitHub Check: Test coverage job (http, file, collection-named)
  • GitHub Check: Test coverage job (cli, memory, gql)
  • GitHub Check: Test coverage job (http, memory, collection-save)
  • GitHub Check: Test macos job
  • GitHub Check: Test coverage job (cli, memory, collection-save)
  • GitHub Check: Test coverage job (http, file, gql)
  • GitHub Check: Test coverage job (go, file, gql)
  • GitHub Check: Test coverage job (go, file, collection-save)
  • GitHub Check: Test coverage job (http, memory, collection-named)
  • GitHub Check: Test coverage job (http, memory, gql)
  • GitHub Check: Test coverage job (http, file, collection-save)
  • GitHub Check: Test coverage job (go, memory, gql)
  • GitHub Check: Test coverage job (go, memory, collection-named)
  • GitHub Check: Test coverage job (go, memory, collection-save)
  • GitHub Check: Test coverage job (go, file, collection-named)
  • GitHub Check: Check wizard health job
  • GitHub Check: Build, install, and test Debian package
  • GitHub Check: Check data format changes job
  • GitHub Check: Test coverage document acp job (go, source-hub)
  • GitHub Check: Validate containerfile job
  • GitHub Check: Test coverage secondary index job
  • GitHub Check: Test coverage document acp job (c, source-hub)
  • GitHub Check: Test coverage document acp job (http, source-hub)
  • GitHub Check: Test coverage signed docs job
  • GitHub Check: Test coverage JS job
  • GitHub Check: Test coverage lens job (wazero)
  • GitHub Check: Test coverage leveldb job
  • GitHub Check: Test coverage encryption job
  • GitHub Check: Test coverage document acp job (cli, source-hub)
  • GitHub Check: Test coverage telemetry job
  • GitHub Check: Test coverage view job
  • GitHub Check: Check mocks job
  • GitHub Check: Test NPX/JS build job
  • GitHub Check: Check vulnerabilities job
  • GitHub Check: Build dependencies job
  • GitHub Check: Check http documentation job
  • GitHub Check: Check cli documentation job
  • GitHub Check: Lint GoLang job
⚠️ CI failures not shown inline (2)

GitHub Actions: Validate Title Workflow / 0_Validate title job.txt: fix(p2p): concurrent SyncDocuments for the same doc no longer time out

Conclusion: failure

View job details

##[group]Run ./tools/scripts/validate-conventional-style.sh "fix(p2p): concurrent SyncDocuments for the same doc no longer time out"
 �[36;1m./tools/scripts/validate-conventional-style.sh "fix(p2p): concurrent SyncDocuments for the same doc no longer time out"�[0m
 shell: /usr/bin/bash -e {0}
 ##[endgroup]
 Error: The length of the title is too long (should be 60 or less).
 ##[error]Process completed with exit code 3.

GitHub Actions: Validate Title Workflow / Validate title job: fix(p2p): concurrent SyncDocuments for the same doc no longer time out

Conclusion: failure

View job details

##[group]Run ./tools/scripts/validate-conventional-style.sh "fix(p2p): concurrent SyncDocuments for the same doc no longer time out"
 �[36;1m./tools/scripts/validate-conventional-style.sh "fix(p2p): concurrent SyncDocuments for the same doc no longer time out"�[0m
 shell: /usr/bin/bash -e {0}
 ##[endgroup]
 Error: The length of the title is too long (should be 60 or less).
 ##[error]Process completed with exit code 3.
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-03-27T08:24:16.083Z
Learnt from: islamaliev
Repo: sourcenetwork/defradb PR: 4661
File: http/middleware.go:93-93
Timestamp: 2026-03-27T08:24:16.083Z
Learning: When handling `client.ErrNotAuthorizedToPerformOperation` in the defradb repository, treat it as an unauthenticated/missing-identity condition (not insufficient permissions). Map it to HTTP 401 Unauthorized rather than 403 Forbidden; this mapping is intentional and consistent with prior behavior (pre-PR `#4661`).

Applied to files:

  • tests/integration/net/sync/documents_test.go
  • tests/integration/test_case.go
  • tests/integration/p2p.go
  • internal/db/p2p/sync_doc.go
📚 Learning: 2026-06-18T16:47:47.339Z
Learnt from: islamaliev
Repo: sourcenetwork/defradb PR: 4942
File: internal/db/db.go:227-231
Timestamp: 2026-06-18T16:47:47.339Z
Learning: When reviewing this repository’s Go code, do not flag `sync.WaitGroup.Go()` (e.g., `wg.Go(fn)`) as a compilation error. This method exists starting in Go 1.25; it should be treated as valid as long as the repo’s configured/minimum Go toolchain version is 1.25+ (e.g., per `go.mod` and/or CI). If the project is building with an older Go version, then it may be appropriate to flag it.

Applied to files:

  • tests/integration/net/sync/documents_test.go
  • tests/integration/test_case.go
  • tests/integration/p2p.go
  • internal/db/p2p/sync_doc.go
📚 Learning: 2026-06-18T16:47:47.339Z
Learnt from: islamaliev
Repo: sourcenetwork/defradb PR: 4942
File: internal/db/db.go:227-231
Timestamp: 2026-06-18T16:47:47.339Z
Learning: In sourcenetwork/defradb code reviews, do not raise findings that are likely compiler/type-check errors (e.g., code that would fail `go test`/`go build`) because CI pipelines will catch compilation failures. Only flag compilation-related problems if there is a clear, verifiable issue beyond what CI will already surface (e.g., a mismatched build tag or missing generated artifact that CI would not cover).

Applied to files:

  • tests/integration/net/sync/documents_test.go
  • tests/integration/test_case.go
  • tests/integration/p2p.go
  • internal/db/p2p/sync_doc.go
🔇 Additional comments (4)
internal/db/p2p/sync_doc.go (1)

15-15: LGTM!

Also applies to: 44-48, 119-125

tests/integration/test_case.go (1)

716-724: LGTM!

tests/integration/p2p.go (1)

16-16: LGTM!

Also applies to: 249-287, 296-315

tests/integration/net/sync/documents_test.go (1)

396-434: LGTM!

Comment thread tests/integration/p2p.go
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.33333% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.21%. Comparing base (13c46ec) to head (f452aa2).

Files with missing lines Patch % Lines
internal/db/p2p/sync_doc.go 33.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #5021      +/-   ##
===========================================
+ Coverage    76.28%   77.21%   +0.93%     
===========================================
  Files          624      607      -17     
  Lines        49519    47541    -1978     
===========================================
- Hits         37774    36706    -1068     
+ Misses        8616     7810     -806     
+ Partials      3129     3025     -104     
Flag Coverage Δ
all-tests 77.21% <33.33%> (+0.93%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
internal/db/p2p/sync_doc.go 63.23% <33.33%> (-0.59%) ⬇️

... and 78 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 13c46ec...f452aa2. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The concurrent SyncDocs path shared a single options builder across all
goroutines. The builder is a stateful, single-use enumerable, so the calls
raced on and exhausted its cursor, failing the race detector in CI. Build a
fresh builder inside each call instead.
@islamaliev islamaliev changed the title fix(p2p): Timeout on concurrent doc-sync for the same doc fix: Timeout on concurrent doc-sync for the same doc Jul 2, 2026
// pubsub-rpc layer keys its response channel by a hash of the request bytes, so without this
// two concurrent syncs for the same docIDs would collide on that key and one would time out
// while the other succeeds. Mirrors the KMS pubsub fix (fetchEncryptionKeyRequest).
RequestID []byte `json:"requestID"`

@AndrewSisley AndrewSisley Jul 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why split the requests like this, instead of combining them?

What is the benefit to users in keeping them seperate?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

combining them sounds interesting. If two callers ask for the same documents at the same time, sending two identical requests is wasteful. We can do that with single-flight at the DB level: the first caller does the real request, and any concurrent caller asking for the same documents waits on that result.

And to be clear: the routing collision only ever happens for byte-identical requests, i.e. the same docIDs. Different or even partially-overlapping docID sets already have different keys and never collide.

I can proceed with single-flight solution unless you had another solution in mind.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another caveat that we should consider if we decide with combining them is that we also have to include an identity to the serialized key. So in total we will serialize key on collection name on doc ids and on identity. We need to do this because for different users the results should be different. That kind of adds additional overhead. To every request! What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sending two identical requests is wasteful

This is a very small amount of wastage though no?

Another caveat that we should consider if we decide with combining them

Sorry, my original comment was ambiguous. I wasn't really talking about combining the request messages, only their results.

So if two sync requests were requested by users, two would be sent, and their timeouts/results would merge - the second request to be handled by the client-node would essentially extend the timeout begun by the first request. The first request would timeout if/when it's timeout its timeout is reached, the second if/when its timeout is reached.

The server-nodes would need to re-check upon receipt of the second request in case their are new heads, but there is no need to keep the requests or responses seperate via a nonce - either results are recieved by the client-node within the relevant timeout, or they are not - it does not matter which request triggered the response, they are all equally valid?

@AndrewSisley AndrewSisley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, sorry - I probably should have approved this when I asked the pending question - it is not a blocker - is just me exploring the solution and wondering if we can make additional improvements (especially as dropping the new prop is a breaking change).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/integration/index/list_collection_name_test.go (1)

60-105: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise the multi-collection query that this test claims to cover.

The test makes two separate action.ListIndexes calls. Each call sets one CollectionID, so neither call returns results from both collections. A bug that labels every result with one collection name can pass this test. Use the actual multi-collection listing path in one call. If no such path exists, rename the test and comments to describe two independent collection-scoped calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/index/list_collection_name_test.go` around lines 60 - 105,
The test currently makes two collection-scoped ListIndexes calls, so it does not
exercise a multi-collection query. Update
TestIndexList_WithMultipleCollections_EachResultNamesItsOwnCollection to use the
actual multi-collection listing path in a single action and assert both
collections’ indexes and names; if that API does not exist, rename the test to
describe independent collection-scoped calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/integration/index/list_collection_name_test.go`:
- Around line 60-105: The test currently makes two collection-scoped ListIndexes
calls, so it does not exercise a multi-collection query. Update
TestIndexList_WithMultipleCollections_EachResultNamesItsOwnCollection to use the
actual multi-collection listing path in a single action and assert both
collections’ indexes and names; if that API does not exist, rename the test to
describe independent collection-scoped calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ce3af0c2-702b-45e3-a5db-5ae7a87588d7

📥 Commits

Reviewing files that changed from the base of the PR and between 1df9a90 and f452aa2.

📒 Files selected for processing (1)
  • tests/integration/index/list_collection_name_test.go
📜 Review details
⏰ Context from checks skipped due to timeout. (25)
  • GitHub Check: Test NPX/JS build job
  • GitHub Check: Check http documentation job
  • GitHub Check: Check cli documentation job
  • GitHub Check: Lint GoLang job
  • GitHub Check: Build, install, and test Debian package
  • GitHub Check: Test coverage job (http, memory, gql)
  • GitHub Check: Test Limited Resource job
  • GitHub Check: Check wizard health job
  • GitHub Check: Validate containerfile job
  • GitHub Check: Test coverage document acp job (cli, source-hub)
  • GitHub Check: Test coverage document acp job (c, source-hub)
  • GitHub Check: Test coverage document acp job (go, source-hub)
  • GitHub Check: Test coverage document acp job (http, source-hub)
  • GitHub Check: Test coverage JS job
  • GitHub Check: Test coverage encryption job
  • GitHub Check: Build and test Linux C shared library
  • GitHub Check: Test coverage view job
  • GitHub Check: Test coverage signed docs job
  • GitHub Check: Test coverage lens job (wazero)
  • GitHub Check: Start binary job
  • GitHub Check: Check mocks job
  • GitHub Check: Build dependencies job
  • GitHub Check: Check vulnerabilities job
  • GitHub Check: Check mod tidy job
  • GitHub Check: Test macos job
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-03-27T08:24:16.083Z
Learnt from: islamaliev
Repo: sourcenetwork/defradb PR: 4661
File: http/middleware.go:93-93
Timestamp: 2026-03-27T08:24:16.083Z
Learning: When handling `client.ErrNotAuthorizedToPerformOperation` in the defradb repository, treat it as an unauthenticated/missing-identity condition (not insufficient permissions). Map it to HTTP 401 Unauthorized rather than 403 Forbidden; this mapping is intentional and consistent with prior behavior (pre-PR `#4661`).

Applied to files:

  • tests/integration/index/list_collection_name_test.go
📚 Learning: 2026-06-18T16:47:47.339Z
Learnt from: islamaliev
Repo: sourcenetwork/defradb PR: 4942
File: internal/db/db.go:227-231
Timestamp: 2026-06-18T16:47:47.339Z
Learning: When reviewing this repository’s Go code, do not flag `sync.WaitGroup.Go()` (e.g., `wg.Go(fn)`) as a compilation error. This method exists starting in Go 1.25; it should be treated as valid as long as the repo’s configured/minimum Go toolchain version is 1.25+ (e.g., per `go.mod` and/or CI). If the project is building with an older Go version, then it may be appropriate to flag it.

Applied to files:

  • tests/integration/index/list_collection_name_test.go
📚 Learning: 2026-06-18T16:47:47.339Z
Learnt from: islamaliev
Repo: sourcenetwork/defradb PR: 4942
File: internal/db/db.go:227-231
Timestamp: 2026-06-18T16:47:47.339Z
Learning: In sourcenetwork/defradb code reviews, do not raise findings that are likely compiler/type-check errors (e.g., code that would fail `go test`/`go build`) because CI pipelines will catch compilation failures. Only flag compilation-related problems if there is a clear, verifiable issue beyond what CI will already surface (e.g., a mismatched build tag or missing generated artifact that CI would not cover).

Applied to files:

  • tests/integration/index/list_collection_name_test.go
🔇 Additional comments (1)
tests/integration/index/list_collection_name_test.go (1)

1-56: LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(p2p): concurrent SyncDocuments for the same doc time out for all but one caller

2 participants