Skip to content

feat(swap-proxy): implement zram dedup storage layer#5

Merged
vi70x4 merged 1 commit into
mainfrom
feat/zram-dedup-layer
Jun 28, 2026
Merged

feat(swap-proxy): implement zram dedup storage layer#5
vi70x4 merged 1 commit into
mainfrom
feat/zram-dedup-layer

Conversation

@vi70x4

@vi70x4 vi70x4 commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add backend slot allocation independent of virtual swap offsets
  • model direct vs deduplicated translations explicitly
  • move dedup table responsibility to metadata/refcounts only
  • release old mappings on overwrite/discard and return zero pages for unmapped reads
  • normalize writes to page size and verify page equality before sharing slots

Scope

This implements the core storage/mapping layer needed by a zram-dedup block frontend. It does not yet register a real /dev/ublkb* device or activate swap through ublk; that remains the next patch.

Verification

  • cargo test -p animaksm-swap-proxy
  • git diff --check
  • previously passed before commit: cargo fmt --all, make check, make test

Ready for AI review.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request refactors the swap-proxy backend by decoupling slot allocation from the deduplication table. It introduces a new SlotAllocator to manage backend page slots independently, and updates the TranslationTable to support both direct and deduplicated mapping kinds. The DedupTable has been simplified to focus on metadata tracking and LRU eviction of singly-referenced entries. Feedback on these changes highlights a critical race condition in remove_reference where concurrent operations could lead to double-frees or slot leaks; using DashMap::remove_if is suggested to resolve this atomically.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +205 to 228
pub fn remove_reference(&self, fp: &Fingerprint) -> ReferenceRemoval {
let should_remove = if let Some(entry) = self.map.get(fp) {
let prev = entry.ref_count.fetch_sub(1, Ordering::Relaxed);
prev <= 1
} else {
false
return ReferenceRemoval::NotTracked;
};

if should_remove {
if let Some((_, entry)) = self.map.remove(fp) {
let slot = entry.backend_offset;
self.free_slots.push(slot);

// Remove from LRU
let mut lru = self.lru.lock();
lru.retain(|(f, _)| f != fp);

return Some(slot);
return ReferenceRemoval::Removed {
backend_offset: slot,
};
}
}

None
ReferenceRemoval::StillReferenced
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

There is a critical race condition in remove_reference when decremented concurrently with add_reference or evict_lru_metadata. If remove_reference decrements the reference count to 0, another thread can concurrently call add_reference and increment it back to 1 before remove_reference actually removes the entry from the map. This results in the entry being removed from the map anyway, leading to a double-free of the backend slot when both translations are released. Additionally, if evict_lru_metadata removes the entry after it was decremented to 0 but before remove_reference calls remove, remove_reference will return StillReferenced instead of Removed, causing a slot leak.

Using remove_if allows us to atomically decrement and conditionally remove the entry, completely eliminating these race conditions.

    pub fn remove_reference(&self, fp: &Fingerprint) -> ReferenceRemoval {
        let mut still_referenced = false;
        let removed = self.map.remove_if(fp, |_, entry| {
            let prev = entry.ref_count.fetch_sub(1, Ordering::Relaxed);
            if prev <= 1 {
                true
            } else {
                still_referenced = true;
                false
            }
        });

        if let Some((_, entry)) = removed {
            let mut lru = self.lru.lock();
            lru.retain(|(f, _)| f != fp);

            ReferenceRemoval::Removed {
                backend_offset: entry.backend_offset,
            }
        } else if still_referenced {
            ReferenceRemoval::StillReferenced
        } else {
            ReferenceRemoval::NotTracked
        }
    }

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.

Addressed in c829f1b.

Changes:

  • remove_reference now uses DashMap::remove_if so the refcount decrement and final removal happen under the same shard lock.
  • evict_lru_metadata now also uses remove_if for the singly-referenced predicate, avoiding a separate check/remove race while not holding the LRU lock across the DashMap write lock.
  • Added test_concurrent_reference_removal_frees_slot_once.

Verification:

  • cargo test -p animaksm-swap-proxy
  • make check

Add backend slot allocation, explicit direct vs deduplicated translations, safe dedup metadata/refcount ownership, and cleanup on overwrite/discard.

Normalize writes to pages, verify duplicate content before sharing backend slots, and fall back to direct mappings when dedup metadata cannot be tracked.

This keeps the implementation scoped to the storage/mapping layer; the ublk block-device frontend remains a follow-up.
@vi70x4 vi70x4 force-pushed the feat/zram-dedup-layer branch from 09d8807 to c829f1b Compare June 28, 2026 00:22
@vi70x4 vi70x4 merged commit 5b47ed3 into main Jun 28, 2026
@vi70x4 vi70x4 deleted the feat/zram-dedup-layer branch June 28, 2026 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants