Skip to content

feat(synced): add SetIfNotExistsMulti for single-lock bulk conditional insert#114

Merged
mrz1836 merged 1 commit into
masterfrom
feat/set-if-not-exists-multi
May 15, 2026
Merged

feat(synced): add SetIfNotExistsMulti for single-lock bulk conditional insert#114
mrz1836 merged 1 commit into
masterfrom
feat/set-if-not-exists-multi

Conversation

@icellan

@icellan icellan commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds SyncedMap.SetIfNotExistsMulti(keys, values) []bool — a bulk conditional-insert variant that takes the write-lock once for the whole batch instead of N times.

Why

The existing single-key SetIfNotExists acquires the internal RWMutex, checks, optionally inserts, and releases. For callers that have already pre-partitioned their workload so a single goroutine owns a single SyncedMap shard (bucket-affinity bulk insert pattern), calling SetIfNotExists in a loop is correct but wasteful: each call does a separate Lock/Unlock even though the lock is held by the same goroutine across the loop.

This shape is common in sharded-map wrappers. For example, the consumer in teranode block-assembly partitions ~30M tx hashes across 4096-16384 buckets and dispatches one goroutine per non-empty bucket. With the per-call lock pattern, profiling shows ~17 % of CPU inside Lock/Unlock even with no cross-worker contention — purely the per-call acquire/release overhead.

A single-lock bulk variant eliminates that overhead while preserving exact per-key SetIfNotExists semantics.

API

func (m *SyncedMap[K, V]) SetIfNotExistsMulti(keys []K, values []V) []bool
  • Walks both slices under one Lock. For each i, if keys[i] is absent inserts values[i] via the existing setUnlocked (so the configured limit-based eviction still applies); otherwise leaves the existing value untouched.
  • Returns a []bool of length min(len(keys), len(values)); wasInserted[i] is true if newly added, false if the key already existed — mirrors the (V, bool) return shape of single-key SetIfNotExists.

Edge-case behaviour (covered by tests)

Input Behaviour
len(values) < len(keys) Truncates to len(values); trailing keys ignored.
len(keys) < len(values) Truncates to len(keys); trailing values ignored.
Empty / nil input Returns []bool{}.
Duplicate keys within a single call First wins — subsequent duplicates return wasInserted=false.

Test plan

  • go build ./... clean
  • go vet ./... clean
  • go test -race ./... passes (existing + new tests)
  • New tests: TestSyncedMapSetIfNotExistsMulti with five subtests covering the cases above

Notes

  • Purely additive — no existing API changes. Safe to backport to any consumer on the current minor version.
  • The setUnlocked helper already exists and is reused as the per-element insert primitive, so the limit-based eviction logic is shared.

…l insert

SetIfNotExists acquires the write-lock and releases it once per key. For
bucket-affinity bulk inserts where a single goroutine has pre-partitioned
its workload to a single bucket's map, calling SetIfNotExists in a loop
incurs N Lock/Unlock cycles where 1 would suffice. On uncontended paths the
acquire/release pair is still measurable per-call work.

Add SetIfNotExistsMulti(keys, values) that takes the lock once, walks both
slices in parallel, and inserts each pair via setUnlocked when the key is
absent. Returns a []bool of length min(len(keys), len(values)) where
wasInserted[i] mirrors the (V, bool) return of SetIfNotExists for the i-th
pair — true if newly added, false if the key already existed.

Edge cases covered by tests: mismatched slice lengths (truncates to the
shorter), duplicate keys within a single call (first wins), nil/empty
input returns an empty bool slice.

The new function is additive; no existing API changes.
@icellan icellan requested a review from mrz1836 as a code owner May 15, 2026 14:40
@github-actions github-actions Bot added size/M Medium change (51–200 lines) feature Any new significant addition labels May 15, 2026
@sonarqubecloud

Copy link
Copy Markdown

icellan added a commit to bsv-blockchain/teranode that referenced this pull request May 15, 2026
PutMultiBucketTxInpoints previously delegated to per-entry
SyncedMap.SetIfNotExists, which acquires and releases the SyncedMap's
internal RWMutex on every call. In the bucket-affinity hot path each
worker owns one or more buckets exclusively, so the lock is never
contended across workers — but the per-entry Lock/Unlock pair is still
measurable work (profiling attributed ~17 % of every SetIfNotExists call
to Lock/Unlock paths).

go-tx-map upstream now exposes SetIfNotExistsMulti (see
bsv-blockchain/go-tx-map#114) which takes the write-lock once for a
batch of (key, value) pairs and walks both slices, inserting each pair
via setUnlocked iff the key is absent. Per-element wasInserted semantics
match SetIfNotExists exactly.

This change:

  - Bumps go-tx-map to a pseudo-version pinned to commit e509e98cad
    on go-tx-map#114. go.mod carries a multi-line comment explaining
    why we are on a pseudo-version and that it should be replaced with
    the next tagged release (>= v1.3.6) once the upstream PR ships.

  - Switches PutMultiBucketTxInpoints to parallel-slice signature
    (keys []Hash, values []*TxInpoints) so it can pass through to the
    new bulk method without re-packing. Drops the unused
    TxInpointsEntry helper struct.

  - Updates the sole caller bucketShardedGetAndSetIfNotExists to build
    the parallel slices directly while iterating its bucket-local
    indices.

  - Updates the corresponding unit test to use the new signature via
    a small testInpointPair helper (renamed and tidied so the inline
    struct literals don't bloat the assertions).

No semantics change. Existing tests in services/blockassembly/subtreeprocessor
continue to pass under -race; staticcheck and golangci-lint on the package
report zero issues.

@mrz1836 mrz1836 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@mrz1836 mrz1836 merged commit aa3dfe9 into master May 15, 2026
47 checks passed
@github-actions github-actions Bot deleted the feat/set-if-not-exists-multi branch May 15, 2026 15:14
icellan added a commit to bsv-blockchain/teranode that referenced this pull request May 15, 2026
PutMultiBucketTxInpoints previously delegated to per-entry
SyncedMap.SetIfNotExists, which acquires and releases the SyncedMap's
internal RWMutex on every call. In the bucket-affinity hot path each
worker owns one or more buckets exclusively, so the lock is never
contended across workers — but the per-entry Lock/Unlock pair is still
measurable work (profiling attributed ~17 % of every SetIfNotExists call
to Lock/Unlock paths).

go-tx-map upstream now exposes SetIfNotExistsMulti (see
bsv-blockchain/go-tx-map#114) which takes the write-lock once for a
batch of (key, value) pairs and walks both slices, inserting each pair
via setUnlocked iff the key is absent. Per-element wasInserted semantics
match SetIfNotExists exactly.

This change:

  - Bumps go-tx-map to a pseudo-version pinned to commit e509e98cad
    on go-tx-map#114. go.mod carries a multi-line comment explaining
    why we are on a pseudo-version and that it should be replaced with
    the next tagged release (>= v1.3.6) once the upstream PR ships.

  - Switches PutMultiBucketTxInpoints to parallel-slice signature
    (keys []Hash, values []*TxInpoints) so it can pass through to the
    new bulk method without re-packing. Drops the unused
    TxInpointsEntry helper struct.

  - Updates the sole caller bucketShardedGetAndSetIfNotExists to build
    the parallel slices directly while iterating its bucket-local
    indices.

  - Updates the corresponding unit test to use the new signature via
    a small testInpointPair helper (renamed and tidied so the inline
    struct literals don't bloat the assertions).

No semantics change. Existing tests in services/blockassembly/subtreeprocessor
continue to pass under -race; staticcheck and golangci-lint on the package
report zero issues.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Any new significant addition size/M Medium change (51–200 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants