Skip to content

feat: Optimize UserEvent decay by refactoring for ForeignKey and GenericForeignKey relationships, and adding reliability safeguards.#3550

Closed
lakshita10341 wants to merge 1 commit intointelowlproject:developfrom
lakshita10341:fix-decay-method
Closed

feat: Optimize UserEvent decay by refactoring for ForeignKey and GenericForeignKey relationships, and adding reliability safeguards.#3550
lakshita10341 wants to merge 1 commit intointelowlproject:developfrom
lakshita10341:fix-decay-method

Conversation

@lakshita10341
Copy link
Copy Markdown
Contributor

Closes #3316

Description

Fixes:

  1. reliability crash fix : Only decrement if reliability > 0. The old == 0 check missed the -1 case, causing bulk_update to write -1 and crash with IntegrityError (DB CHECK constraint).

  2. Shared data_model dedup: Track data models by PK so two events sharing the same row decrement it exactly once per cycle, not twice (previously the last write won, under-decrementing).

  3. Refactored into _decay_with_fk() / _decay_with_gfk() : Cleanly separates the FK path (select_related) from the GFK path (prefetch_related + grouped bulk_update).

  4. exists() guard : Short-circuits early when no events are eligible.

Tests added:

  • test_decay_reliability_zero_does_not_crash : regression for IntegrityError crash
  • test_decay_query_count_is_constant : asserts O(1) bulk queries via CaptureQueriesContext
  • test_decay_shared_data_model_reliability_decrements_correctly : verifies shared rows decrement once

Test results:

test_decay_linear ... ok
test_decay_multiple_events ... ok
test_decay_no_events ... ok
test_decay_reliability_reaches_zero ... ok
test_decay_reliability_zero_does_not_crash ... ok
test_decay_linear ... ok
test_matches ... ok
test_decay_query_count_is_constant ... ok
test_decay_shared_data_model_reliability_decrements_correctly ... ok
test_decay_linear ... ok
test_matches ... ok

----------------------------------------------------------------------
Ran 13 tests in 3.214s

OK

Type of change

  • Bug fix (non-breaking change which fixes an issue).

Checklist

  • I have read and understood the rules about how to Contribute to this project
  • The pull request is for the branch develop
    type inside the archive test_files.zip and you added the default tests for that mimetype in test_classes.py.
  • Please avoid adding new libraries as requirements whenever it is possible. Use new libraries only if strictly needed to solve the issue you are working for. In case of doubt, ask a maintainer permission to use a specific library.
  • Linters (Ruff) gave 0 errors.
  • I have added tests for the feature/bug I solved (see tests folder). All the tests (new and old ones) gave 0 errors.

…ricForeignKey relationships, and adding reliability safeguards.
Copilot AI review requested due to automatic review settings March 25, 2026 20:12
@lakshita10341 lakshita10341 marked this pull request as draft March 25, 2026 20:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors UserEventQuerySet.decay() to avoid N+1 updates by splitting decay logic for ForeignKey vs GenericForeignKey relationships, adding reliability guards, and introducing tests for regressions and query-count stability.

Changes:

  • Refactor decay() into _decay_with_fk() and _decay_with_gfk() using select_related/prefetch_related plus bulk_update.
  • Add safeguards to prevent reliability from decrementing below zero and to deduplicate shared data_model rows per cycle.
  • Add tests covering the prior IntegrityError crash, shared-row behavior, and constant query count.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
api_app/user_events_manager/queryset.py Refactors decay paths (FK vs GFK), adds dedup + reliability safeguards, switches to bulk updates.
tests/api_app/user_events_manager/test_queryset.py Adds regression + query-count + shared-row tests for decay behavior.
Comments suppressed due to low confidence (1)

api_app/user_events_manager/queryset.py:126

  • In the GFK path, models_list is already built with unique instances (you only append when (class, pk) is first seen). The extra unique_models = {m.pk: m for m in models_list}.values() dedup step is redundant and can be removed to simplify the code and avoid extra work.
            for model_class, models_list in data_models_by_class.items():
                unique_models = {m.pk: m for m in models_list}.values()
                model_class.objects.bulk_update(unique_models, ["reliability"], batch_size=1000)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +24 to +26
if not objects.exists():
return 0

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The objects.exists() guard adds an extra DB query on every decay() call and increases the race window between eligibility check and the actual updates. Since both _decay_with_fk() and _decay_with_gfk() already materialize events = list(...) and handle the empty case, consider removing the exists() check and relying on the later if not events: return 0 to avoid the redundant query.

Suggested change
if not objects.exists():
return 0

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +78
with transaction.atomic():
if data_models_by_pk:
type(next(iter(data_models_by_pk.values()))).objects.bulk_update(
data_models_by_pk.values(), ["reliability"]
)
self.model.objects.bulk_update(events, ["decay_times", "next_decay"])
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

These bulk_update() calls go through the default manager (model_class.objects / self.model.objects) and transaction.atomic() uses the default DB. If decay() is invoked on a queryset with a non-default DB alias (e.g., .using('replica')), this can write to the wrong database. Use transaction.atomic(using=self.db) and run bulk updates via the queryset’s DB alias (e.g., self.model._base_manager.using(self.db).bulk_update(...) and model_class._base_manager.using(self.db).bulk_update(...)).

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +78
data_models_by_pk.values(), ["reliability"]
)
self.model.objects.bulk_update(events, ["decay_times", "next_decay"])
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

_decay_with_fk() currently calls bulk_update() without a batch_size, while _decay_with_gfk() uses batch_size=1000. For large decay runs this can generate very large SQL statements (CASE expressions) and hit DB/driver parameter limits. Consider adding a batch_size here as well (and for the FK data_models_by_pk update) to keep query size bounded.

Suggested change
data_models_by_pk.values(), ["reliability"]
)
self.model.objects.bulk_update(events, ["decay_times", "next_decay"])
data_models_by_pk.values(), ["reliability"], batch_size=1000
)
self.model.objects.bulk_update(events, ["decay_times", "next_decay"], batch_size=1000)

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 5, 2026

This pull request has been marked as stale because it has had no activity for 10 days. If you are still working on this, please provide some updates or it will be closed in 5 days.

@github-actions github-actions Bot added the stale label Apr 5, 2026
@github-actions
Copy link
Copy Markdown

This pull request has been closed because it had no updates in 15 days. If you're still working on this fell free to reopen.

@github-actions github-actions Bot closed this Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants