Skip to content

Fix duplicate_service.py empty-store crash and concurrency bugs - #2035

Merged
riteshbonthalakoti merged 1 commit into
riteshbonthalakoti:gssocfrom
Ojas2095:main
Jun 6, 2026
Merged

Fix duplicate_service.py empty-store crash and concurrency bugs#2035
riteshbonthalakoti merged 1 commit into
riteshbonthalakoti:gssocfrom
Ojas2095:main

Conversation

@Ojas2095

@Ojas2095 Ojas2095 commented Jun 6, 2026

Copy link
Copy Markdown

Fixes #1824 by introducing empty-store guard, atomic save_to_disk, and threading.Lock in duplicate_service.py. It also introduces tests to prevent regressions.

Summary by CodeRabbit

  • New Features

    • Duplicate detection service now operates with significantly improved performance through optimized similarity checking.
  • Bug Fixes

    • Enhanced handling when the detection model is unavailable or ticket storage is empty.
    • Improved data persistence using atomic file writes to prevent corruption during system interruptions.

@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

@rhoggs-bot-test-account is attempting to deploy a commit to the ritesh Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR hardens DuplicateService against concurrent access corruption and empty-store crashes, refactors similarity checking to use batched computation with a new return contract, and adds comprehensive mocked unit tests covering all major code paths including thread-safety.

Changes

Thread-safe duplicate detection with atomic persistence

Layer / File(s) Summary
Thread-safety and atomic persistence infrastructure
backend/services/duplicate_service.py
Adds threading, tempfile, torch imports; introduces _lock (in-memory ticket access) and _file_lock (disk writes); wraps model loading and ticket history sync under _lock; rewrites save_to_disk() to use file lock, tolerate corrupted JSON, write atomically via temp file and os.replace; updates add_ticket() to append under _lock.
Batched similarity checking with new return contract
backend/services/duplicate_service.py
Refactors check_duplicate() to return None when unavailable/empty/below-threshold instead of a no-match dict; snapshots _tickets under _lock, encodes query, stacks embeddings, computes cosine similarities in batch via util.pytorch_cos_sim, and returns match dict with similarity_score (replacing per-ticket loop and similarity field).
Mocking, test fixtures, and comprehensive test suite
backend/tests/conftest.py, backend/tests/test_duplicate_service.py, backend/tests/__init__.py, backend/data/case_history_cache.json
conftest.py mocks sentence_transformers with deterministic seeded encode() and provides pytorch_cos_sim helper; dup_svc fixture injects mocked model into DuplicateService. Test suite covers empty-store, exact-match detection, below-threshold non-match, best-match selection, unavailable-service, and concurrent add/check thread-safety. Case history cache provides sample WiFi/VPN issue data.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ritesh-1918/HELPDESK.AI#1825: Directly overlaps with this PR's core refactoring of DuplicateService persistence, locking, and check_duplicate() return contract, plus identical test mocking and fixtures.
  • ritesh-1918/HELPDESK.AI#1147: Adds/adjusts test_duplicate_service.py and conftest.py mocking for sentence_transformers to validate check_duplicate() under vectorized embedding workflow.
  • ritesh-1918/HELPDESK.AI#648: Modifies DuplicateService.check_duplicate() to replace per-ticket similarity looping with batched/vectorized cosine-similarity computation including best-match selection.

Suggested labels

gssoc, gssoc:approved, level:critical, quality:clean, type:bug, type:testing

Suggested reviewers

  • ritesh-1918

Poem

🐰 Locks protect the ticket store,
Atomic writes won't crash anymore,
Batched embeddings fly so fast,
Threads compete and all threads pass,
Tests now guard the replica quest!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: fixing empty-store crashes and concurrency bugs in duplicate_service.py, which aligns with the primary objectives.
Linked Issues check ✅ Passed All objectives from issue #1824 are met: empty-store guard returning None, atomic save_to_disk with temp file and os.replace, threading.Lock protecting _tickets and _loaded, JSONDecodeError handling, and comprehensive unit tests covering all required scenarios.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #1824 requirements: modifications to duplicate_service.py, addition of test infrastructure (conftest.py, test_duplicate_service.py, init.py), and test data (case_history_cache.json) are all in-scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@riteshbonthalakoti
riteshbonthalakoti changed the base branch from main to gssoc June 6, 2026 20:30
@riteshbonthalakoti

Copy link
Copy Markdown
Owner

Hi @Ojas2095! Thanks for the contribution. I have triaged your PR and set it to merge into the gssoc branch.

⚠️ MANDATORY GSSOC ONBOARDING STEPS:
Before your PR points are finalized on the leaderboard, you MUST complete these required steps:

  1. Star this repository: https://github.com/ritesh-1918/HELPDESK.AI (Mandatory)
  2. 👤 Follow the Project Admin: https://github.com/ritesh-1918 (Mandatory)
  3. 💼 Connect on LinkedIn: https://www.linkedin.com/in/ritesh1908/ (Mandatory)

Welcome to the HELPDESK.AI developer family! 🚀💻

@riteshbonthalakoti

Copy link
Copy Markdown
Owner

Superb implementation, @Ojas2095! I've successfully resolved all conflicts in your PR and queued it for merging into gssoc.

⚠️ MANDATORY STEPS FOR LEADERBOARD CREDITS:
To ensure you receive full points, please make sure you have taken 10 seconds to:

Keep up the outstanding work! Let's build together! 🔥

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
backend/services/duplicate_service.py (2)

34-60: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Synchronize load() initialization to avoid first-call races.

Line 34’s guard is unsynchronized, so concurrent first requests can both enter load(), load the model twice, and append cached tickets more than once. This undermines the intended thread-safety hardening.

💡 Suggested fix
 class DuplicateService:
     def __init__(self):
         self.model = None
         self._loaded = False
         self._load_failed = False
+        self._init_lock = threading.Lock()
         # In-memory store: list of (ticket_id, embedding, text)
         self._tickets: list[tuple[str, object, str]] = []

     def load(self):
         """Load the sentence-transformer model and saved tickets."""
-        if self._loaded or self._load_failed:
-            return
+        if self._loaded or self._load_failed:
+            return
+        with self._init_lock:
+            if self._loaded or self._load_failed:
+                return

-        print("[DuplicateService] Loading model...")
-        try:
+            print("[DuplicateService] Loading model...")
+            try:
                 ...
-        except Exception as e:
+            except Exception as e:
                 ...

111-158: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

check_duplicate() return contract now breaks existing request-path consumers.

check_duplicate() now returns None (Line 131/138/158) and renamed similarity to similarity_score (Line 155), but the current backend/main.py flow dereferences dup_result["is_duplicate"] and dup_result["similarity"] directly. This can crash requests when no duplicate exists.

Please either (a) update all callers to handle None + new key, or (b) preserve backward compatibility in this method until caller migration is complete.

🤖 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 `@backend/services/duplicate_service.py` around lines 111 - 158, The
check_duplicate function's changed return contract breaks callers; restore
backward compatibility by ensuring check_duplicate always returns a dict (never
None) with both the old key "similarity" and the new "similarity_score" plus
"is_duplicate" and "duplicate_ticket_id"; when no duplicate is found or the
model is unavailable, return {"is_duplicate": False, "duplicate_ticket_id":
None, "similarity": 0.0, "similarity_score": 0.0} so existing backend/main.py
callers that dereference dup_result["is_duplicate"] and dup_result["similarity"]
keep working while also providing the new similarity_score field for migration.
🧹 Nitpick comments (1)
backend/tests/conftest.py (1)

24-24: ⚡ Quick win

Avoid global sys.modules patch leakage across the whole test suite.

Line 24 mutates global import state for all tests. Prefer scoping this through monkeypatch in a fixture so mocks are reverted automatically per test/session boundary.

🤖 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 `@backend/tests/conftest.py` at line 24, The global assignment
sys.modules["sentence_transformers"] = _mock_st leaks a mock across the entire
test process; replace it with a pytest fixture that uses monkeypatch to scope
the replacement (e.g., def mock_sentence_transformers(monkeypatch):
monkeypatch.setitem(sys.modules, "sentence_transformers", _mock_st)) and
register it either as a normal fixture used by tests or as autouse with the
desired scope (function/session) so the mock is reverted automatically; update
tests to depend on or rely on that fixture instead of the direct global
assignment.
🤖 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 `@backend/tests/conftest.py`:
- Around line 12-15: The current special-case branch "if 'VPN' in text" assigns
the same seed value and therefore identical embeddings for all VPN-containing
inputs; change the seed derivation so it's still deterministic but varies per
input (e.g., incorporate the full text or a hash of the text instead of a fixed
42, or combine 42 with sum(ord(c) for c in text) or the ticket id) so different
VPN tickets produce different embeddings; update the code around the "if 'VPN'
in text'" branch and the variable "seed" used by the mock encoder so embeddings
remain stable yet distinct for different texts.

In `@backend/tests/test_duplicate_service.py`:
- Around line 59-60: The two one-line for loops using the variable threads (for
t in threads: t.start() and for t in threads: t.join()) should be expanded to
multi-line for statements to satisfy Ruff E701; replace each single-line form
with a normal block (for t in threads: then newline and an indented t.start())
and do the same for t.join() so the loops are on separate lines and no multiple
statements appear on one line.

---

Outside diff comments:
In `@backend/services/duplicate_service.py`:
- Around line 111-158: The check_duplicate function's changed return contract
breaks callers; restore backward compatibility by ensuring check_duplicate
always returns a dict (never None) with both the old key "similarity" and the
new "similarity_score" plus "is_duplicate" and "duplicate_ticket_id"; when no
duplicate is found or the model is unavailable, return {"is_duplicate": False,
"duplicate_ticket_id": None, "similarity": 0.0, "similarity_score": 0.0} so
existing backend/main.py callers that dereference dup_result["is_duplicate"] and
dup_result["similarity"] keep working while also providing the new
similarity_score field for migration.

---

Nitpick comments:
In `@backend/tests/conftest.py`:
- Line 24: The global assignment sys.modules["sentence_transformers"] = _mock_st
leaks a mock across the entire test process; replace it with a pytest fixture
that uses monkeypatch to scope the replacement (e.g., def
mock_sentence_transformers(monkeypatch): monkeypatch.setitem(sys.modules,
"sentence_transformers", _mock_st)) and register it either as a normal fixture
used by tests or as autouse with the desired scope (function/session) so the
mock is reverted automatically; update tests to depend on or rely on that
fixture instead of the direct global assignment.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 92fc221a-d279-4622-90c8-6cd8f2c5abed

📥 Commits

Reviewing files that changed from the base of the PR and between da8faf2 and 8827f22.

📒 Files selected for processing (5)
  • backend/data/case_history_cache.json
  • backend/services/duplicate_service.py
  • backend/tests/__init__.py
  • backend/tests/conftest.py
  • backend/tests/test_duplicate_service.py

Comment thread backend/tests/conftest.py
Comment on lines +12 to +15
if 'VPN' in text:
seed = 42
else:
seed = sum(ord(c) for c in text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The encoder mock creates tie scores, so “best-match” behavior is not truly validated.

Line 12-15 maps every VPN-containing text to the same seed/embedding. That makes multiple VPN tickets indistinguishable and can hide regressions in ranking logic.

💡 Suggested fix
 def _fake_encode(text, convert_to_tensor=False):
-    import hashlib, torch
-    if 'VPN' in text:
-        seed = 42
-    else:
-        seed = sum(ord(c) for c in text)
+    import hashlib, torch
+    digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
+    seed = int(digest[:8], 16)
     torch.manual_seed(seed)
     return torch.nn.functional.normalize(torch.randn(1, 384), dim=1).squeeze(0)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if 'VPN' in text:
seed = 42
else:
seed = sum(ord(c) for c in text)
def _fake_encode(text, convert_to_tensor=False):
import hashlib, torch
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
seed = int(digest[:8], 16)
torch.manual_seed(seed)
return torch.nn.functional.normalize(torch.randn(1, 384), dim=1).squeeze(0)
🤖 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 `@backend/tests/conftest.py` around lines 12 - 15, The current special-case
branch "if 'VPN' in text" assigns the same seed value and therefore identical
embeddings for all VPN-containing inputs; change the seed derivation so it's
still deterministic but varies per input (e.g., incorporate the full text or a
hash of the text instead of a fixed 42, or combine 42 with sum(ord(c) for c in
text) or the ticket id) so different VPN tickets produce different embeddings;
update the code around the "if 'VPN' in text'" branch and the variable "seed"
used by the mock encoder so embeddings remain stable yet distinct for different
texts.

Comment on lines +59 to +60
for t in threads: t.start()
for t in threads: t.join()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Split one-line for statements to satisfy Ruff E701.

Line 59 and Line 60 currently use multiple statements on one line; this violates configured lint rules and can fail CI.

💡 Suggested fix
-    for t in threads: t.start()
-    for t in threads: t.join()
+    for t in threads:
+        t.start()
+    for t in threads:
+        t.join()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for t in threads: t.start()
for t in threads: t.join()
for t in threads:
t.start()
for t in threads:
t.join()
🧰 Tools
🪛 Ruff (0.15.15)

[error] 59-59: Multiple statements on one line (colon)

(E701)


[error] 60-60: Multiple statements on one line (colon)

(E701)

🤖 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 `@backend/tests/test_duplicate_service.py` around lines 59 - 60, The two
one-line for loops using the variable threads (for t in threads: t.start() and
for t in threads: t.join()) should be expanded to multi-line for statements to
satisfy Ruff E701; replace each single-line form with a normal block (for t in
threads: then newline and an indented t.start()) and do the same for t.join() so
the loops are on separate lines and no multiple statements appear on one line.

Source: Linters/SAST tools

@riteshbonthalakoti
riteshbonthalakoti merged commit 7241c30 into riteshbonthalakoti:gssoc Jun 6, 2026
9 of 10 checks passed
@Ojas2095

Copy link
Copy Markdown
Author

/claim

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.

[Critical] duplicate_service.py: empty-store RuntimeError crash + concurrent JSON corruption + missing thread lock + zero test coverage

2 participants