Skip to content

[feature] Added generic solution for cache invalidation in config models #1095#1418

Open
pandafy wants to merge 13 commits into
masterfrom
issues/1095-generic-cache-invalidation
Open

[feature] Added generic solution for cache invalidation in config models #1095#1418
pandafy wants to merge 13 commits into
masterfrom
issues/1095-generic-cache-invalidation

Conversation

@pandafy

@pandafy pandafy commented Jul 1, 2026

Copy link
Copy Markdown
Member

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #1095

Blockers

@pandafy pandafy self-assigned this Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a declarative cache invalidation system with CacheDependency and CacheInvalidationMixin, wires it into ConfigConfig, AbstractConfig, and AbstractVpn, and removes legacy save-based invalidation paths from device group and organization settings models. It also splits device-group template handling from cache invalidation, updates tests for checksum and cache behavior, and adjusts a CI install step and admin help text.

Estimated code review effort: 4 (Complex) | ~75 minutes

Suggested labels: enhancement

Suggested reviewers: nemesifier

Sequence Diagram(s)

sequenceDiagram
  participant Django
  participant ConfigConfig
  participant CacheInvalidationMixin
  participant AbstractConfig
  participant AbstractVpn
  Django->>ConfigConfig: ready()
  ConfigConfig->>CacheInvalidationMixin: register cache dependencies
  CacheInvalidationMixin->>AbstractConfig: connect config dependencies
  CacheInvalidationMixin->>AbstractVpn: connect VPN dependencies
Loading

Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ❌ Error The PR includes unrelated changes in subnet_division, CI, PKI tests, and test utilities that do not support the cache invalidation issue. Remove or split out the unrelated workflow, admin, subnet division, PKI, and test utility edits into separate PRs.
General Rules ❌ Error Tracked-field snapshots are never consumed, so later unrelated saves can re-fire invalidations; device/config cache invalidations also still run before commit. Pop per-signal snapshots after comparison, and defer the device post_save/config_deactivated invalidations to transaction.on_commit to avoid stale-cache races.
Description check ❌ Error The description omits the required Description of Changes and Screenshot sections, so it does not follow the repository template. Add a Description of Changes section and a Screenshot section, and fill in the template headings required by this repository.
Features ⚠️ Warning FAIL: the linked issue exists and the PR adds tests, but no user-facing docs mention the new cache-invalidation feature and no docs files were changed. Add a docs/changelog entry describing the declarative cache-invalidation mechanism and which models/signals now invalidate caches.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement a generic related-field cache invalidation mechanism for Config, VPN, and Device controller views as requested in #1095.
Bug Fixes ✅ Passed Code adds generic CacheDependency wiring and deterministic regression tests for device/VPN deletes, device OS/org changes, and deferred commit invalidation; no UI test needed.
Changes ✅ Passed Non-trivial cache invalidation changes are covered by new tests; no UI changes, and removed symbols appear internal with no external references/docs.
Title check ✅ Passed The title uses the required [feature] prefix and clearly describes the cache-invalidation work tied to issue #1095.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issues/1095-generic-cache-invalidation

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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 `@openwisp_controller/config/apps.py`:
- Line 82: The inline comment in apps.py exceeds the repository’s 88-character
flake8 limit and needs to be wrapped. Update the comment near the
DeviceChecksumView cache invalidation logic in the relevant module so it fits
within max-line-length while preserving the same meaning.
- Around line 90-95: The delete-path cache invalidation in CacheDependency is
happening too early because it uses pre_delete with on_commit=False. Update the
dependency in apps.py to use DeviceChecksumView.invalidate_get_device_cache with
post_delete and commit-time invalidation so the cache is cleared only after the
delete transaction is committed.

In `@openwisp_controller/config/base/config.py`:
- Around line 225-240: Defer the checksum invalidation work for the DeviceGroup
and OrganizationConfigSettings CacheDependency entries until after the
transaction commits, because on_commit=False can let the async recomputation run
before the saved context is visible. Update the CacheDependency registrations in
the config.Config-related dependency list to use on_commit=True so
cls._invalidate_configs_in_group and cls._invalidate_configs_in_org run only
after the save is committed.

In `@openwisp_controller/config/base/vpn.py`:
- Around line 351-356: The VPN view cache invalidation in the CacheDependency
registration for config.Vpn is happening too early, before the delete is
committed. Update the signal from pre_delete to post_delete and keep on_commit
handling so cls._invalidate_vpn_view_cache runs only after the transaction
commits, preventing GetVpnView.get_vpn from caching a row that is already
deleted.

In `@openwisp_controller/config/handlers.py`:
- Around line 72-74: The cache invalidation task in the post-save handler is
enqueued too early and may run before the surrounding transaction is committed.
Update the handler that calls tasks.invalidate_devicegroup_cache_change.delay to
wrap the enqueue in transaction.on_commit so the task is scheduled only after a
successful commit. Use the existing post-save callback logic and keep the
instance.id and instance._meta.model_name arguments unchanged.

In `@openwisp_controller/config/tests/test_config.py`:
- Around line 1025-1099: Add test coverage for the default on_commit=True
behavior in TestCacheDependency, since the current tests only exercise
on_commit=False and miss the transaction.on_commit deferral path used by real
CacheDependency wiring. Add a test around a save-triggered dependency using
self.captureOnCommitCallbacks(execute=True) so the callback is actually executed
under TestCase, and verify the target is invoked only after commit; reference
CacheDependency, _connect, and the existing post_save save flow when adding the
new test.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5ff567e8-9b77-4f24-bac9-2d85ca9c32ee

📥 Commits

Reviewing files that changed from the base of the PR and between 8b875e7 and 6a8f4ff.

📒 Files selected for processing (9)
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/tests/test_controller.py
💤 Files with no reviewable changes (2)
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/base/multitenancy.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/apps.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/apps.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/apps.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/apps.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Mark user-facing strings for translation with Django i18n helpers in Django code
Avoid unnecessary blank lines inside function and method bodies
Be careful with authentication, authorization, queryset filtering, serializers, admin behavior, cache invalidation, signals, Celery tasks, and websocket updates in Django code
Preserve validation around templates, VPN/PKI material, SSH credentials, device commands, uploaded files, URLs, and subnet/IP data
Write comments and docstrings only when they explain why code is shaped a certain way, placing them before the relevant code block instead of scattering them inside it

Files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/apps.py
🧠 Learnings (8)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/apps.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/apps.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/apps.py
📚 Learning: 2026-06-07T12:07:08.468Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_admin.py:2335-2335
Timestamp: 2026-06-07T12:07:08.468Z
Learning: In this project’s Python test suite (files under openwisp_controller/**/tests/), don’t require or request prose/inline comments that document the breakdown of query-count changes (e.g., assertions around template/DB query counts in helpers like _verify_template_queries). Treat query-count assertions as volatile implementation details that change frequently; review should focus on whether the test asserts the expected behavior, not on explaining the specific query-count deltas in comments.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:24.608Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/pki/tests/test_api.py:155-155
Timestamp: 2026-06-07T12:07:24.608Z
Learning: When reviewing Python test files in this repository, avoid recommending inline comments that explain or justify `assertNumQueries` (Django query count) expectations. Query counts can change frequently as implementations evolve, and inline explanations add maintenance burden; the expected count should be understandable without added comment blocks.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-25T12:20:18.414Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/base/models.py:571-572
Timestamp: 2026-06-25T12:20:18.414Z
Learning: When writing or reviewing tests that override pagination behavior via OpenWispPagination.paginate_queryset(), patch `view.pagination_page_size` (not `page_size`). The method uses `getattr(view, "pagination_page_size", self.page_size)`, so tests must set the attribute on the view to affect pagination. If the view class does not define `pagination_page_size`, using `unittest.mock.patch(..., create=True)` is intentional and correct because the attribute may not exist until patched.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:25.164Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_config.py:864-865
Timestamp: 2026-06-07T12:07:25.164Z
Learning: When reviewing this repo’s Python test suite, treat changes to the *expected* query count in `assertNumQueries(...)` calls as routine test maintenance. If a PR updates the numeric argument (e.g., in `test_config.py`, `test_api.py`, `test_admin.py`, `test_pki.py`) and the test remains consistent with the feature changes, reviewers should not flag the increased number as a performance regression that requires investigation solely because the count went up; instead, focus on whether the update is intentional and the surrounding test/code changes justify the revised expectation.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-25T12:20:45.387Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/tests/test_api.py:916-932
Timestamp: 2026-06-25T12:20:45.387Z
Learning: When reviewing API pagination behavior in openwisp-controller, assume `OpenWispPagination.paginate_queryset()` allows a per-view page-size override via `getattr(view, "pagination_page_size", self.page_size)` (so `view.pagination_page_size`, if present, should affect pagination). In Python tests, it is valid to patch `pagination_page_size` on a view class even if the attribute isn’t declared on the class by default, by using `unittest.mock.patch.object(..., "pagination_page_size", ..., create=True)` so the override is available for the pagination logic during the test.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_config.py
🪛 ast-grep (0.44.0)
openwisp_controller/config/tests/test_config.py

[warning] 26-26: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("config", "Template")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_controller/config/base/base.py

[warning] 197-197: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model(app_label, model_name)
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

🔇 Additional comments (7)
openwisp_controller/config/tests/test_config.py (1)

2-2: LGTM!

Also applies to: 14-14, 26-26

openwisp_controller/config/tests/test_controller.py (1)

478-489: 🩺 Stability & Availability

Check the cache invalidation path in this test
If vpn.delete() still defers GetVpnView invalidation with transaction.on_commit(), TestCase won’t run that callback automatically; wrap the delete in self.captureOnCommitCallbacks(execute=True) or use a test case that commits transactions.

openwisp_controller/config/base/base.py (1)

9-35: LGTM!

Also applies to: 112-329

openwisp_controller/config/apps.py (1)

5-5: LGTM!

Also applies to: 45-56, 57-89, 96-151, 352-383

openwisp_controller/config/base/config.py (1)

29-35: LGTM!

Also applies to: 49-49, 177-222

openwisp_controller/config/base/vpn.py (1)

36-41: LGTM!

Also applies to: 51-56, 329-350, 357-365

openwisp_controller/config/handlers.py (1)

44-71: LGTM!

Comment thread openwisp_controller/config/apps.py Outdated
Comment thread openwisp_controller/config/apps.py
Comment thread openwisp_controller/config/base/config.py
Comment thread openwisp_controller/config/base/vpn.py
Comment on lines +72 to +74
tasks.invalidate_devicegroup_cache_change.delay(
instance.id, instance._meta.model_name
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Schedule device-group cache invalidation after commit.

This handler is invoked from post-save cache dependencies. Enqueuing the task immediately can let the worker read pre-commit state or run for a transaction that later rolls back. Defer the task enqueue with transaction.on_commit().

Suggested fix
-    tasks.invalidate_devicegroup_cache_change.delay(
-        instance.id, instance._meta.model_name
+    transaction.on_commit(
+        lambda: tasks.invalidate_devicegroup_cache_change.delay(
+            instance.id, instance._meta.model_name
+        )
     )
📝 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
tasks.invalidate_devicegroup_cache_change.delay(
instance.id, instance._meta.model_name
)
transaction.on_commit(
lambda: tasks.invalidate_devicegroup_cache_change.delay(
instance.id, instance._meta.model_name
)
)
🤖 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 `@openwisp_controller/config/handlers.py` around lines 72 - 74, The cache
invalidation task in the post-save handler is enqueued too early and may run
before the surrounding transaction is committed. Update the handler that calls
tasks.invalidate_devicegroup_cache_change.delay to wrap the enqueue in
transaction.on_commit so the task is scheduled only after a successful commit.
Use the existing post-save callback logic and keep the instance.id and
instance._meta.model_name arguments unchanged.

Comment thread openwisp_controller/config/tests/test_config.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@openwisp_controller/config/tests/test_config.py`:
- Around line 578-584: The test is updating cert.revoked via cert.save(), but
the transaction.on_commit cache invalidation callback never runs under TestCase,
so the assertion may not exercise _resolve_cert_dependency() as intended. In
test_config.py, wrap the cert.save() call in
captureOnCommitCallbacks(execute=True) so the deferred callback is executed
before config.refresh_from_db(), and keep the existing assertions around
_resolve_cert_dependency() and the VpnClient deletion path.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e656bc4e-934a-4c2e-b5ae-599ad2e1e2ab

📥 Commits

Reviewing files that changed from the base of the PR and between 6a8f4ff and 2c520fd.

📒 Files selected for processing (2)
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/tests/test_config.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Mark user-facing strings for translation with Django i18n helpers in Django code
Avoid unnecessary blank lines inside function and method bodies
Be careful with authentication, authorization, queryset filtering, serializers, admin behavior, cache invalidation, signals, Celery tasks, and websocket updates in Django code
Preserve validation around templates, VPN/PKI material, SSH credentials, device commands, uploaded files, URLs, and subnet/IP data
Write comments and docstrings only when they explain why code is shaped a certain way, placing them before the relevant code block instead of scattering them inside it

Files:

  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
🧠 Learnings (8)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
📚 Learning: 2026-06-07T12:07:08.468Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_admin.py:2335-2335
Timestamp: 2026-06-07T12:07:08.468Z
Learning: In this project’s Python test suite (files under openwisp_controller/**/tests/), don’t require or request prose/inline comments that document the breakdown of query-count changes (e.g., assertions around template/DB query counts in helpers like _verify_template_queries). Treat query-count assertions as volatile implementation details that change frequently; review should focus on whether the test asserts the expected behavior, not on explaining the specific query-count deltas in comments.

Applied to files:

  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:24.608Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/pki/tests/test_api.py:155-155
Timestamp: 2026-06-07T12:07:24.608Z
Learning: When reviewing Python test files in this repository, avoid recommending inline comments that explain or justify `assertNumQueries` (Django query count) expectations. Query counts can change frequently as implementations evolve, and inline explanations add maintenance burden; the expected count should be understandable without added comment blocks.

Applied to files:

  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-25T12:20:18.414Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/base/models.py:571-572
Timestamp: 2026-06-25T12:20:18.414Z
Learning: When writing or reviewing tests that override pagination behavior via OpenWispPagination.paginate_queryset(), patch `view.pagination_page_size` (not `page_size`). The method uses `getattr(view, "pagination_page_size", self.page_size)`, so tests must set the attribute on the view to affect pagination. If the view class does not define `pagination_page_size`, using `unittest.mock.patch(..., create=True)` is intentional and correct because the attribute may not exist until patched.

Applied to files:

  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:25.164Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_config.py:864-865
Timestamp: 2026-06-07T12:07:25.164Z
Learning: When reviewing this repo’s Python test suite, treat changes to the *expected* query count in `assertNumQueries(...)` calls as routine test maintenance. If a PR updates the numeric argument (e.g., in `test_config.py`, `test_api.py`, `test_admin.py`, `test_pki.py`) and the test remains consistent with the feature changes, reviewers should not flag the increased number as a performance regression that requires investigation solely because the count went up; instead, focus on whether the update is intentional and the surrounding test/code changes justify the revised expectation.

Applied to files:

  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-25T12:20:45.387Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/tests/test_api.py:916-932
Timestamp: 2026-06-25T12:20:45.387Z
Learning: When reviewing API pagination behavior in openwisp-controller, assume `OpenWispPagination.paginate_queryset()` allows a per-view page-size override via `getattr(view, "pagination_page_size", self.page_size)` (so `view.pagination_page_size`, if present, should affect pagination). In Python tests, it is valid to patch `pagination_page_size` on a view class even if the attribute isn’t declared on the class by default, by using `unittest.mock.patch.object(..., "pagination_page_size", ..., create=True)` so the override is available for the pagination logic during the test.

Applied to files:

  • openwisp_controller/config/tests/test_config.py
🔇 Additional comments (4)
openwisp_controller/config/apps.py (3)

91-96: Previously flagged: delete invalidation still runs before commit.

This matches the existing review comment: pre_delete with on_commit=False can clear the device cache before the delete transaction commits, allowing stale cache repopulation before commit.


45-56: LGTM!


57-90: LGTM!

Also applies to: 97-150, 351-382

openwisp_controller/config/tests/test_config.py (1)

1032-1036: LGTM!

Comment thread openwisp_controller/config/tests/test_config.py
@coveralls

coveralls commented Jul 1, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 98.374% (-0.2%) from 98.547% — issues/1095-generic-cache-invalidation into master

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@openwisp_controller/config/base/vpn.py`:
- Around line 1111-1115: The loop in vpn.vpnclient_set.iterator() is causing
per-client N+1 queries because client.config and its render path fetch related
objects repeatedly. Update the queryset used in this block to preload config
plus config__device and the device’s organization/group relations before
iterating, so update_status_if_checksum_changed() can run without extra database
hits while preserving the _config_modified_action 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 89cff386-fcf6-47ad-aa46-3e736cf376f7

📥 Commits

Reviewing files that changed from the base of the PR and between 2c520fd and 6c414c5.

📒 Files selected for processing (6)
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/subnet_division/admin.py
📜 Review details
⏰ Context from checks skipped due to timeout. (11)
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/config.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/config.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/config.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/config.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Mark user-facing strings for translation with Django i18n helpers in Django code
Avoid unnecessary blank lines inside function and method bodies
Be careful with authentication, authorization, queryset filtering, serializers, admin behavior, cache invalidation, signals, Celery tasks, and websocket updates in Django code
Preserve validation around templates, VPN/PKI material, SSH credentials, device commands, uploaded files, URLs, and subnet/IP data
Write comments and docstrings only when they explain why code is shaped a certain way, placing them before the relevant code block instead of scattering them inside it

Files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/config.py
🧠 Learnings (8)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/config.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/config.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/base/config.py
📚 Learning: 2026-06-07T12:07:08.468Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_admin.py:2335-2335
Timestamp: 2026-06-07T12:07:08.468Z
Learning: In this project’s Python test suite (files under openwisp_controller/**/tests/), don’t require or request prose/inline comments that document the breakdown of query-count changes (e.g., assertions around template/DB query counts in helpers like _verify_template_queries). Treat query-count assertions as volatile implementation details that change frequently; review should focus on whether the test asserts the expected behavior, not on explaining the specific query-count deltas in comments.

Applied to files:

  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:24.608Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/pki/tests/test_api.py:155-155
Timestamp: 2026-06-07T12:07:24.608Z
Learning: When reviewing Python test files in this repository, avoid recommending inline comments that explain or justify `assertNumQueries` (Django query count) expectations. Query counts can change frequently as implementations evolve, and inline explanations add maintenance burden; the expected count should be understandable without added comment blocks.

Applied to files:

  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-25T12:20:18.414Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/base/models.py:571-572
Timestamp: 2026-06-25T12:20:18.414Z
Learning: When writing or reviewing tests that override pagination behavior via OpenWispPagination.paginate_queryset(), patch `view.pagination_page_size` (not `page_size`). The method uses `getattr(view, "pagination_page_size", self.page_size)`, so tests must set the attribute on the view to affect pagination. If the view class does not define `pagination_page_size`, using `unittest.mock.patch(..., create=True)` is intentional and correct because the attribute may not exist until patched.

Applied to files:

  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-25T12:20:45.387Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/tests/test_api.py:916-932
Timestamp: 2026-06-25T12:20:45.387Z
Learning: When reviewing API pagination behavior in openwisp-controller, assume `OpenWispPagination.paginate_queryset()` allows a per-view page-size override via `getattr(view, "pagination_page_size", self.page_size)` (so `view.pagination_page_size`, if present, should affect pagination). In Python tests, it is valid to patch `pagination_page_size` on a view class even if the attribute isn’t declared on the class by default, by using `unittest.mock.patch.object(..., "pagination_page_size", ..., create=True)` so the override is available for the pagination logic during the test.

Applied to files:

  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:25.164Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_config.py:864-865
Timestamp: 2026-06-07T12:07:25.164Z
Learning: When reviewing this repo’s Python test suite, treat changes to the *expected* query count in `assertNumQueries(...)` calls as routine test maintenance. If a PR updates the numeric argument (e.g., in `test_config.py`, `test_api.py`, `test_admin.py`, `test_pki.py`) and the test remains consistent with the feature changes, reviewers should not flag the increased number as a performance regression that requires investigation solely because the count went up; instead, focus on whether the update is intentional and the surrounding test/code changes justify the revised expectation.

Applied to files:

  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/test_config.py
🪛 ast-grep (0.44.0)
openwisp_controller/config/tests/test_config.py

[warning] 27-27: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("config", "Template")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

🔇 Additional comments (7)
openwisp_controller/subnet_division/admin.py (1)

28-38: LGTM!

openwisp_controller/config/tests/test_config.py (3)

579-584: 🩺 Stability & Availability

Unaddressed: deferred on_commit callback never executed in this test.

cert.save() schedules the cache-invalidation callback via transaction.on_commit (per CacheDependency), but under plain TestCase this callback never runs unless wrapped in captureOnCommitCallbacks(execute=True). This was already flagged in a previous review round and remains unresolved in this version.

Proposed fix
-        cert.revoked = False
-        cert.save()
+        with self.captureOnCommitCallbacks(execute=True):
+            cert.revoked = False
+            cert.save()

1084-1160: 🩺 Stability & Availability

Still missing coverage for the default on_commit=True deferral path.

All four TestCacheDependency tests pass on_commit=False, so the default deferred-to-transaction.on_commit() behavior used by all real production wiring (Config/Vpn) is still never exercised. This was already raised in a prior review and remains open.

Add at least one test using self.captureOnCommitCallbacks(execute=True) around a save that triggers a dependency with the default on_commit=True.


1014-1073: LGTM!

openwisp_controller/config/tests/test_vpn.py (1)

541-559: LGTM!

openwisp_controller/config/tests/utils.py (1)

180-180: 🎯 Functional Correctness

No follow-up needed. The WireGuard tests that use this helper only assert VPN/client behavior; none rely on the template carrying interface entries.

			> Likely an incorrect or invalid review comment.
openwisp_controller/config/base/config.py (1)

212-240: LGTM!

Comment on lines 1111 to +1115
for client in vpn.vpnclient_set.iterator():
# invalidate cache for device
client.config._send_config_modified_signal(
action="related_template_changed"
)
config = client.config
# keep the historical signal action for this related change
config._config_modified_action = "related_template_changed"
config.update_status_if_checksum_changed()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find where invalidate_clients_cache is wired/called to confirm execution context
rg -nP -C4 'invalidate_clients_cache' --type=py

Repository: openwisp/openwisp-controller

Length of output: 166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and surrounding code to assess the loop, query pattern,
# and whether the recompute path is synchronous or deferred.
file="openwisp_controller/config/base/vpn.py"

echo "== File size =="
wc -l "$file"

echo "== Relevant outline =="
ast-grep outline "$file" --view expanded || true

echo "== Lines around the reported section =="
sed -n '1080,1145p' "$file" | cat -n

echo "== Search for related cache invalidation / recompute wiring =="
rg -n -C 3 'update_status_if_checksum_changed|related_template_changed|invalidate_clients_cache|vpnclient_set|select_related|iterator\(' openwisp_controller -g '*.py'

Repository: openwisp/openwisp-controller

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== vpn.py: save / signal / invalidate path =="
sed -n '240,360p' openwisp_controller/config/base/vpn.py | cat -n
echo
sed -n '430,470p' openwisp_controller/config/base/vpn.py | cat -n
echo
sed -n '1101,1118p' openwisp_controller/config/base/vpn.py | cat -n

echo "== config.py: checksum recompute internals =="
sed -n '690,820p' openwisp_controller/config/base/config.py | cat -n

echo "== signal wiring for vpn modified / cache invalidation =="
rg -n -C 4 'post_save.*Vpn|vpn_modified|invalidate_clients_cache|update_vpn_server_configuration|related_template_changed' openwisp_controller -g '*.py'

Repository: openwisp/openwisp-controller

Length of output: 26786


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Where the VPN cache-invalidation task is triggered =="
rg -n -C 3 'invalidate_vpn_server_devices_cache_change\(' openwisp_controller -g '*.py'

echo
echo "== Config checksum / render path that runs during recompute =="
sed -n '820,980p' openwisp_controller/config/base/config.py | cat -n

echo
echo "== VpnClient model fields (to assess client.config and related selects) =="
sed -n '840,890p' openwisp_controller/config/base/vpn.py | cat -n

Repository: openwisp/openwisp-controller

Length of output: 10554


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Checksum / render path in Config =="
rg -n -C 4 'def checksum|`@property`\s+def checksum|backend_instance|get_context\(' openwisp_controller/config/base/config.py -g '*.py'

echo
echo "== Task trigger / async wiring for VPN client cache invalidation =="
rg -n -C 4 'invalidate_vpn_server_devices_cache_change|vpn_server_modified|shared_task|delay\(' openwisp_controller/config -g '*.py'

Repository: openwisp/openwisp-controller

Length of output: 43166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Device helper methods used by Config.get_context() =="
rg -n -C 4 'def _get_organization__config_settings|def _get_group|def _has_group|def get_context\(' openwisp_controller/config/base -g '*.py'

echo
echo "== Surrounding Device methods =="
sed -n '1,220p' openwisp_controller/config/base/device.py | cat -n

Repository: openwisp/openwisp-controller

Length of output: 15868


Avoid the per-client N+1 here. client.config and the config render path both load related objects inside this loop; preloading config, config__device, and the device’s organization/group relations will cut the extra queries.

🤖 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 `@openwisp_controller/config/base/vpn.py` around lines 1111 - 1115, The loop in
vpn.vpnclient_set.iterator() is causing per-client N+1 queries because
client.config and its render path fetch related objects repeatedly. Update the
queryset used in this block to preload config plus config__device and the
device’s organization/group relations before iterating, so
update_status_if_checksum_changed() can run without extra database hits while
preserving the _config_modified_action assignment.

Source: Coding guidelines

@openwisp-companion

Copy link
Copy Markdown

Test Failures in Query Counts

Hello @pandafy,
(Analysis for commit 6c414c5)

There are multiple test failures related to unexpected query counts:

  1. test_organization_shares_management_ip_address_space: Expected 6 queries, but 7 were executed.
  • Fix: Review the test openwisp_controller/config/tests/test_controller.py around line 1608. The test expects 6 queries, but 7 are being made. Identify the extra query and optimize the code or adjust the test's query count.
  1. test_remove_duplicated_last_ip_no_nplus1_queries: The assertion self.assertEqual(three - one, 2) failed with AssertionError: 6 != 2. This indicates a significant deviation in the number of queries.
  • Fix: Examine the test openwisp_controller/config/tests/test_controller.py around line 1600. The test logic three - one seems to be comparing incorrect values or the underlying code is performing more operations than anticipated.
  1. test_last_ip_management_command_queries: Expected 4 queries, but 10 were executed.
  • Fix: Investigate the test openwisp_controller/config/whois/tests/tests.py around line 351. The test expects 4 queries, but the execution shows 10. Analyze the captured queries to pinpoint the excess database interactions.

These failures suggest that the code being tested is performing more database operations than the tests are configured to expect. You should review the test files mentioned and the corresponding code to identify and resolve the N+1 query issues or optimize the database interactions.

@nemesifier nemesifier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good progress, see my comments below.

Comment thread openwisp_controller/config/apps.py Outdated
),
CacheDependency(
source=self.device_model,
signal="pre_delete",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would move this to post_delete and let it run on commit. With pre_delete plus on_commit=False, the cache is cleared while the row is still visible. A controller request during the delete transaction can repopulate DeviceChecksumView.get_device and leave a deleted device cached after commit. The smallest fix is to use post_delete with the default on_commit=True.

source="config.DeviceGroup",
signal="post_save",
track_fields=["context"],
on_commit=False,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would not enqueue this Celery recomputation before commit. If a group or organization context is saved inside a transaction, the worker can run against the old context and decide the checksums did not change. Please use the default on_commit=True for both this dependency and the OrganizationConfigSettings dependency below.

Comment thread openwisp_controller/config/base/vpn.py Outdated
),
CacheDependency(
source="config.Vpn",
signal="pre_delete",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same issue as the device view cache: this should be post_delete and deferred to commit. Otherwise a request can repopulate GetVpnView.get_vpn between the early invalidation and the transaction commit, leaving the deleted VPN cached.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
openwisp_controller/config/base/vpn.py (1)

366-372: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Defer VPN view-cache invalidation to commit

post_save and the vpn_server_modified path emitted from Vpn.save() both run before the outer transaction commits, so a concurrent get_vpn() can repopulate the cache from the old row. Switch these cache dependencies to on_commit=True; the related-object cascade already runs after commit.

🤖 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 `@openwisp_controller/config/base/vpn.py` around lines 366 - 372, The VPN
view-cache invalidation for the Vpn model is still happening before the
surrounding transaction commits, which allows stale cache repopulation. Update
the CacheDependency registration in Vpn-related cache setup to use
on_commit=True for the post_save path, and ensure the vpn_server_modified flow
from Vpn.save() also defers invalidation until commit; keep the related-object
cascade behavior aligned with the commit-time invalidation.
🤖 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 @.github/workflows/ci.yml:
- Around line 75-76: The CI workaround in the workflow step that installs
django-x509 from the issues/236-renew-signals tarball is using a mutable branch
ref, which can break reproducibility. Update the pip install in the CI job to
pin django-x509 to a specific commit SHA instead of the branch tarball, and keep
the existing TODO/removal tracking tied to the upstream django-x509 release. Use
the workflow step containing the temporary workaround install comment to locate
it.

---

Outside diff comments:
In `@openwisp_controller/config/base/vpn.py`:
- Around line 366-372: The VPN view-cache invalidation for the Vpn model is
still happening before the surrounding transaction commits, which allows stale
cache repopulation. Update the CacheDependency registration in Vpn-related cache
setup to use on_commit=True for the post_save path, and ensure the
vpn_server_modified flow from Vpn.save() also defers invalidation until commit;
keep the related-object cascade behavior aligned with the commit-time
invalidation.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 04131af7-bc5d-49d4-936a-f06ffe009c06

📥 Commits

Reviewing files that changed from the base of the PR and between 457a4b7 and 5dc927e.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/pki/tests/test_api.py
📜 Review details
⏰ Context from checks skipped due to timeout. (9)
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Mark user-facing strings for translation with Django i18n helpers in Django code
Avoid unnecessary blank lines inside function and method bodies
Be careful with authentication, authorization, queryset filtering, serializers, admin behavior, cache invalidation, signals, Celery tasks, and websocket updates in Django code
Preserve validation around templates, VPN/PKI material, SSH credentials, device commands, uploaded files, URLs, and subnet/IP data
Write comments and docstrings only when they explain why code is shaped a certain way, placing them before the relevant code block instead of scattering them inside it

Files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
🧠 Learnings (10)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:08.468Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_admin.py:2335-2335
Timestamp: 2026-06-07T12:07:08.468Z
Learning: In this project’s Python test suite (files under openwisp_controller/**/tests/), don’t require or request prose/inline comments that document the breakdown of query-count changes (e.g., assertions around template/DB query counts in helpers like _verify_template_queries). Treat query-count assertions as volatile implementation details that change frequently; review should focus on whether the test asserts the expected behavior, not on explaining the specific query-count deltas in comments.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:24.608Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/pki/tests/test_api.py:155-155
Timestamp: 2026-06-07T12:07:24.608Z
Learning: When reviewing Python test files in this repository, avoid recommending inline comments that explain or justify `assertNumQueries` (Django query count) expectations. Query counts can change frequently as implementations evolve, and inline explanations add maintenance burden; the expected count should be understandable without added comment blocks.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-25T12:20:18.414Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/base/models.py:571-572
Timestamp: 2026-06-25T12:20:18.414Z
Learning: When writing or reviewing tests that override pagination behavior via OpenWispPagination.paginate_queryset(), patch `view.pagination_page_size` (not `page_size`). The method uses `getattr(view, "pagination_page_size", self.page_size)`, so tests must set the attribute on the view to affect pagination. If the view class does not define `pagination_page_size`, using `unittest.mock.patch(..., create=True)` is intentional and correct because the attribute may not exist until patched.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:25.164Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_config.py:864-865
Timestamp: 2026-06-07T12:07:25.164Z
Learning: When reviewing this repo’s Python test suite, treat changes to the *expected* query count in `assertNumQueries(...)` calls as routine test maintenance. If a PR updates the numeric argument (e.g., in `test_config.py`, `test_api.py`, `test_admin.py`, `test_pki.py`) and the test remains consistent with the feature changes, reviewers should not flag the increased number as a performance regression that requires investigation solely because the count went up; instead, focus on whether the update is intentional and the surrounding test/code changes justify the revised expectation.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-25T12:20:45.387Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/tests/test_api.py:916-932
Timestamp: 2026-06-25T12:20:45.387Z
Learning: When reviewing API pagination behavior in openwisp-controller, assume `OpenWispPagination.paginate_queryset()` allows a per-view page-size override via `getattr(view, "pagination_page_size", self.page_size)` (so `view.pagination_page_size`, if present, should affect pagination). In Python tests, it is valid to patch `pagination_page_size` on a view class even if the attribute isn’t declared on the class by default, by using `unittest.mock.patch.object(..., "pagination_page_size", ..., create=True)` so the override is available for the pagination logic during the test.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-02-24T16:24:55.443Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1233
File: .github/workflows/backport.yml:22-22
Timestamp: 2026-02-24T16:24:55.443Z
Learning: In repositories within the OpenWISP organization, it is acceptable to reference reusable workflows from other OpenWISP-controlled repos using mutable refs (e.g., master) in .github/workflows. This is permissible due to the shared trust boundary within the organization. If applying this pattern, ensure the target repos are under the same organization and maintain awareness of potential breakages from upstream mutable refs; consider pinning to a tagged version for longer-term stability when appropriate.

Applied to files:

  • .github/workflows/ci.yml
📚 Learning: 2026-02-24T16:25:20.080Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1233
File: .github/workflows/backport.yml:35-35
Timestamp: 2026-02-24T16:25:20.080Z
Learning: In .github/workflows/backport.yml, enforce that backport-on-comment triggers only for users with author_association MEMBE R or OWNER (COLLABORATOR excluded), reflecting maintainer feedback. Update the trigger condition to check author_association and restrict to MEMBERS/OWNERS; document rationale and PR `#1233` reference in code comments.

Applied to files:

  • .github/workflows/ci.yml
🔇 Additional comments (9)
openwisp_controller/config/tests/test_config.py (4)

1117-1166: Missing coverage for the default on_commit=True deferral path.

All three tests here pass on_commit=False, so the transaction.on_commit() deferral — the default and the mode used by all production wiring (Config/Vpn) — is never exercised. This was already flagged in a previous review of this file.


1086-1101: LGTM!


1168-1295: LGTM!


1104-1116: 🩺 Stability & Availability

Shared test uid is safe here disconnect() unwinds both the main receiver and the .snapshot pre_save receiver, so this helper does not leak handlers across tests.

			> Likely an incorrect or invalid review comment.
openwisp_controller/config/base/vpn.py (4)

373-378: pre_delete + on_commit=False invalidates the view cache before the row is deleted, allowing a concurrent request to repopulate GetVpnView.get_vpn with the soon-to-be-deleted VPN. Use post_delete with on_commit=True. Already raised in prior review.


1141-1154: vpn.vpnclient_set.iterator() triggers per-client N+1 queries: client.config and the checksum recompute path load related objects inside the loop. Preload config, config__device, and org/group relations. Already raised in prior review.


387-403: 🎯 Functional Correctness

x509_renewed isn’t a django-x509 signal, so this sender check doesn’t apply. Wire the cache invalidation to the actual renewal hook instead.

			> Likely an incorrect or invalid review comment.

52-57: 📐 Maintainability & Code Quality

No MRO issue. Only clean() is shared, and the current order keeps ShareableOrgMixinUniqueName.clean() before BaseConfig.clean(). save, get_context, get_config, and checksum aren’t impacted.

			> Likely an incorrect or invalid review comment.
openwisp_controller/pki/tests/test_api.py (1)

164-164: LGTM!

Also applies to: 292-292

Comment thread .github/workflows/ci.yml
Comment on lines +75 to +76
# TODO: Remove before merging
pip install --no-cache-dir --no-deps --force-reinstall https://github.com/openwisp/django-x509/tarball/issues/236-renew-signals

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Temporary workaround install — ensure it's removed/tracked before merge.

This force-reinstalls django-x509 from a mutable branch tarball (issues/236-renew-signals) to unblock the new x509_renewed-based VPN cache invalidation (see vpn.py's CacheDependency on x509_renewed). The TODO already flags this for removal, but since the branch ref is mutable, CI could silently break if that branch is force-pushed or deleted before the upstream fix is merged/released.

Consider pinning to a specific commit SHA instead of the branch name for reproducibility while this is pending, and track removal of this step against the corresponding django-x509 release.

💡 Suggested pin to commit SHA
-          # TODO: Remove before merging 
-          pip install --no-cache-dir --no-deps --force-reinstall https://github.com/openwisp/django-x509/tarball/issues/236-renew-signals
+          # TODO: Remove once django-x509 issue `#236` fix is released; pinned to a commit for CI reproducibility.
+          pip install --no-cache-dir --no-deps --force-reinstall https://github.com/openwisp/django-x509/archive/<commit-sha>.tar.gz
🤖 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 @.github/workflows/ci.yml around lines 75 - 76, The CI workaround in the
workflow step that installs django-x509 from the issues/236-renew-signals
tarball is using a mutable branch ref, which can break reproducibility. Update
the pip install in the CI job to pin django-x509 to a specific commit SHA
instead of the branch tarball, and keep the existing TODO/removal tracking tied
to the upstream django-x509 release. Use the workflow step containing the
temporary workaround install comment to locate it.

@openwisp-companion

Copy link
Copy Markdown

The CI is failing due to transient infrastructure issues (not related to your code). I have restarted the failed jobs automatically (1/3).

@openwisp-companion

Copy link
Copy Markdown

Test Failures in OpenWISP Controller CI

Hello @pandafy,
(Analysis for commit 5dc927e)

There are test failures in the CI pipeline.

1. Django Test Failures:

The CI logs indicate a failure during the Django test execution. The traceback points to an ImproperlyConfigured error originating from the swapper.load_model function within openwisp_controller/config/base/base.py, specifically when trying to load a model in the build_dispatch_uid method. This suggests an issue with how models are being loaded or referenced, possibly due to a missing or incorrectly configured model in the INSTALLED_APPS or a settings.py issue.

Fix:
Review the INSTALLED_APPS in tests/openwisp2/settings.py and ensure that all necessary apps, especially those related to swapper and the models being loaded, are correctly included and ordered. Check for any circular dependencies or missing configurations that might lead to ImproperlyConfigured errors during application startup.

2. Coverage Warning:

A CoverageWarning: No data was collected. message is present. This typically means that the coverage tool did not instrument or execute any code during the test run.

Fix:
This warning often accompanies test failures. Resolving the primary test failures might also resolve this coverage warning. If it persists after fixing other issues, ensure that the coverage configuration is correctly set up to include all relevant source files.

old_serial_num = ca1.serial_number
path = reverse("pki_api:ca_renew", args=[ca1.pk])
with self.assertNumQueries(4):
with self.assertNumQueries(5):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Number of queries has increased because renewing a CA also invalidates the VPN checksum. In order to find related Vpn, we need to perform another query.

old_serial_num = cert1.serial_number
path = reverse("pki_api:cert_renew", args=[cert1.pk])
with self.assertNumQueries(5):
with self.assertNumQueries(6):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Number of queries has increased because renewing a Cert also invalidates the VPN and Config checksum. In order to find related Vpn, we need to perform another query.

@openwisp-companion

Copy link
Copy Markdown
[
  {"line": 2026, "message": "Could not find pki.Ca!"}
]

ImproperlyConfigured: Could not find pki.Ca!

Hello @pandafy,
(Analysis for commit e1d4b50)

The CI failed because the pki.Ca model could not be found during the Django setup. This typically happens when an app is listed in INSTALLED_APPS but its migrations haven't been applied or the app itself is not correctly configured.

Fix:

Ensure that the openwisp_controller.pki app is correctly installed and that its migrations are applied. If you are using the SAMPLE_APP environment, verify that the sample_pki app is correctly configured in tests/openwisp2/settings.py.

The traceback points to openwisp_controller/config/apps.py, specifically in the connect_cache_dependencies method, which tries to load the pki.Ca model. This suggests an issue with how the pki app is being loaded or accessed, possibly related to the SAMPLE_APP configuration.

The failure occurs during the django.setup() call in tests/manage.py, which is triggered by the runtests.py script. The error message Could not find pki.Ca! indicates that the swapper library, used for swapping models, is unable to locate the Ca model from the pki app.

@openwisp-companion

Copy link
Copy Markdown

CI Failure: Test Failures and Configuration Issues

Hello @pandafy,
(Analysis for commit 5a07dca)

There are two main issues identified in the CI logs:

  1. django.core.exceptions.ImproperlyConfigured: Could not find pki.Cert!: This indicates that the Django application registry is unable to locate the pki.Cert model. This is likely due to an incorrect INSTALLED_APPS configuration or a missing dependency.
  • Fix: Review your settings.py file, specifically the INSTALLED_APPS setting, to ensure that openwisp_controller.pki (or its sample equivalent if SAMPLE_APP is set) is correctly included and that all necessary dependencies for the pki app are installed.
  1. WHOIS/GeoIP Lookup Failures: Multiple WHOIS lookup failed errors suggest issues with the GeoIP service configuration or availability. These errors are related to the OPENWISP_CONTROLLER_WHOIS_GEOIP_ACCOUNT and OPENWISP_CONTROLLER_WHOIS_GEOIP_KEY settings.
  • Fix: Ensure that the OPENWISP_CONTROLLER_WHOIS_GEOIP_ACCOUNT and OPENWISP_CONTROLLER_WHOIS_GEOIP_KEY environment variables are correctly set in your CI environment, or that the corresponding settings in settings.py are properly configured if not using environment variables. If these services are temporarily unavailable, the tests might be failing due to this infrastructure issue.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
openwisp_controller/config/base/vpn.py (1)

366-371: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Defer VPN cache invalidation until commit.

post_save, pre_delete, and vpn_server_modified all clear GetVpnView before commit; another request can repopulate the cache with pre-commit VPN data and leave stale responses after the transaction finishes.

🤖 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 `@openwisp_controller/config/base/vpn.py` around lines 366 - 371, The VPN cache
invalidation hooks are running before the database transaction commits, which
can let stale pre-commit data be cached again by another request. Update the
cache invalidation setup in the Vpn class so the CacheDependency entries for
post_save, pre_delete, and vpn_server_modified defer clearing GetVpnView until
commit by using the existing cls._invalidate_vpn_view_cache target with
on_commit enabled.
🤖 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.

Outside diff comments:
In `@openwisp_controller/config/base/vpn.py`:
- Around line 366-371: The VPN cache invalidation hooks are running before the
database transaction commits, which can let stale pre-commit data be cached
again by another request. Update the cache invalidation setup in the Vpn class
so the CacheDependency entries for post_save, pre_delete, and
vpn_server_modified defer clearing GetVpnView until commit by using the existing
cls._invalidate_vpn_view_cache target with on_commit enabled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c7e5a077-bdc0-4cc0-a2c3-1c3d7bc7cba6

📥 Commits

Reviewing files that changed from the base of the PR and between 457a4b7 and 5a07dca.

📒 Files selected for processing (5)
  • .github/workflows/ci.yml
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/pki/tests/test_api.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
openwisp_controller/config/apps.py (1)

139-149: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Defer these post_delete invalidations to commit. Running them inside the transaction leaves a window where a concurrent request can repopulate DeviceGroupCommonName cache with stale data before the delete commits. The handler only needs organization_id/common_name, so this can safely be deferred.

🤖 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 `@openwisp_controller/config/apps.py` around lines 139 - 149, The post_delete
cache invalidations for the CacheDependency entries using
devicegroup_delete_handler are running too early. Update the CacheDependency
configuration for self.devicegroup_model and self.cert_model to defer the
invalidation until commit by enabling on_commit, so the handler runs after the
transaction is finalized. Keep the change focused on the CacheDependency
definitions in apps.py and preserve the existing handler target.
♻️ Duplicate comments (1)
openwisp_controller/config/base/vpn.py (1)

1147-1160: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

N+1 queries per client still present.

Same structural issue flagged in a previous review (then targeting _send_config_modified_signal, now update_status_if_checksum_changed()): vpn.vpnclient_set.iterator() has no select_related, so client.config and the checksum recompute path will hit the DB per client.

♻️ Proposed fix
-        for client in vpn.vpnclient_set.iterator():
+        for client in vpn.vpnclient_set.select_related(
+            "config", "config__device", "config__device__group"
+        ).iterator():
             config = client.config
             # keep the historical signal action for this related change
             config._config_modified_action = "related_template_changed"
             config.update_status_if_checksum_changed()
🤖 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 `@openwisp_controller/config/base/vpn.py` around lines 1147 - 1160, Repeated
per-client database hits remain in invalidate_clients_cache because
vpn.vpnclient_set.iterator() does not prefetch related config data, so
client.config and update_status_if_checksum_changed() trigger N+1 queries.
Update the queryset used in invalidate_clients_cache to fetch the related config
up front (for example via select_related on the client relation), then keep the
existing loop and _config_modified_action assignment so the checksum/status
update still happens per client without extra queries.
🤖 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.

Outside diff comments:
In `@openwisp_controller/config/apps.py`:
- Around line 139-149: The post_delete cache invalidations for the
CacheDependency entries using devicegroup_delete_handler are running too early.
Update the CacheDependency configuration for self.devicegroup_model and
self.cert_model to defer the invalidation until commit by enabling on_commit, so
the handler runs after the transaction is finalized. Keep the change focused on
the CacheDependency definitions in apps.py and preserve the existing handler
target.

---

Duplicate comments:
In `@openwisp_controller/config/base/vpn.py`:
- Around line 1147-1160: Repeated per-client database hits remain in
invalidate_clients_cache because vpn.vpnclient_set.iterator() does not prefetch
related config data, so client.config and update_status_if_checksum_changed()
trigger N+1 queries. Update the queryset used in invalidate_clients_cache to
fetch the related config up front (for example via select_related on the client
relation), then keep the existing loop and _config_modified_action assignment so
the checksum/status update still happens per client without extra queries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 67c8d211-2a4d-4ef0-aecd-340045ec772f

📥 Commits

Reviewing files that changed from the base of the PR and between 5a07dca and 3931e97.

📒 Files selected for processing (7)
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/tests/test_controller.py
💤 Files with no reviewable changes (1)
  • openwisp_controller/config/base/config.py
📜 Review details
⏰ Context from checks skipped due to timeout. (9)
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.1.0
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Mark user-facing strings for translation with Django i18n helpers in Django code
Avoid unnecessary blank lines inside function and method bodies
Be careful with authentication, authorization, queryset filtering, serializers, admin behavior, cache invalidation, signals, Celery tasks, and websocket updates in Django code
Preserve validation around templates, VPN/PKI material, SSH credentials, device commands, uploaded files, URLs, and subnet/IP data
Write comments and docstrings only when they explain why code is shaped a certain way, placing them before the relevant code block instead of scattering them inside it

Files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/tests/test_config.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework.

Files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/tests/test_config.py
🧠 Learnings (8)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:08.468Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_admin.py:2335-2335
Timestamp: 2026-06-07T12:07:08.468Z
Learning: In this project’s Python test suite (files under openwisp_controller/**/tests/), don’t require or request prose/inline comments that document the breakdown of query-count changes (e.g., assertions around template/DB query counts in helpers like _verify_template_queries). Treat query-count assertions as volatile implementation details that change frequently; review should focus on whether the test asserts the expected behavior, not on explaining the specific query-count deltas in comments.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:24.608Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/pki/tests/test_api.py:155-155
Timestamp: 2026-06-07T12:07:24.608Z
Learning: When reviewing Python test files in this repository, avoid recommending inline comments that explain or justify `assertNumQueries` (Django query count) expectations. Query counts can change frequently as implementations evolve, and inline explanations add maintenance burden; the expected count should be understandable without added comment blocks.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-25T12:20:18.414Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/base/models.py:571-572
Timestamp: 2026-06-25T12:20:18.414Z
Learning: When writing or reviewing tests that override pagination behavior via OpenWispPagination.paginate_queryset(), patch `view.pagination_page_size` (not `page_size`). The method uses `getattr(view, "pagination_page_size", self.page_size)`, so tests must set the attribute on the view to affect pagination. If the view class does not define `pagination_page_size`, using `unittest.mock.patch(..., create=True)` is intentional and correct because the attribute may not exist until patched.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:25.164Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_config.py:864-865
Timestamp: 2026-06-07T12:07:25.164Z
Learning: When reviewing this repo’s Python test suite, treat changes to the *expected* query count in `assertNumQueries(...)` calls as routine test maintenance. If a PR updates the numeric argument (e.g., in `test_config.py`, `test_api.py`, `test_admin.py`, `test_pki.py`) and the test remains consistent with the feature changes, reviewers should not flag the increased number as a performance regression that requires investigation solely because the count went up; instead, focus on whether the update is intentional and the surrounding test/code changes justify the revised expectation.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-25T12:20:45.387Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/tests/test_api.py:916-932
Timestamp: 2026-06-25T12:20:45.387Z
Learning: When reviewing API pagination behavior in openwisp-controller, assume `OpenWispPagination.paginate_queryset()` allows a per-view page-size override via `getattr(view, "pagination_page_size", self.page_size)` (so `view.pagination_page_size`, if present, should affect pagination). In Python tests, it is valid to patch `pagination_page_size` on a view class even if the attribute isn’t declared on the class by default, by using `unittest.mock.patch.object(..., "pagination_page_size", ..., create=True)` so the override is available for the pagination logic during the test.

Applied to files:

  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
🔇 Additional comments (10)
openwisp_controller/config/tests/test_config.py (5)

571-587: Still missing captureOnCommitCallbacks — assertion doesn't exercise _resolve_cert_dependency().

Only the comments changed here; cert.save() still isn't wrapped in self.captureOnCommitCallbacks(execute=True). Since TestConfig extends TestCase, the transaction.on_commit callback that would invoke _resolve_cert_dependency() never fires, so this test passes trivially regardless of whether the "returns early because VpnClient was deleted" logic is correct. This was already raised in a previous review of this test.


932-975: LGTM!


1059-1197: LGTM!


1365-1391: LGTM!


1207-1211: 🎯 Functional Correctness

No issue here Each test method calls _connect() only once, so the fixed dispatch_uid does not create a collision in this class.

			> Likely an incorrect or invalid review comment.
openwisp_controller/config/base/base.py (1)

5-50: LGTM!

Also applies to: 263-283, 341-355, 377-387, 407-415

openwisp_controller/config/apps.py (1)

45-155: LGTM!

Also applies to: 168-253, 356-387

openwisp_controller/config/base/vpn.py (1)

340-410: LGTM!

openwisp_controller/config/tests/test_api.py (1)

6-6: LGTM!

Also applies to: 20-20, 1492-1517

openwisp_controller/config/tests/test_controller.py (1)

187-188: LGTM!

Also applies to: 479-491

@pandafy

pandafy commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai summary

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Summary regeneration triggered.

@coderabbitai coderabbitai Bot added bug and removed enhancement labels Jul 3, 2026
@pandafy pandafy force-pushed the issues/1095-generic-cache-invalidation branch from 86f5e0c to d32576a Compare July 3, 2026 18:12
@coderabbitai coderabbitai Bot added enhancement and removed bug labels Jul 3, 2026
@pandafy pandafy marked this pull request as ready for review July 3, 2026 18:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 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 `@openwisp_controller/config/apps.py`:
- Around line 139-149: The post_delete cache invalidations for the device group
and cert dependencies are running before the transaction commits, which can race
with concurrent reads. Update the CacheDependency entries in apps.py to use
on_commit for the delete handlers, and adjust devicegroup_delete_handler so it
receives the needed snapshot data (organization_id, common_name, and model name)
instead of relying on the soon-to-be-deleted object. Make sure the delete path
still targets devicegroup_delete_handler and defers the invalidation until
commit.
- Around line 84-108: The cache invalidations wired in `CacheDependency` for
`DeviceChecksumView.invalidate_get_device_cache` and
`DeviceChecksumView.invalidate_get_device_cache_on_config_deactivated` should
run after transaction commit instead of immediately. Update the
`CacheDependency` entries for `post_save` and `config_deactivated` in the app
config setup so they defer to commit consistently with the existing
`post_delete` handling, preventing stale `DeviceChecksumView` data from being
repopulated before the transaction is finalized.

In `@openwisp_controller/config/base/base.py`:
- Around line 263-365: The tracked-field snapshot is being reused across later
saves, so `_snapshot_handler` should consume and clear the stored snapshot after
each post-save comparison. Update the snapshot lifecycle around
`_snapshot_handler`, `_tracked_fields_changed`, and `_should_skip` so a fresh
snapshot is taken per save and any old entry in
`instance.__dict__[self._SNAPSHOT_ATTR]` for `self._uid` is removed once it has
been evaluated. This prevents stale snapshot objects from affecting unrelated
future `save(update_fields=...)` calls.

In `@openwisp_controller/config/base/vpn.py`:
- Around line 367-391: Update the CacheDependency entries in Vpn view-cache
invalidation so that both the post_save path and the vpn_server_modified signal
defer invalidation until transaction commit. In the Vpn cache dependency setup,
change the on_commit behavior for cls._invalidate_vpn_view_cache so
GetVpnView.get_vpn is not invalidated/repopulated before the transaction is
committed.

In `@openwisp_controller/config/tests/test_vpn.py`:
- Around line 573-600: The two renew-cascade tests duplicate the same
VPN/template/device setup and initial config checksum assertions, so factor that
shared logic into a small helper in test_vpn.py and keep each test focused on
its specific action (`vpn.ca.renew()` vs `vpn.cert.renew()`). Use the existing
test methods `test_ca_renew_cascades_to_client_config` and
`test_cert_renew_cascades_to_client_config`, plus the helper builders like
`_create_vpn`, `_create_template`, and `_create_device_config`, to reduce
repetition while preserving the distinct post-renew assertions and signal check.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bfba3d59-70c2-4c62-8109-c22c977554d8

📥 Commits

Reviewing files that changed from the base of the PR and between 86f5e0c and d32576a.

📒 Files selected for processing (18)
  • .github/workflows/ci.yml
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/subnet_division/rule_types/vpn.py
  • openwisp_controller/subnet_division/tests/test_models.py
💤 Files with no reviewable changes (2)
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/base/device_group.py
📜 Review details
⏰ Context from checks skipped due to timeout. (11)
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Mark user-facing strings for translation with Django i18n helpers in Django code
Avoid unnecessary blank lines inside function and method bodies
Be careful with authentication, authorization, queryset filtering, serializers, admin behavior, cache invalidation, signals, Celery tasks, and websocket updates in Django code
Preserve validation around templates, VPN/PKI material, SSH credentials, device commands, uploaded files, URLs, and subnet/IP data
Write comments and docstrings only when they explain why code is shaped a certain way, placing them before the relevant code block instead of scattering them inside it

Files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/subnet_division/rule_types/vpn.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework.

Files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/subnet_division/rule_types/vpn.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
🧠 Learnings (10)
📚 Learning: 2026-02-24T16:24:55.443Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1233
File: .github/workflows/backport.yml:22-22
Timestamp: 2026-02-24T16:24:55.443Z
Learning: In repositories within the OpenWISP organization, it is acceptable to reference reusable workflows from other OpenWISP-controlled repos using mutable refs (e.g., master) in .github/workflows. This is permissible due to the shared trust boundary within the organization. If applying this pattern, ensure the target repos are under the same organization and maintain awareness of potential breakages from upstream mutable refs; consider pinning to a tagged version for longer-term stability when appropriate.

Applied to files:

  • .github/workflows/ci.yml
📚 Learning: 2026-02-24T16:25:20.080Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1233
File: .github/workflows/backport.yml:35-35
Timestamp: 2026-02-24T16:25:20.080Z
Learning: In .github/workflows/backport.yml, enforce that backport-on-comment triggers only for users with author_association MEMBE R or OWNER (COLLABORATOR excluded), reflecting maintainer feedback. Update the trigger condition to check author_association and restrict to MEMBERS/OWNERS; document rationale and PR `#1233` reference in code comments.

Applied to files:

  • .github/workflows/ci.yml
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/subnet_division/rule_types/vpn.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/subnet_division/rule_types/vpn.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/subnet_division/admin.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/subnet_division/rule_types/vpn.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/base.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/base/vpn.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/config/apps.py
📚 Learning: 2026-06-07T12:07:08.468Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_admin.py:2335-2335
Timestamp: 2026-06-07T12:07:08.468Z
Learning: In this project’s Python test suite (files under openwisp_controller/**/tests/), don’t require or request prose/inline comments that document the breakdown of query-count changes (e.g., assertions around template/DB query counts in helpers like _verify_template_queries). Treat query-count assertions as volatile implementation details that change frequently; review should focus on whether the test asserts the expected behavior, not on explaining the specific query-count deltas in comments.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:24.608Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/pki/tests/test_api.py:155-155
Timestamp: 2026-06-07T12:07:24.608Z
Learning: When reviewing Python test files in this repository, avoid recommending inline comments that explain or justify `assertNumQueries` (Django query count) expectations. Query counts can change frequently as implementations evolve, and inline explanations add maintenance burden; the expected count should be understandable without added comment blocks.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-25T12:20:18.414Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/base/models.py:571-572
Timestamp: 2026-06-25T12:20:18.414Z
Learning: When writing or reviewing tests that override pagination behavior via OpenWispPagination.paginate_queryset(), patch `view.pagination_page_size` (not `page_size`). The method uses `getattr(view, "pagination_page_size", self.page_size)`, so tests must set the attribute on the view to affect pagination. If the view class does not define `pagination_page_size`, using `unittest.mock.patch(..., create=True)` is intentional and correct because the attribute may not exist until patched.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-07T12:07:25.164Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1378
File: openwisp_controller/config/tests/test_config.py:864-865
Timestamp: 2026-06-07T12:07:25.164Z
Learning: When reviewing this repo’s Python test suite, treat changes to the *expected* query count in `assertNumQueries(...)` calls as routine test maintenance. If a PR updates the numeric argument (e.g., in `test_config.py`, `test_api.py`, `test_admin.py`, `test_pki.py`) and the test remains consistent with the feature changes, reviewers should not flag the increased number as a performance regression that requires investigation solely because the count went up; instead, focus on whether the update is intentional and the surrounding test/code changes justify the revised expectation.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/test_config.py
📚 Learning: 2026-06-25T12:20:45.387Z
Learnt from: dee077
Repo: openwisp/openwisp-controller PR: 1395
File: openwisp_controller/connection/tests/test_api.py:916-932
Timestamp: 2026-06-25T12:20:45.387Z
Learning: When reviewing API pagination behavior in openwisp-controller, assume `OpenWispPagination.paginate_queryset()` allows a per-view page-size override via `getattr(view, "pagination_page_size", self.page_size)` (so `view.pagination_page_size`, if present, should affect pagination). In Python tests, it is valid to patch `pagination_page_size` on a view class even if the attribute isn’t declared on the class by default, by using `unittest.mock.patch.object(..., "pagination_page_size", ..., create=True)` so the override is available for the pagination logic during the test.

Applied to files:

  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/tests/test_controller.py
  • openwisp_controller/config/tests/utils.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/config/tests/test_vpn.py
  • openwisp_controller/config/tests/test_config.py
🪛 ast-grep (0.44.0)
openwisp_controller/subnet_division/tests/test_models.py

[warning] 26-26: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("config", "VpnClient")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_controller/config/base/config.py

[warning] 241-241: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("openwisp_users", "Organization")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_controller/config/base/base.py

[warning] 212-212: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model(app_label, model_name)
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

openwisp_controller/config/tests/test_config.py

[warning] 27-27: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("config", "OrganizationConfigSettings")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)


[warning] 28-28: Loading a Keras model from an untrusted file can execute arbitrary code via Lambda layers or custom objects. Load only trusted models and avoid deserializing custom objects from untrusted sources.
Context: load_model("config", "Template")
Note: [CWE-502] Deserialization of Untrusted Data.

(keras-load-model-python)

🔇 Additional comments (17)
.github/workflows/ci.yml (1)

75-76: Same concern already raised.

This TODO workaround using a mutable branch tarball was already flagged in a previous review requesting a pinned commit SHA for reproducibility.

openwisp_controller/subnet_division/admin.py (1)

28-38: LGTM!

openwisp_controller/subnet_division/rule_types/vpn.py (1)

44-58: LGTM! Reordering correctly ensures instance.ip is persisted before super().post_provision_handler() caches the checksum, resolving the ip_address_<vpn_pk> template variable at the right time — validated by the new regression test in test_models.py.

openwisp_controller/subnet_division/tests/test_models.py (1)

26-26: LGTM! Regression test correctly exercises the post_provision_handler ordering fix by asserting the ip_address_<vpn_pk> context resolves and that checksum_db matches the freshly computed checksum after a DB refresh.

Also applies to: 66-94

openwisp_controller/config/tests/test_device.py (1)

606-663: LGTM! Moving these cache-invalidation tests into TestTransactionDevice (a TransactionTestCase) is necessary since CacheDependency defers invalidation via transaction.on_commit, which never fires under the default TestCase transaction wrapping.

openwisp_controller/config/tests/test_config.py (1)

571-975: LGTM!

Also applies to: 1059-1391

openwisp_controller/config/tests/test_api.py (1)

1492-1517: LGTM!

openwisp_controller/config/tests/test_controller.py (1)

187-188: LGTM!

Also applies to: 479-491

openwisp_controller/config/tests/test_vpn.py (1)

519-557: LGTM!

Also applies to: 601-2008

openwisp_controller/pki/tests/test_api.py (1)

164-164: LGTM!

Based on learnings, "treat changes to the expected query count in assertNumQueries(...) calls as routine test maintenance" and avoid requiring justification comments for such deltas.

Also applies to: 292-292

Source: Learnings

openwisp_controller/config/tests/utils.py (1)

174-181: 🎯 Functional Correctness

No issue with the shared WireGuard test fixture config override

openwisp_controller/config/handlers.py (2)

72-74: Duplicate: schedule this cache invalidation after commit.

The direct .delay() call can run before the surrounding transaction commits; this was already flagged in a previous review comment for this handler.


44-60: LGTM!

Also applies to: 62-71

openwisp_controller/config/base/base.py (1)

5-51: LGTM!

Also applies to: 127-262, 368-416

openwisp_controller/config/apps.py (1)

5-5: LGTM!

Also applies to: 50-83, 110-138, 152-155, 356-387

openwisp_controller/config/base/config.py (1)

29-35: LGTM!

Also applies to: 49-49, 177-299

openwisp_controller/config/base/vpn.py (1)

18-18: LGTM!

Also applies to: 37-58, 331-360, 393-410

Comment on lines +84 to +108
CacheDependency(
source=self.device_model,
signal="post_save",
on_create=True,
on_commit=False,
target=DeviceChecksumView.invalidate_get_device_cache,
),
# Deferred to commit so a concurrent request cannot repopulate the
# cache with a device that is about to be (or was just) deleted.
# ``post_delete`` + ``_resolve_pk_snapshot`` because Django clears
# ``instance.pk`` on deleted instances before the deferred
# on_commit callback runs (see ``_resolve_pk_snapshot``).
CacheDependency(
source=self.device_model,
signal="post_delete",
resolve=_resolve_pk_snapshot,
target=DeviceChecksumView.invalidate_get_device_cache,
),
CacheDependency(
signal_obj=config_deactivated,
name="config_deactivated",
on_commit=False,
target=(
DeviceChecksumView.invalidate_get_device_cache_on_config_deactivated
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant definitions and inspect the surrounding code.
git ls-files | rg '(^|/)openwisp_controller/config/apps\.py$|(^|/)cache.*\.py$|(^|/)dependencies?\.py$|(^|/)signals?\.py$'
printf '\n--- apps.py outline ---\n'
ast-grep outline openwisp_controller/config/apps.py --view expanded || true
printf '\n--- apps.py relevant section ---\n'
cat -n openwisp_controller/config/apps.py | sed -n '70,170p'
printf '\n--- search CacheDependency definition/usages ---\n'
rg -n "class CacheDependency|def .*invalidate|get_device_cache|on_commit" openwisp_controller -S

Repository: openwisp/openwisp-controller

Length of output: 16607


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# If the first search was incomplete, inspect the cache dependency implementation.
fd -a "cache" openwisp_controller || true
rg -n "class CacheDependency|on_commit|post_delete|resolve=.*_resolve_pk_snapshot|invalidate_get_device_cache_on_config_deactivated" . -S

Repository: openwisp/openwisp-controller

Length of output: 10895


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the cache dependency implementation if it exists in-tree.
candidate=$(rg -l "class CacheDependency" . | head -n 1 || true)
if [ -n "${candidate}" ]; then
  printf '--- candidate: %s ---\n' "$candidate"
  cat -n "$candidate" | sed -n '1,260p'
fi

Repository: openwisp/openwisp-controller

Length of output: 257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrow read of the target file around the comment locations.
cat -n openwisp_controller/config/apps.py | sed -n '80,160p'

Repository: openwisp/openwisp-controller

Length of output: 4138


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and the CacheDependency implementation.
printf '\n--- openwisp_controller/config/apps.py (80-160) ---\n'
cat -n openwisp_controller/config/apps.py | sed -n '80,160p'

printf '\n--- CacheDependency references ---\n'
rg -n "class CacheDependency|on_commit|post_delete|_resolve_pk_snapshot|invalidate_get_device_cache_on_config_deactivated|invalidate_get_device_cache" openwisp_controller -S

Repository: openwisp/openwisp-controller

Length of output: 15389


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- openwisp_controller/config/apps.py (80-160) ---\n'
cat -n openwisp_controller/config/apps.py | sed -n '80,160p'

printf '\n--- CacheDependency references ---\n'
rg -n "class CacheDependency|on_commit|post_delete|_resolve_pk_snapshot|invalidate_get_device_cache_on_config_deactivated|invalidate_get_device_cache" openwisp_controller -S

Repository: openwisp/openwisp-controller

Length of output: 15389


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the cache dependency implementation and the device cache invalidation path.
printf '\n--- openwisp_controller/config/base/base.py (class CacheDependency) ---\n'
cat -n openwisp_controller/config/base/base.py | sed -n '39,230p'

printf '\n--- openwisp_controller/config/controller/views.py (device cache methods) ---\n'
cat -n openwisp_controller/config/controller/views.py | sed -n '170,220p'

printf '\n--- openwisp_controller/config/tests/test_api.py relevant note ---\n'
cat -n openwisp_controller/config/tests/test_api.py | sed -n '1488,1510p'

Repository: openwisp/openwisp-controller

Length of output: 12047


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether there are tests or comments discussing commit timing for similar invalidations.
rg -n "on_commit=False|on_commit=True|concurrent request|repopulate|stale cache|invalidate_get_device_cache" openwisp_controller/config/tests openwisp_controller/config/base openwisp_controller/config/controller -S

Repository: openwisp/openwisp-controller

Length of output: 1494


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the exact test cases that mention on_commit=False in config tests.
cat -n openwisp_controller/config/tests/test_config.py | sed -n '1200,1265p'

Repository: openwisp/openwisp-controller

Length of output: 2999


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the deletion/invalidation helper to understand whether immediate invalidation is safe.
cat -n openwisp_controller/config/tasks.py | sed -n '190,235p'

Repository: openwisp/openwisp-controller

Length of output: 1307


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for explicit transaction.atomic() contexts around device/config save operations.
rg -n "transaction\.atomic\(|atomic\(" openwisp_controller/config -S

Repository: openwisp/openwisp-controller

Length of output: 1087


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect where config_deactivated is emitted and what transaction timing it uses.
rg -n "config_deactivated" openwisp_controller/config -S
printf '\n--- config handlers around relevant lines ---\n'
cat -n openwisp_controller/config/handlers.py | sed -n '1,170p'

Repository: openwisp/openwisp-controller

Length of output: 9626


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Trace the config_deactivated signal and its emission path.
rg -n "config_deactivated|deactivated" openwisp_controller/config -S

Repository: openwisp/openwisp-controller

Length of output: 29130


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the analogous VPN cache dependencies and surrounding commentary.
cat -n openwisp_controller/config/base/vpn.py | sed -n '350,400p'

# Check the CacheDependency apply/connect logic to see when immediate invalidation runs.
cat -n openwisp_controller/config/base/base.py | sed -n '340,420p'

Repository: openwisp/openwisp-controller

Length of output: 6158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect any tests exercising immediate invalidation timing for device/config save paths.
rg -n "DeviceChecksumView|get_device.*invalidate|cache invalidat.*commit|on_commit=False" openwisp_controller/config/tests/test_controller.py openwisp_controller/config/tests/test_device.py openwisp_controller/config/tests/test_config.py -S

Repository: openwisp/openwisp-controller

Length of output: 1161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect controller tests around DeviceChecksumView caching and invalidation timing.
cat -n openwisp_controller/config/tests/test_controller.py | sed -n '120,210p'
printf '\n---\n'
cat -n openwisp_controller/config/tests/test_controller.py | sed -n '1548,1615p'

Repository: openwisp/openwisp-controller

Length of output: 8337


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the config tests that intentionally use on_commit=False.
cat -n openwisp_controller/config/tests/test_config.py | sed -n '1208,1260p'

Repository: openwisp/openwisp-controller

Length of output: 2427


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect save/deactivation paths that emit the relevant signals inside transactions.
cat -n openwisp_controller/config/base/device.py | sed -n '180,225p'
printf '\n---\n'
cat -n openwisp_controller/config/base/config.py | sed -n '900,980p'
printf '\n---\n'
cat -n openwisp_controller/config/base/config.py | sed -n '430,500p'

Repository: openwisp/openwisp-controller

Length of output: 9952


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the device-cache tests assert immediate invalidation after save/deactivation.
rg -n "invalidate_get_device_cache|get_device\(\)|config_deactivated|deactivated" openwisp_controller/config/tests/test_controller.py -S

Repository: openwisp/openwisp-controller

Length of output: 1800


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect when the config_deactivated signal is emitted relative to the save transaction.
cat -n openwisp_controller/config/base/config.py | sed -n '680,720p'
printf '\n---\n'
cat -n openwisp_controller/config/base/config.py | sed -n '860,885p'

# Inspect any test explicitly covering cache invalidation on config deactivation.
cat -n openwisp_controller/config/tests/test_config.py | sed -n '250,285p'

Repository: openwisp/openwisp-controller

Length of output: 5095


Defer these controller-view invalidations to commit. on_commit=False on post_save and config_deactivated can let a concurrent request repopulate DeviceChecksumView with stale device/config data before the transaction commits, and that stale cache can linger afterward.

Proposed fix
             CacheDependency(
                 source=self.device_model,
                 signal="post_save",
                 on_create=True,
-                on_commit=False,
                 target=DeviceChecksumView.invalidate_get_device_cache,
             ),
@@
             CacheDependency(
                 signal_obj=config_deactivated,
                 name="config_deactivated",
-                on_commit=False,
                 target=(
                     DeviceChecksumView.invalidate_get_device_cache_on_config_deactivated
                 ),
📝 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
CacheDependency(
source=self.device_model,
signal="post_save",
on_create=True,
on_commit=False,
target=DeviceChecksumView.invalidate_get_device_cache,
),
# Deferred to commit so a concurrent request cannot repopulate the
# cache with a device that is about to be (or was just) deleted.
# ``post_delete`` + ``_resolve_pk_snapshot`` because Django clears
# ``instance.pk`` on deleted instances before the deferred
# on_commit callback runs (see ``_resolve_pk_snapshot``).
CacheDependency(
source=self.device_model,
signal="post_delete",
resolve=_resolve_pk_snapshot,
target=DeviceChecksumView.invalidate_get_device_cache,
),
CacheDependency(
signal_obj=config_deactivated,
name="config_deactivated",
on_commit=False,
target=(
DeviceChecksumView.invalidate_get_device_cache_on_config_deactivated
),
CacheDependency(
source=self.device_model,
signal="post_save",
on_create=True,
target=DeviceChecksumView.invalidate_get_device_cache,
),
# Deferred to commit so a concurrent request cannot repopulate the
# cache with a device that is about to be (or was just) deleted.
# ``post_delete`` + ``_resolve_pk_snapshot`` because Django clears
# ``instance.pk`` on deleted instances before the deferred
# on_commit callback runs (see ``_resolve_pk_snapshot``).
CacheDependency(
source=self.device_model,
signal="post_delete",
resolve=_resolve_pk_snapshot,
target=DeviceChecksumView.invalidate_get_device_cache,
),
CacheDependency(
signal_obj=config_deactivated,
name="config_deactivated",
target=(
DeviceChecksumView.invalidate_get_device_cache_on_config_deactivated
),
🤖 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 `@openwisp_controller/config/apps.py` around lines 84 - 108, The cache
invalidations wired in `CacheDependency` for
`DeviceChecksumView.invalidate_get_device_cache` and
`DeviceChecksumView.invalidate_get_device_cache_on_config_deactivated` should
run after transaction commit instead of immediately. Update the
`CacheDependency` entries for `post_save` and `config_deactivated` in the app
config setup so they defer to commit consistently with the existing
`post_delete` handling, preventing stale `DeviceChecksumView` data from being
repopulated before the transaction is finalized.

Comment on lines +139 to +149
CacheDependency(
source=self.devicegroup_model,
signal="post_delete",
on_commit=False,
target=devicegroup_delete_handler,
),
CacheDependency(
source=self.cert_model,
signal="post_delete",
on_commit=False,
target=devicegroup_delete_handler,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== apps outline ==\n'
ast-grep outline openwisp_controller/config/apps.py --view expanded || true

printf '\n== relevant slice ==\n'
sed -n '110,190p' openwisp_controller/config/apps.py

printf '\n== search cache dependency / handler ==\n'
rg -n "devicegroup_delete_handler|CacheDependency|post_delete|on_commit|common_name" openwisp_controller -S

Repository: openwisp/openwisp-controller

Length of output: 28934


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== files mentioning devicegroup_delete_handler ==\n'
rg -n "devicegroup_delete_handler" -S .

printf '\n== files mentioning DeviceGroupCommonName ==\n'
rg -n "DeviceGroupCommonName" -S openwisp_controller

printf '\n== files mentioning CacheDependency\(.*post_delete ==\n'
rg -n "CacheDependency\\(|signal=\"post_delete\"" -S openwisp_controller

Repository: openwisp/openwisp-controller

Length of output: 4421


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== openwisp_controller/config/handlers.py relevant slice ==\n'
sed -n '1,180p' openwisp_controller/config/handlers.py

printf '\n== openwisp_controller/config/base/base.py relevant slice ==\n'
sed -n '1,460p' openwisp_controller/config/base/base.py

printf '\n== openwisp_controller/config/apps.py relevant slice around cache deps ==\n'
sed -n '60,160p' openwisp_controller/config/apps.py

printf '\n== regression tests around delete cache dependencies ==\n'
sed -n '920,980p' openwisp_controller/config/tests/test_config.py
sed -n '1488,1525p' openwisp_controller/config/tests/test_api.py

Repository: openwisp/openwisp-controller

Length of output: 31230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== openwisp_controller/config/tasks.py relevant slice ==\n'
sed -n '70,135p' openwisp_controller/config/tasks.py

printf '\n== openwisp_controller/config/api/views.py relevant slice around delete invalidation ==\n'
sed -n '160,300p' openwisp_controller/config/api/views.py

printf '\n== openwisp_controller/config/tests/test_api.py regression section around delete invalidation ==\n'
sed -n '1488,1518p' openwisp_controller/config/tests/test_api.py

Repository: openwisp/openwisp-controller

Length of output: 9155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("openwisp_controller/config/tasks.py")
text = p.read_text()
for needle in [
    "invalidate_devicegroup_cache_delete",
    "devicegroup_delete_invalidates_cache",
    "certificate_delete_invalidates_cache",
]:
    idx = text.find(needle)
    print(f"== {needle} @ {idx} ==")
    if idx != -1:
        start = max(0, text.rfind("\n", 0, idx-400))
        end = min(len(text), idx+800)
        print(text[start:end])
        print()
PY

Repository: openwisp/openwisp-controller

Length of output: 4047


Defer these delete invalidations to on_commit

These post_delete hooks can race with concurrent reads and repopulate DeviceGroupCommonName with data for an object that is being deleted. Snapshot the needed delete attributes (organization_id, common_name, model name) and defer the task until commit.

🤖 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 `@openwisp_controller/config/apps.py` around lines 139 - 149, The post_delete
cache invalidations for the device group and cert dependencies are running
before the transaction commits, which can race with concurrent reads. Update the
CacheDependency entries in apps.py to use on_commit for the delete handlers, and
adjust devicegroup_delete_handler so it receives the needed snapshot data
(organization_id, common_name, and model name) instead of relying on the
soon-to-be-deleted object. Make sure the delete path still targets
devicegroup_delete_handler and defers the invalidation until commit.

Comment on lines +263 to +365
def _snapshot_handler(self, sender, instance, **kwargs):
"""Store the old values of ``track_fields`` before the instance saves."""
if instance._state.adding or instance.pk is None:
return
fields = self._get_fields_to_track(instance, **kwargs)
if not fields:
return
snapshot, db_fields = self._snapshot_track_fields_from_initial_values(
instance, fields=fields
)
if db_fields:
db_snapshot = self._snapshot_track_fields_from_db(
sender, instance, fields=db_fields
)
if db_snapshot is None:
return
snapshot.update(db_snapshot)
if snapshot is None:
return
snapshots = instance.__dict__.setdefault(self._SNAPSHOT_ATTR, {})
snapshots[self._uid] = snapshot

def _get_fields_to_track(self, instance, **kwargs):
fields = list(self.track_fields or [])
if not fields:
return fields
update_fields = kwargs.get("update_fields")
# Full save: all tracked fields could have changed.
if update_fields is None:
return fields
# save(update_fields=[...]) narrows the set of potentially changed fields.
# Expand names to include both field.name and field.attname so a tracked
# field like ``organization_id`` matches ``organization`` updates.
expanded = set(update_fields)
for name in list(update_fields):
try:
model_field = instance._meta.get_field(name)
except FieldDoesNotExist:
continue
expanded.add(model_field.name)
expanded.add(model_field.attname)
return [field for field in fields if field in expanded]

def _snapshot_track_fields_from_initial_values(self, instance, fields=None):
"""
Returns a tuple ``(snapshot, db_fields)`` where ``snapshot`` contains
values obtained from ``_initial_<field>`` attrs (or ``models.DEFERRED``
for still deferred fields), while ``db_fields`` contains unresolved
fields which must be fetched from DB.
"""
fields = fields or self.track_fields or []
if not fields:
return dict(), []
deferred_fields = instance.get_deferred_fields()
snapshot = dict()
db_fields = []
missing = object()
for field in fields:
attr = f"_initial_{field}"
value = getattr(instance, attr, missing)
if value is not missing and value != models.DEFERRED:
snapshot[field] = value
elif field in deferred_fields:
snapshot[field] = models.DEFERRED
else:
db_fields.append(field)
return snapshot, db_fields

def _snapshot_track_fields_from_db(self, sender, instance, fields=None):
fields = fields or self.track_fields or []
if not fields:
return dict()
try:
old = sender._default_manager.only(*fields).get(pk=instance.pk)
except sender.DoesNotExist:
return None
return {field: getattr(old, field) for field in fields}

def _tracked_fields_changed(self, instance):
snapshots = getattr(instance, self._SNAPSHOT_ATTR, None) or {}
old = snapshots.get(self._uid)
if old is None:
# No snapshot (e.g. on creation) -> nothing to compare against.
return False
deferred_fields = instance.get_deferred_fields()
for field, old_value in old.items():
if field in deferred_fields:
continue
if old_value == models.DEFERRED:
return True
if old_value != getattr(instance, field):
return True
return False

def _should_skip(self, instance, **kwargs):
if (
self.signal is post_save
and kwargs.get("created", False)
and not self.on_create
):
return True
if self.track_fields and not self._tracked_fields_changed(instance):
return True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Consume tracked-field snapshots after each post-save comparison.

Line 267 can skip creating a fresh snapshot, but Line 343 reuses any old snapshot left on the instance. Reusing the same Python object after a tracked-field change can therefore trigger cache invalidation on later unrelated save(update_fields=...) calls.

Proposed fix
         fields = self._get_fields_to_track(instance, **kwargs)
         if not fields:
+            snapshots = getattr(instance, self._SNAPSHOT_ATTR, None)
+            if snapshots is not None:
+                snapshots.pop(self._uid, None)
             return
@@
         snapshots = getattr(instance, self._SNAPSHOT_ATTR, None) or {}
-        old = snapshots.get(self._uid)
+        old = snapshots.pop(self._uid, None)
📝 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
def _snapshot_handler(self, sender, instance, **kwargs):
"""Store the old values of ``track_fields`` before the instance saves."""
if instance._state.adding or instance.pk is None:
return
fields = self._get_fields_to_track(instance, **kwargs)
if not fields:
return
snapshot, db_fields = self._snapshot_track_fields_from_initial_values(
instance, fields=fields
)
if db_fields:
db_snapshot = self._snapshot_track_fields_from_db(
sender, instance, fields=db_fields
)
if db_snapshot is None:
return
snapshot.update(db_snapshot)
if snapshot is None:
return
snapshots = instance.__dict__.setdefault(self._SNAPSHOT_ATTR, {})
snapshots[self._uid] = snapshot
def _get_fields_to_track(self, instance, **kwargs):
fields = list(self.track_fields or [])
if not fields:
return fields
update_fields = kwargs.get("update_fields")
# Full save: all tracked fields could have changed.
if update_fields is None:
return fields
# save(update_fields=[...]) narrows the set of potentially changed fields.
# Expand names to include both field.name and field.attname so a tracked
# field like ``organization_id`` matches ``organization`` updates.
expanded = set(update_fields)
for name in list(update_fields):
try:
model_field = instance._meta.get_field(name)
except FieldDoesNotExist:
continue
expanded.add(model_field.name)
expanded.add(model_field.attname)
return [field for field in fields if field in expanded]
def _snapshot_track_fields_from_initial_values(self, instance, fields=None):
"""
Returns a tuple ``(snapshot, db_fields)`` where ``snapshot`` contains
values obtained from ``_initial_<field>`` attrs (or ``models.DEFERRED``
for still deferred fields), while ``db_fields`` contains unresolved
fields which must be fetched from DB.
"""
fields = fields or self.track_fields or []
if not fields:
return dict(), []
deferred_fields = instance.get_deferred_fields()
snapshot = dict()
db_fields = []
missing = object()
for field in fields:
attr = f"_initial_{field}"
value = getattr(instance, attr, missing)
if value is not missing and value != models.DEFERRED:
snapshot[field] = value
elif field in deferred_fields:
snapshot[field] = models.DEFERRED
else:
db_fields.append(field)
return snapshot, db_fields
def _snapshot_track_fields_from_db(self, sender, instance, fields=None):
fields = fields or self.track_fields or []
if not fields:
return dict()
try:
old = sender._default_manager.only(*fields).get(pk=instance.pk)
except sender.DoesNotExist:
return None
return {field: getattr(old, field) for field in fields}
def _tracked_fields_changed(self, instance):
snapshots = getattr(instance, self._SNAPSHOT_ATTR, None) or {}
old = snapshots.get(self._uid)
if old is None:
# No snapshot (e.g. on creation) -> nothing to compare against.
return False
deferred_fields = instance.get_deferred_fields()
for field, old_value in old.items():
if field in deferred_fields:
continue
if old_value == models.DEFERRED:
return True
if old_value != getattr(instance, field):
return True
return False
def _should_skip(self, instance, **kwargs):
if (
self.signal is post_save
and kwargs.get("created", False)
and not self.on_create
):
return True
if self.track_fields and not self._tracked_fields_changed(instance):
return True
def _snapshot_handler(self, sender, instance, **kwargs):
"""Store the old values of ``track_fields`` before the instance saves."""
if instance._state.adding or instance.pk is None:
return
fields = self._get_fields_to_track(instance, **kwargs)
if not fields:
snapshots = getattr(instance, self._SNAPSHOT_ATTR, None)
if snapshots is not None:
snapshots.pop(self._uid, None)
return
snapshot, db_fields = self._snapshot_track_fields_from_initial_values(
instance, fields=fields
)
if db_fields:
db_snapshot = self._snapshot_track_fields_from_db(
sender, instance, fields=db_fields
)
if db_snapshot is None:
return
snapshot.update(db_snapshot)
if snapshot is None:
return
snapshots = instance.__dict__.setdefault(self._SNAPSHOT_ATTR, {})
snapshots[self._uid] = snapshot
def _get_fields_to_track(self, instance, **kwargs):
fields = list(self.track_fields or [])
if not fields:
return fields
update_fields = kwargs.get("update_fields")
# Full save: all tracked fields could have changed.
if update_fields is None:
return fields
# save(update_fields=[...]) narrows the set of potentially changed fields.
# Expand names to include both field.name and field.attname so a tracked
# field like ``organization_id`` matches ``organization`` updates.
expanded = set(update_fields)
for name in list(update_fields):
try:
model_field = instance._meta.get_field(name)
except FieldDoesNotExist:
continue
expanded.add(model_field.name)
expanded.add(model_field.attname)
return [field for field in fields if field in expanded]
def _snapshot_track_fields_from_initial_values(self, instance, fields=None):
"""
Returns a tuple ``(snapshot, db_fields)`` where ``snapshot`` contains
values obtained from ``_initial_<field>`` attrs (or ``models.DEFERRED``
for still deferred fields), while ``db_fields`` contains unresolved
fields which must be fetched from DB.
"""
fields = fields or self.track_fields or []
if not fields:
return dict(), []
deferred_fields = instance.get_deferred_fields()
snapshot = dict()
db_fields = []
missing = object()
for field in fields:
attr = f"_initial_{field}"
value = getattr(instance, attr, missing)
if value is not missing and value != models.DEFERRED:
snapshot[field] = value
elif field in deferred_fields:
snapshot[field] = models.DEFERRED
else:
db_fields.append(field)
return snapshot, db_fields
def _snapshot_track_fields_from_db(self, sender, instance, fields=None):
fields = fields or self.track_fields or []
if not fields:
return dict()
try:
old = sender._default_manager.only(*fields).get(pk=instance.pk)
except sender.DoesNotExist:
return None
return {field: getattr(old, field) for field in fields}
def _tracked_fields_changed(self, instance):
snapshots = getattr(instance, self._SNAPSHOT_ATTR, None) or {}
old = snapshots.pop(self._uid, None)
if old is None:
# No snapshot (e.g. on creation) -> nothing to compare against.
return False
deferred_fields = instance.get_deferred_fields()
for field, old_value in old.items():
if field in deferred_fields:
continue
if old_value == models.DEFERRED:
return True
if old_value != getattr(instance, field):
return True
return False
def _should_skip(self, instance, **kwargs):
if (
self.signal is post_save
and kwargs.get("created", False)
and not self.on_create
):
return True
if self.track_fields and not self._tracked_fields_changed(instance):
return True
🤖 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 `@openwisp_controller/config/base/base.py` around lines 263 - 365, The
tracked-field snapshot is being reused across later saves, so
`_snapshot_handler` should consume and clear the stored snapshot after each
post-save comparison. Update the snapshot lifecycle around `_snapshot_handler`,
`_tracked_fields_changed`, and `_should_skip` so a fresh snapshot is taken per
save and any old entry in `instance.__dict__[self._SNAPSHOT_ATTR]` for
`self._uid` is removed once it has been evaluated. This prevents stale snapshot
objects from affecting unrelated future `save(update_fields=...)` calls.

Comment on lines +367 to +391
CacheDependency(
source="config.Vpn",
signal="post_save",
on_create=True,
on_commit=False,
target=cls._invalidate_vpn_view_cache,
),
# Deferred to commit so a concurrent request cannot repopulate the
# cache with a VPN that is about to be (or was just) deleted.
# ``post_delete`` + ``_resolve_pk_snapshot`` because Django clears
# ``instance.pk`` on deleted instances before the deferred
# on_commit callback runs (see ``_resolve_pk_snapshot``).
CacheDependency(
source="config.Vpn",
signal="post_delete",
resolve=_resolve_pk_snapshot,
target=cls._invalidate_vpn_view_cache,
),
# A change to the VPN server configuration (e.g. via related objects)
# emits ``vpn_server_modified`` and must invalidate the view cache.
CacheDependency(
signal_obj=vpn_server_modified,
name="vpn_server_modified",
on_commit=False,
target=cls._invalidate_vpn_view_cache,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- openwisp_controller/config/base/vpn.py (around the cited lines) ---\n'
sed -n '330,430p' openwisp_controller/config/base/vpn.py

printf '\n--- locate CacheDependency definition/usages ---\n'
rg -n "class CacheDependency|CacheDependency\(" -S .

printf '\n--- find vpn_server_modified definition/usages ---\n'
rg -n "vpn_server_modified" -S .

Repository: openwisp/openwisp-controller

Length of output: 257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the CacheDependency implementation and any transaction helpers it uses.
for f in $(rg -l "class CacheDependency|def .*on_commit|transaction.on_commit" -S .); do
  echo "--- $f ---"
  wc -l "$f"
done

# Print the most relevant implementation slices if found.
for f in $(rg -l "class CacheDependency" -S .); do
  echo "--- $f (definition) ---"
  sed -n '1,260p' "$f"
done

Repository: openwisp/openwisp-controller

Length of output: 10887


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- openwisp_controller/config/base/vpn.py around the cited block ---'
sed -n '350,410p' openwisp_controller/config/base/vpn.py

echo
echo '--- openwisp_controller/config/base/base.py handler methods around on_commit ---'
sed -n '260,430p' openwisp_controller/config/base/base.py

Repository: openwisp/openwisp-controller

Length of output: 9236


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('openwisp_controller/config/base/vpn.py')
text = p.read_text()
for needle in [
    'CacheDependency(',
    'signal="post_save"',
    'signal_obj=vpn_server_modified',
    'on_commit=False',
    '_invalidate_vpn_view_cache',
]:
    print(f'--- occurrences for {needle!r} ---')
    for i, line in enumerate(text.splitlines(), 1):
        if needle in line:
            print(f'{i}: {line}')
PY

Repository: openwisp/openwisp-controller

Length of output: 977


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- vpn_server_modified definition and emit sites ---'
rg -n -A3 -B3 "vpn_server_modified|send\\(|emit" openwisp_controller/config/base/vpn.py openwisp_controller/config/base -S

echo
echo '--- _invalidate_vpn_view_cache implementation ---'
sed -n '320,350p' openwisp_controller/config/base/vpn.py

Repository: openwisp/openwisp-controller

Length of output: 22147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for transaction.atomic around VPN saves or signal emissions.
rg -n -A4 -B4 "atomic\\(|vpn_server_modified|post_save|save\\(" openwisp_controller/config/base/vpn.py openwisp_controller/config/base/base.py openwisp_controller/config/base/config.py -S

Repository: openwisp/openwisp-controller

Length of output: 24983


Defer VPN view-cache invalidation to commit.
post_save and vpn_server_modified can both run inside an open transaction; invalidating GetVpnView.get_vpn immediately lets a concurrent request repopulate the cache from the pre-commit row.

🤖 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 `@openwisp_controller/config/base/vpn.py` around lines 367 - 391, Update the
CacheDependency entries in Vpn view-cache invalidation so that both the
post_save path and the vpn_server_modified signal defer invalidation until
transaction commit. In the Vpn cache dependency setup, change the on_commit
behavior for cls._invalidate_vpn_view_cache so GetVpnView.get_vpn is not
invalidated/repopulated before the transaction is committed.

Comment on lines +573 to +600
def test_ca_renew_cascades_to_client_config(self):
vpn = self._create_vpn()
vpn_template = self._create_template(
name="vpn-template", type="vpn", vpn=vpn, config={}
)
device = self._create_device_config()
device.config.templates.add(vpn_template)
config = Config.objects.get(pk=device.config.pk)
old_checksum_db = config.checksum_db
self.assertEqual(old_checksum_db, config.checksum)
vpn.ca.renew()
config = Config.objects.get(pk=device.config.pk)
self.assertNotEqual(config.checksum_db, old_checksum_db)
self.assertEqual(config.checksum_db, config.checksum)
self.assertEqual(config.status, "modified")

def test_cert_renew_cascades_to_client_config(self):
vpn = self._create_vpn()
vpn_template = self._create_template(
name="vpn-template", type="vpn", vpn=vpn, config={}
)
device = self._create_device_config()
device.config.templates.add(vpn_template)
config = Config.objects.get(pk=device.config.pk)
self.assertEqual(config.checksum_db, config.checksum)
with catch_signal(vpn_server_modified) as mocked_vpn_server_modified:
vpn.cert.renew()
mocked_vpn_server_modified.assert_called_once()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor duplication between renew-cascade tests.

test_ca_renew_cascades_to_client_config and test_cert_renew_cascades_to_client_config share nearly identical setup (vpn/template/device/config creation and checksum assertions). Could be factored into a shared helper, but low priority given clarity of standalone tests.

🤖 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 `@openwisp_controller/config/tests/test_vpn.py` around lines 573 - 600, The two
renew-cascade tests duplicate the same VPN/template/device setup and initial
config checksum assertions, so factor that shared logic into a small helper in
test_vpn.py and keep each test focused on its specific action (`vpn.ca.renew()`
vs `vpn.cert.renew()`). Use the existing test methods
`test_ca_renew_cascades_to_client_config` and
`test_cert_renew_cascades_to_client_config`, plus the helper builders like
`_create_vpn`, `_create_template`, and `_create_device_config`, to reduce
repetition while preserving the distinct post-renew assertions and signal check.

@openwisp-companion

Copy link
Copy Markdown

The CI is failing due to transient infrastructure issues (not related to your code). I have restarted the failed jobs automatically (1/3).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] Generic solution for invalidating cache on related field changes

3 participants