Skip to content

Implement a conflict-free replicated data type (CRDT) based follower count to eliminate race conditions under concurrent follow/unfollow bursts #757

Description

@Chucks1093

Summary

The current follower count uses a simple integer increment/decrement with no concurrency protection. Under a burst of concurrent follow and unfollow operations for the same creator, lost updates cause the stored count to diverge from the true number of followers. This issue replaces the counter with a G-Counter CRDT per node — each server instance maintains its own increment and decrement registers, and the true count is computed as the sum of all increments minus the sum of all decrements across all nodes at read time.

Scope

1. CRDT counter storage

  • Add a FollowerCounterShard table: creatorWallet, nodeId, increments (BigInt), decrements (BigInt), updatedAt
  • nodeId is a stable identifier for the server instance (read from NODE_ID env var or generated on startup and persisted)
  • Each node only ever increments its own shard — no cross-node writes

2. Follow and unfollow operations

  • On follow: atomically increment the increments column of the local node's shard using a database-level UPDATE ... SET increments = increments + 1 (no read-modify-write)
  • On unfollow: atomically increment the decrements column of the local node's shard
  • Both operations must be idempotent: store a FollowEvent record per (followerWallet, creatorWallet) and skip the shard update if the event already exists in the expected direction

3. Count resolution

  • getFollowerCount(creatorWallet): sum all increments across all shards, subtract sum of all decrements, clamp to 0
  • Count resolution must read from the primary database replica (not a read replica) to avoid stale shard reads
  • Cache the resolved count in Redis with a 10-second TTL; invalidate on any local shard write

4. Shard compaction

  • A nightly compaction job merges all shards for a creator into a single canonical shard, resetting the per-node shards to 0 after recording the compacted total
  • Compaction must be atomic: write the canonical shard and delete per-node shards in a single transaction
  • Compaction skips any creator whose shards were written to in the last 5 minutes to avoid racing with active operations

5. Integration tests

  • Simulate 100 concurrent follows from different wallets — assert the final count equals 100 with no lost updates
  • Simulate 50 concurrent follows and 30 concurrent unfollows — assert the final count equals 20
  • Double-follow from the same wallet — assert count increments only once
  • Run compaction — assert the resolved count before and after compaction is identical
  • Simulate two nodes each recording follows — assert count resolution sums across both shards

Acceptance Criteria

  • Follow and unfollow use atomic shard increments (no read-modify-write)
  • Double-follow from the same wallet is idempotent
  • Count resolution sums all shards and clamps to 0
  • 100 concurrent follows produce a count of exactly 100
  • Nightly compaction merges shards atomically
  • Redis cache invalidated on each local shard write

ETA: 24 hours


Coordinate on Telegram

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third Campaign

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions