Skip to content

[fix] Invalidate controller views cache when org/group context variables change #1070#1308

Open
mn-ram wants to merge 7 commits intoopenwisp:masterfrom
mn-ram:fix/invalidate-controller-views-cache-on-org-context-change
Open

[fix] Invalidate controller views cache when org/group context variables change #1070#1308
mn-ram wants to merge 7 commits intoopenwisp:masterfrom
mn-ram:fix/invalidate-controller-views-cache-on-org-context-change

Conversation

@mn-ram
Copy link
Copy Markdown

@mn-ram mn-ram commented Mar 21, 2026

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 #1070.

Description of Changes

When organization or device group configuration variables are updated, only
bulk_invalidate_config_get_cached_checksum was called to bust the Config
checksum cache. However, the DeviceChecksumView controller endpoint maintains
a separate cache that was never invalidated on this code path.

As a result, devices kept receiving the old (stale) checksum when polling the
controller, so they never transitioned to modified status and never pulled
the updated configuration.

Root cause: two separate cache layers exist for configuration checksums:

  1. Config checksum cache — invalidated by bulk_invalidate_config_get_cached_checksum
  2. DeviceChecksumView cache — invalidated by invalidate_controller_views_cache

Only layer 1 was being cleared; layer 2 was left stale.

Fix:

  • openwisp_controller/config/base/multitenancy.py — call
    invalidate_controller_views_cache when org context changes, mirroring
    the existing behaviour already done in handlers.py when an organisation
    is disabled or enabled.

  • openwisp_controller/config/base/device_group.py — add a new group-scoped
    task invalidate_controller_views_for_group instead of the org-wide
    invalidate_controller_views_cache, so only the DeviceChecksumView entries
    for devices in the changed group are cleared (avoids unnecessary invalidation
    of unrelated devices and VPN caches).

  • openwisp_controller/config/tasks.py — new task
    invalidate_controller_views_for_group(group_id) that clears
    DeviceChecksumView only for devices filtered by group_id.

  • openwisp_controller/config/tests/test_device.py — regression tests for
    both org and group variable changes; verifies only configs whose rendered
    output is actually affected transition to modified, and asserts the
    group-scoped task is enqueued with the correct group_id.

When organization context variables are updated,
`bulk_invalidate_config_get_cached_checksum` was called to bust the
Config checksum cache, but `invalidate_controller_views_cache` was
never called. This left the DeviceChecksumView cache stale, so devices
kept receiving the old checksum, never transitioned to `modified`
status, and never pulled the updated configuration.

Fix: also call `invalidate_controller_views_cache` whenever the org
`context` field changes, mirroring the existing behaviour that already
fires this task when an organisation is disabled/enabled.

Fixes openwisp#1070
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

When an existing Organization or DeviceGroup instance is saved and its context field changes, the save() method enqueues two asynchronous tasks: the existing bulk_invalidate_config_get_cached_checksum.delay(...) and a new controller-views invalidation task — invalidate_controller_views_cache.delay(organization_id) for organization changes or invalidate_controller_views_for_group.delay(group_id) for group changes. A new Celery task invalidate_controller_views_for_group(group_id) was added to invalidate DeviceChecksumView entries for devices in a group. Tests were added to verify config status updates from applied to modified when scoped variables change.

Sequence Diagram(s)

sequenceDiagram
    participant Admin as Admin (updates context)
    participant Django as Django Model Save
    participant Celery as Celery Broker/Worker
    participant DB as Database (Config rows)
    participant Cache as Controller Views Cache

    Admin->>Django: save(Organization with changed context)
    Django->>Celery: bulk_invalidate_config_get_cached_checksum.delay({"device__organization_id": org_id})
    Django->>Celery: invalidate_controller_views_cache.delay(org_id)
    Celery->>DB: locate affected Config rows and mark as "modified"
    Celery->>Cache: invalidate DeviceChecksumView & GetVpnView for org_id
Loading
sequenceDiagram
    participant Admin as Admin (updates context)
    participant Django as Django Model Save
    participant Celery as Celery Broker/Worker
    participant DB as Database (Config rows)
    participant Cache as Controller Views Cache

    Admin->>Django: save(DeviceGroup with changed context)
    Django->>Celery: bulk_invalidate_config_get_cached_checksum.delay({"device__group_id": group_id})
    Django->>Celery: invalidate_controller_views_for_group.delay(group_id)
    Celery->>DB: locate affected Config rows and mark as "modified"
    Celery->>Cache: invalidate DeviceChecksumView for devices in group_id
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses all objectives from issue #1070 by invalidating DeviceChecksumView cache when organization context changes and adding regression tests verifying config status transitions.
Out of Scope Changes check ✅ Passed All changes—multitenancy.py, device_group.py, tasks.py, and test_device.py—directly support fixing the cache invalidation issue and are within scope of issue #1070.
Bug Fixes ✅ Passed Pull request correctly fixes the root cause by adding DeviceChecksumView cache invalidation alongside Config checksum cache invalidation when organization or device group context variables change. Two comprehensive regression tests verify config status correctly transitions from applied to modified only for affected configs.
Title check ✅ Passed The title follows the required [type] format with 'fix', is descriptive of the main change (invalidating controller views cache on context variable changes), and references the linked issue #1070.
Description check ✅ Passed The description covers all required template sections: checklist items marked, issue reference (Closes #1070), and detailed description of changes with root cause analysis and technical implementation details.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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/multitenancy.py (1)

99-103: ⚠️ Potential issue | 🟠 Major

Enforce deterministic task order to avoid stale cache re-population.

At Line 100 and Line 103, dispatching two independent Celery .delay() calls can run out of order. If controller-view cache invalidation finishes first, stale checksum data can be cached again before checksum invalidation completes.

Proposed fix (chain the tasks in order)
+from celery import chain
 from ..tasks import bulk_invalidate_config_get_cached_checksum, invalidate_controller_views_cache
@@
         if context_changed:
-            bulk_invalidate_config_get_cached_checksum.delay(
-                {"device__organization_id": str(self.organization_id)}
-            )
-            invalidate_controller_views_cache.delay(str(self.organization_id))
+            chain(
+                bulk_invalidate_config_get_cached_checksum.si(
+                    {"device__organization_id": str(self.organization_id)}
+                ),
+                invalidate_controller_views_cache.si(str(self.organization_id)),
+            ).delay()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/config/base/multitenancy.py` around lines 99 - 103, Two
independent Celery .delay() calls
(bulk_invalidate_config_get_cached_checksum.delay and
invalidate_controller_views_cache.delay) can run out of order; replace them with
a Celery chain so checksum invalidation runs first and then controller-view
invalidation. Create task signatures (e.g.,
bulk_invalidate_config_get_cached_checksum.s({"device__organization_id":
str(self.organization_id)}) and
invalidate_controller_views_cache.s(str(self.organization_id))) and execute them
with chain(...).apply_async() so the tasks execute deterministically in that
order.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@openwisp_controller/config/base/multitenancy.py`:
- Around line 99-103: Two independent Celery .delay() calls
(bulk_invalidate_config_get_cached_checksum.delay and
invalidate_controller_views_cache.delay) can run out of order; replace them with
a Celery chain so checksum invalidation runs first and then controller-view
invalidation. Create task signatures (e.g.,
bulk_invalidate_config_get_cached_checksum.s({"device__organization_id":
str(self.organization_id)}) and
invalidate_controller_views_cache.s(str(self.organization_id))) and execute them
with chain(...).apply_async() so the tasks execute deterministically in that
order.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7bbfb950-c36f-4654-be6f-cab7769c0ee2

📥 Commits

Reviewing files that changed from the base of the PR and between 45b24b6 and 4e0ea20.

📒 Files selected for processing (1)
  • openwisp_controller/config/base/multitenancy.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/base/multitenancy.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/base/multitenancy.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/base/multitenancy.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/base/multitenancy.py
🧠 Learnings (3)
📚 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/base/multitenancy.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/base/multitenancy.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/base/multitenancy.py
🔇 Additional comments (1)
openwisp_controller/config/base/multitenancy.py (1)

15-15: Import update is correct and consistent.

The new task import matches the usage in save() and follows the established task call pattern.

mn-ram and others added 2 commits March 21, 2026 21:08
Split the long import line into multi-line format to stay within
the 88-character line length limit required by Black and isort.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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/multitenancy.py (1)

102-106: ⚠️ Potential issue | 🟠 Major

LGTM! The fix correctly invalidates controller views cache on context change.

The implementation mirrors the existing pattern from handlers.py (passing str(organization_id)) and correctly positions the call after super().save() within the context_changed block.

However: AbstractDeviceGroup.save() in device_group.py (lines 79-90) should also call invalidate_controller_views_cache when its context changes. Currently it only calls bulk_invalidate_config_get_cached_checksum, leaving device checksum view caches stale when device group variables change. This mirrors the pattern established in this PR for organization-level context changes and should be applied consistently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/config/base/multitenancy.py` around lines 102 - 106,
AbstractDeviceGroup.save currently only calls
bulk_invalidate_config_get_cached_checksum when context changes; update
AbstractDeviceGroup.save to also call invalidate_controller_views_cache with the
organization's id to avoid stale device checksum view caches. Locate the
context_changed handling in AbstractDeviceGroup.save (method name: save,
variable: context_changed) and add a call to
invalidate_controller_views_cache.delay(str(self.organization_id)) in the same
post-save block (after super().save()) mirroring the pattern used in
multitenancy.py and handlers.py so the organization id is passed as a string.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@openwisp_controller/config/base/multitenancy.py`:
- Around line 102-106: AbstractDeviceGroup.save currently only calls
bulk_invalidate_config_get_cached_checksum when context changes; update
AbstractDeviceGroup.save to also call invalidate_controller_views_cache with the
organization's id to avoid stale device checksum view caches. Locate the
context_changed handling in AbstractDeviceGroup.save (method name: save,
variable: context_changed) and add a call to
invalidate_controller_views_cache.delay(str(self.organization_id)) in the same
post-save block (after super().save()) mirroring the pattern used in
multitenancy.py and handlers.py so the organization id is passed as a string.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 951a382e-02af-4524-b783-a0ab36bb1bd6

📥 Commits

Reviewing files that changed from the base of the PR and between 4e0ea20 and 97cbf14.

📒 Files selected for processing (1)
  • openwisp_controller/config/base/multitenancy.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.13 | 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.12 | django~=5.2.0
  • GitHub Check: Python==3.13 | 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.10 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/base/multitenancy.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/base/multitenancy.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/base/multitenancy.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/base/multitenancy.py
🧠 Learnings (3)
📚 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/base/multitenancy.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/base/multitenancy.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/base/multitenancy.py
🔇 Additional comments (1)
openwisp_controller/config/base/multitenancy.py (1)

15-18: LGTM!

The import is correctly added alongside the existing bulk_invalidate_config_get_cached_checksum import, maintaining clean organization of task imports.

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 21, 2026
@coveralls
Copy link
Copy Markdown

coveralls commented Mar 21, 2026

Coverage Status

coverage: 98.661% (+0.001%) from 98.66%
when pulling 1a0bb83 on mn-ram:fix/invalidate-controller-views-cache-on-org-context-change
into 0d17acd on openwisp:master.

Fixes openwisp#1070

Add tests to verify that when organization or device group
configuration variables change, the config status updates to
"modified" only for devices whose rendered configuration is
actually affected by the change.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_controller/config/base/device_group.py`:
- Line 94: Currently the DeviceGroup.save path calls
invalidate_controller_views_cache.delay(str(self.organization_id)) which
triggers an org-wide loop; replace this with a new, group-scoped task to avoid
invalidating unrelated devices and VPNs: add a task function in
openwisp_controller/config/tasks.py (e.g.,
invalidate_controller_views_for_group(group_id, organization_id=None)) that
queries devices filtered by group_id and only clears DeviceChecksumView (and any
other group-local views) for those device IDs, then call that new task from
DeviceGroup.context/save instead of invalidate_controller_views_cache; also
ensure the new task does not perform VPN invalidation and accepts group_id as
the unique identifier to limit scope.

In `@openwisp_controller/config/tests/test_device.py`:
- Around line 463-490: Add a mock.patch around the controller-view cache
invalidation task (patch invalidate_controller_views_cache.delay) in
test_status_update_on_group_variable_change so you assert the task is enqueued
when dg.context is changed: patch the task before assigning dg.context and
saving (after dg.full_clean()/dg.save()), assert the patched delay was called
(e.g., called_once or called_with expected args) to ensure
invalidate_controller_views_cache.delay is triggered when context changes while
keeping the existing assertions for c1.status and c2.status.
🪄 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: 3661edd0-e1f9-4a60-aa51-69420385e73f

📥 Commits

Reviewing files that changed from the base of the PR and between 97cbf14 and 4a3ef52.

📒 Files selected for processing (2)
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/tests/test_device.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | 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.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/base/device_group.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tests/test_device.py
🧠 Learnings (4)
📚 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/base/device_group.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • openwisp_controller/config/tests/test_device.py
🔇 Additional comments (2)
openwisp_controller/config/base/device_group.py (1)

18-21: Import update looks correct.

Good addition of invalidate_controller_views_cache in the task imports; this aligns with the new save-path behavior.

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

436-462: Nice targeted regression coverage for selective status updates.

The affected/unaffected assertions are clear and directly validate the intended org-context behavior.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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/device_group.py (1)

90-94: ⚠️ Potential issue | 🟠 Major

Enforce deterministic ordering between checksum invalidation and view-cache invalidation.

Line 91 and Line 94 enqueue two independent async tasks. Their execution order is nondeterministic, so DeviceChecksumView can be repopulated with stale data if view invalidation runs first.

Suggested fix
+from celery import chain
@@
         if context_changed:
-            bulk_invalidate_config_get_cached_checksum.delay(
-                {"device__group_id": str(self.id)}
-            )
-            invalidate_controller_views_for_group.delay(str(self.id))
+            chain(
+                bulk_invalidate_config_get_cached_checksum.si(
+                    {"device__group_id": str(self.id)}
+                ),
+                invalidate_controller_views_for_group.si(str(self.id)),
+            ).delay()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/config/base/device_group.py` around lines 90 - 94, Two
async tasks (bulk_invalidate_config_get_cached_checksum.delay and
invalidate_controller_views_for_group.delay) are enqueued in nondeterministic
order, risking DeviceChecksumView repopulation with stale data; change the
enqueueing so checksum invalidation always runs before view invalidation by
chaining the tasks (e.g., use Celery chain or apply_async with link/chain)
instead of calling .delay independently, referencing
bulk_invalidate_config_get_cached_checksum and
invalidate_controller_views_for_group and ensuring the chain guarantees
completion of the checksum task before the view invalidation task is scheduled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@openwisp_controller/config/base/device_group.py`:
- Around line 90-94: Two async tasks
(bulk_invalidate_config_get_cached_checksum.delay and
invalidate_controller_views_for_group.delay) are enqueued in nondeterministic
order, risking DeviceChecksumView repopulation with stale data; change the
enqueueing so checksum invalidation always runs before view invalidation by
chaining the tasks (e.g., use Celery chain or apply_async with link/chain)
instead of calling .delay independently, referencing
bulk_invalidate_config_get_cached_checksum and
invalidate_controller_views_for_group and ensuring the chain guarantees
completion of the checksum task before the view invalidation task is scheduled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d5f34fc8-c8ee-44c4-9512-61ddd9d3e4f1

📥 Commits

Reviewing files that changed from the base of the PR and between 4a3ef52 and 966c931.

📒 Files selected for processing (3)
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.py
🧠 Learnings (5)
📚 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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-03-17T09:20:10.456Z
Learnt from: pandafy
Repo: openwisp/openwisp-controller PR: 1292
File: openwisp_controller/connection/tasks.py:27-31
Timestamp: 2026-03-17T09:20:10.456Z
Learning: In `openwisp_controller/connection/tasks.py`, the `update_config` Celery task only accepts one argument `device_id`, which is always passed as a string (via `str(device.pk)`) from the call site in `openwisp_controller/connection/apps.py`. Do not flag `str(device_id) in task["args"]` as unreliable due to typed args — the args are always strings.

Applied to files:

  • openwisp_controller/config/tasks.py
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • openwisp_controller/config/tests/test_device.py
🔇 Additional comments (3)
openwisp_controller/config/tasks.py (1)

222-237: Good scoped cache invalidation for group context updates.

This narrows invalidation to devices in the changed group and avoids unnecessary VPN/org-wide cache churn.

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

436-462: Regression test for org-context impact looks solid.

The affected/unaffected status assertions are clear and directly validate the intended behavior.


463-493: Nice addition of explicit enqueue assertion for group view-cache invalidation.

This protects the root fix path by asserting invalidate_controller_views_for_group.delay(...) is triggered when group context changes.

@openwisp-companion
Copy link
Copy Markdown

QA, Test, and Commit Message Failures

Hello @mn-ram,
(Analysis for commit 966c931)

  1. Code Style/QA:
  • Black: The Black check failed! error indicates a formatting issue. Please run openwisp-qa-format to automatically fix this.
  • Flake8: The E501 line too long (105 > 88 characters) error in ./openwisp_controller/config/tests/test_device.py needs to be fixed manually. Please shorten the line exceeding the maximum length.
  1. Commit Message:
  • The commit message is missing the required tag in the header. It should start with [fix], [feat], [chore], etc.
  • The issue number #1070 is referenced in the body but not in the header.

A correct commit message format looks like this:

[fix] Short, imperative title about the change #123

More detailed explanation of the changes.
  1. Test Failure:
  • The CI run shows a significant number of slow tests, with 592 detected. While not a failure itself, this indicates potential performance issues or tests that could be optimized.
  • The overall test run failed with Process completed with exit code 1. The specific test failures are not detailed in the provided logs, but the large number of slow tests might be a contributing factor. Please review the test execution for more specific AssertionError or other test-related failures.

@mn-ram mn-ram force-pushed the fix/invalidate-controller-views-cache-on-org-context-change branch from 449af8f to e46e816 Compare March 25, 2026 12:37
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_controller/config/tasks.py`:
- Around line 222-237: The task decorator for
invalidate_controller_views_for_group should include a soft_time_limit to match
other long-running iteration tasks; update the `@shared_task`(...) call on the
invalidate_controller_views_for_group function to pass soft_time_limit=7200
(preserving base=OpenwispCeleryTask) so the task will have a soft timeout
safeguard for large device groups.

In `@openwisp_controller/config/tests/test_device.py`:
- Around line 436-461: The test test_status_update_on_org_variable_change
verifies config status changes but omits asserting that the org-level cache
invalidation task was enqueued; update the test to patch/mocking
invalidate_controller_views_cache.delay (same approach used in
test_status_update_on_group_variable_change), set
OrganizationConfigSettings.context, save, then assert
invalidate_controller_views_cache.delay was called (and with appropriate args if
required) after saving OrganizationConfigSettings; reference
OrganizationConfigSettings and invalidate_controller_views_cache.delay to locate
where to add the mock and assertion.
🪄 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: 1c23ae23-770b-4200-98fe-3b10bc9ef0e4

📥 Commits

Reviewing files that changed from the base of the PR and between 966c931 and e9b0da0.

📒 Files selected for processing (3)
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.py
🧠 Learnings (5)
📚 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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.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/base/device_group.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_device.py
📚 Learning: 2026-03-17T09:20:10.456Z
Learnt from: pandafy
Repo: openwisp/openwisp-controller PR: 1292
File: openwisp_controller/connection/tasks.py:27-31
Timestamp: 2026-03-17T09:20:10.456Z
Learning: In `openwisp_controller/connection/tasks.py`, the `update_config` Celery task only accepts one argument `device_id`, which is always passed as a string (via `str(device.pk)`) from the call site in `openwisp_controller/connection/apps.py`. Do not flag `str(device_id) in task["args"]` as unreliable due to typed args — the args are always strings.

Applied to files:

  • openwisp_controller/config/tasks.py
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • openwisp_controller/config/tests/test_device.py
🔇 Additional comments (3)
openwisp_controller/config/base/device_group.py (1)

18-21: LGTM! Group-scoped cache invalidation correctly implemented.

The new invalidate_controller_views_for_group task is appropriately imported and invoked with the group ID (str(self.id)), ensuring only devices in the affected group have their DeviceChecksumView cache invalidated. This addresses the previous performance concern about org-wide invalidation.

Also applies to: 94-94

openwisp_controller/config/tasks.py (1)

223-229: Good documentation of task scope.

The docstring clearly explains the difference from invalidate_controller_views_cache and notes that VPN caches are not invalidated. This helps maintainers understand the intentional scoping.

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

463-495: LGTM! Comprehensive test with proper mock verification.

The test correctly:

  1. Sets up a device group with two devices where only one config uses the variable
  2. Mocks the task at the correct import path in device_group module
  3. Verifies the async task is called with the correct group ID
  4. Uses subTest to clearly separate the status assertions

This addresses the previous review comment requesting explicit assertion for controller-view cache invalidation.

Comment on lines +222 to +237
@shared_task(base=OpenwispCeleryTask)
def invalidate_controller_views_for_group(group_id):
"""
Invalidates the DeviceChecksumView cache only for devices in the given group.

Unlike invalidate_controller_views_cache, this is scoped to a single device
group and does not invalidate VPN caches.
"""
from .controller.views import DeviceChecksumView

Device = load_model("config", "Device")

for device in (
Device.objects.filter(group_id=group_id).only("id").iterator()
):
DeviceChecksumView.invalidate_get_device_cache(device)
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.

🧹 Nitpick | 🔵 Trivial

Consider adding soft_time_limit for consistency with similar tasks.

The new task lacks the soft_time_limit parameter that other iteration-based tasks in this file use (e.g., invalidate_controller_views_cache implicitly inherits defaults, but tasks like update_template_related_config_status explicitly set soft_time_limit=7200). For large device groups, this task could run longer than expected without a safeguard.

💡 Suggested fix
-@shared_task(base=OpenwispCeleryTask)
+@shared_task(base=OpenwispCeleryTask, soft_time_limit=7200)
 def invalidate_controller_views_for_group(group_id):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/config/tasks.py` around lines 222 - 237, The task
decorator for invalidate_controller_views_for_group should include a
soft_time_limit to match other long-running iteration tasks; update the
`@shared_task`(...) call on the invalidate_controller_views_for_group function to
pass soft_time_limit=7200 (preserving base=OpenwispCeleryTask) so the task will
have a soft timeout safeguard for large device groups.

Comment on lines +436 to +461
def test_status_update_on_org_variable_change(self):
org = self._get_org()
cs = OrganizationConfigSettings.objects.create(organization=org, context={})
c1 = self._create_config(organization=org)
c1.templates.add(
self._create_template(
name="t-with-var",
config={"interfaces": [{"name": "{{ ssid }}", "type": "ethernet"}]},
default_values={"ssid": "eth0"},
)
)
c1.set_status_applied()
d2 = self._create_device(
organization=org, name="d2", mac_address="00:11:22:33:44:56"
)
c2 = self._create_config(device=d2)
c2.set_status_applied()
cs.context = {"ssid": "OrgA"}
cs.full_clean()
cs.save()
c1.refresh_from_db()
c2.refresh_from_db()
with self.subTest("affected config changes to modified"):
self.assertEqual(c1.status, "modified")
with self.subTest("unaffected config remains applied"):
self.assertEqual(c2.status, "applied")
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.

🧹 Nitpick | 🔵 Trivial

Consider adding explicit mock assertion for org-level cache invalidation.

The organization variable change test validates the status transition correctly, but unlike test_status_update_on_group_variable_change, it doesn't explicitly verify that invalidate_controller_views_cache.delay is called when OrganizationConfigSettings.context changes. Adding a mock assertion would strengthen the regression test for the PR's core fix.

💡 Suggested addition
         cs.context = {"ssid": "OrgA"}
         cs.full_clean()
-        cs.save()
+        patch_path = (
+            "openwisp_controller.config.base.multitenancy"
+            ".invalidate_controller_views_cache"
+        )
+        with mock.patch(patch_path) as mocked_task:
+            cs.save()
+            mocked_task.delay.assert_called_once_with(str(org.id))
         c1.refresh_from_db()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/config/tests/test_device.py` around lines 436 - 461, The
test test_status_update_on_org_variable_change verifies config status changes
but omits asserting that the org-level cache invalidation task was enqueued;
update the test to patch/mocking invalidate_controller_views_cache.delay (same
approach used in test_status_update_on_group_variable_change), set
OrganizationConfigSettings.context, save, then assert
invalidate_controller_views_cache.delay was called (and with appropriate args if
required) after saving OrganizationConfigSettings; reference
OrganizationConfigSettings and invalidate_controller_views_cache.delay to locate
where to add the mock and assertion.

@mn-ram mn-ram force-pushed the fix/invalidate-controller-views-cache-on-org-context-change branch from e9b0da0 to 8538212 Compare March 25, 2026 13:04
…xt change openwisp#1070

When device group context variables are updated,
bulk_invalidate_config_get_cached_checksum was already called to mark
affected configs as modified, but invalidate_controller_views_cache was
never called, leaving the DeviceChecksumView cache stale for devices in
the group.

Replace the org-wide invalidate_controller_views_cache with a new
group-scoped task, invalidate_controller_views_for_group, that only
clears DeviceChecksumView entries for devices in the changed group,
avoiding unnecessary invalidation of unrelated devices and VPNs.
@mn-ram mn-ram force-pushed the fix/invalidate-controller-views-cache-on-org-context-change branch from 8538212 to 4aff88c Compare March 25, 2026 13:05
@mn-ram mn-ram changed the title [fix] Invalidate controller views cache when org context variables change [fix] Invalidate controller views cache when org/group context variables change #1070 Mar 25, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

Tip

For best results, initiate chat on the files or code changes.

@mn-ram Sure, I'll approve the PR now.

[approve]

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

Tip

For best results, initiate chat on the files or code changes.

@mn-ram My apologies for that! Let me approve the PR now.

[approve]

@mn-ram
Copy link
Copy Markdown
Author

mn-ram commented Mar 25, 2026

Hi @nemesifier, the root cause of #1070 was that DeviceChecksumView cache was never invalidated when org/group context changed, so devices kept receiving the stale checksum and never pulled the updated config.

Fix: call invalidate_controller_views_cache for org context changes, and a new group-scoped task invalidate_controller_views_for_group for device group changes (avoids invalidating unrelated devices/VPNs). All CI checks are green.

Would appreciate your review whenever you get a chance. Thank you!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Device config status does not update to "modified" when organization variable is changed

2 participants