Skip to content

Conversation

@Srinath0916
Copy link
Contributor

@Srinath0916 Srinath0916 commented Dec 15, 2025

When organization configuration variables are updated, VPN cache wasn't being invalidated automatically. This caused VPNs to use stale config data leading to connectivity issues.

Added signal handler to detect context changes in OrganizationConfigSettings and trigger cache invalidation for all VPNs in that organization. Also added comprehensive tests to ensure the functionality works correctly.

Fixes #1098

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

Description of Changes

  • Added organization_config_settings_change_handler in handlers.py to detect when OrganizationConfigSettings context field changes
  • Added invalidate_organization_vpn_cache Celery task in tasks.py to invalidate VPN cache for all VPNs in the affected organization
  • Connected the signal handler to OrganizationConfigSettings post_save signal in apps.py
  • Added comprehensive test suite in test_organization_vpn_cache.py covering signal handling, task execution, and edge cases

The implementation ensures VPN cache is automatically invalidated whenever organization configuration variables change, preventing stale configuration issues.

Screenshot

Not applicable - this is a backend functionality change with no UI components.

@Srinath0916 Srinath0916 force-pushed the feature/vpn-cache-invalidation-1098 branch 10 times, most recently from fb709ba to 954e35c Compare December 15, 2025 23:52
@coveralls
Copy link

coveralls commented Dec 16, 2025

Coverage Status

coverage: 98.467% (-0.2%) from 98.657%
when pulling 240026a on Srinath0916:feature/vpn-cache-invalidation-1098
into 0c5c4a5 on openwisp:master.

@Srinath0916 Srinath0916 force-pushed the feature/vpn-cache-invalidation-1098 branch 3 times, most recently from 497fd5a to 3342a52 Compare December 16, 2025 00:40
…nge openwisp#1098

Implemented automatic VPN cache invalidation when OrganizationConfigSettings
context changes to prevent stale VPN configurations and connectivity issues.

Changes:
- Added signal handler to detect OrganizationConfigSettings context changes
- Added Celery task to invalidate VPN cache for all VPNs in organization
- Connected post_save signal to trigger cache invalidation automatically
- Uses string import to avoid circular import issues
- Ensures VPNs always use latest configuration variables

This resolves the issue where VPN cache was not automatically invalidated
when organization configuration variables changed, causing inconsistency
and connectivity problems.

Fixes openwisp#1098
@Srinath0916 Srinath0916 force-pushed the feature/vpn-cache-invalidation-1098 branch from 3342a52 to 7e365af Compare December 16, 2025 00:47
@Srinath0916
Copy link
Contributor Author

Srinath0916 commented Dec 16, 2025

Hi @pandafy,

I've been working on implementing the VPN cache invalidation for issue #1098 and spent most of the night trying to get it working, but I'm running into a really strange CI problem.

I have the implementation ready - signal handler to detect OrganizationConfigSettings context changes, Celery task to invalidate VPN cache, and proper signal connections. The logic is straightforward and follows the same patterns as existing handlers in the codebase.

But I keep hitting this geckodriver error (visible in the checks above) that seems completely unrelated to my changes. The weird thing is that it happens with ANY modification I make - even just adding TODO comments or copying the exact pattern from working handlers like vpn_server_change_handler.

I tried many different approaches throughout the night to get past this CI error. You can see all my attempts in the issue page - I know it looks messy with all those commits, but I was just trying everything I could think of. Every single approach fails with the same geckodriver error, even when the code changes are minimal.

My other PR (#1173) passes all CI checks fine, so I don't think it's an account or general setup issue. This seems specific to this particular feature.

Could you help me understand what might be causing this? Is there something about VPN-related code that triggers different test suites? Or maybe there's a specific approach you'd recommend for this type of cache invalidation?

I really want to get this working and contribute to OpenWISP properly. The implementation is ready, just need to figure out this CI puzzle.

Sorry for all the iteration - I was just determined to get past this error!

@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

Walkthrough

Adds app config attribute organization_config_settings_model. AbstractOrganizationConfigSettings now snapshots an instance's initial context on init and, on save, compares it to detect changes. If the context changed and the organization is active, it registers transaction.on_commit callbacks to enqueue tasks: bulk_invalidate_config_get_cached_checksum and the new Celery task invalidate_organization_vpn_cache(organization_id). The new task loads VPNs for the organization and calls GetVpnView.invalidate_get_vpn_cache(vpn) and vpn.invalidate_checksum_cache() for each. Tests covering cache invalidation scenarios were added.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant DjangoORM as Django ORM
    participant OrgConfig as OrganizationConfigSettings
    participant Txn as transaction.on_commit
    participant Celery as Celery Queue
    participant DB as Database
    participant GetVpnView
    participant VPN

    Client->>DjangoORM: save OrganizationConfigSettings
    DjangoORM->>OrgConfig: __init__ (snapshot _initial_context)
    DjangoORM->>OrgConfig: save()
    OrgConfig->>OrgConfig: compare context vs _initial_context
    alt context changed AND organization is active
        OrgConfig->>Txn: register on_commit callbacks
        Txn->>Celery: enqueue invalidate_organization_vpn_cache(org_id) on commit
        Celery->>DB: query VPNs by organization_id
        loop each VPN
            Celery->>GetVpnView: invalidate_get_vpn_cache(vpn)
            Celery->>VPN: invalidate_checksum_cache()
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title '[feature] Invalidate VPN cache when organization config variables change' directly and clearly describes the main objective of the PR - adding automatic VPN cache invalidation when organization configuration variables change.
Description check ✅ Passed The PR description covers all required template sections: contributor checklist (with three items checked), issue reference (#1098), detailed description of changes across multiple files, and acknowledges UI changes are not applicable.
Linked Issues check ✅ Passed The code changes implement the core requirement from issue #1098: automatically invalidating VPN cache when organization configuration variables change, via signal handler, Celery task, and comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing automatic VPN cache invalidation for organization config changes. Minor test refactoring (removing inactive org tests) aligns with the new implementation pattern using transaction.on_commit.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dfc4ee7 and ed6beb7.

📒 Files selected for processing (2)
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/tests/test_handlers.py
🧰 Additional context used
🧠 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
  • openwisp_controller/config/tests/test_handlers.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/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
  • openwisp_controller/config/tests/test_handlers.py
🧬 Code graph analysis (2)
openwisp_controller/config/base/multitenancy.py (3)
openwisp_controller/config/base/device_group.py (1)
  • save (79-90)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
openwisp_controller/config/tasks.py (2)
  • bulk_invalidate_config_get_cached_checksum (199-201)
  • invalidate_organization_vpn_cache (104-122)
openwisp_controller/config/tests/test_handlers.py (3)
openwisp_controller/config/tests/test_controller.py (1)
  • _create_org (44-49)
openwisp_controller/config/base/multitenancy.py (1)
  • save (63-92)
openwisp_controller/config/tasks.py (2)
  • invalidate_organization_vpn_cache (104-122)
  • bulk_invalidate_config_get_cached_checksum (199-201)
⏰ 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). (9)
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | 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
🔇 Additional comments (9)
openwisp_controller/config/base/multitenancy.py (3)

5-11: Clear and necessary imports for transactional cache invalidation.

The added imports are directly used in the updated save flow and tasks dispatch.


50-55: Good snapshot of initial context, including deferred-field handling.

Capturing DEFERRED vs deepcopy upfront keeps change detection reliable.


66-92: Robust change detection with commit-time invalidation.

Context change tracking, deferred handling, and on-commit task dispatch are consistent and safe.

openwisp_controller/config/tests/test_handlers.py (6)

21-27: Inactive→active transition path is well covered.

The added branch improves the handler test’s completeness.


29-42: Helper isolates config settings creation cleanly.

Good reuse point for the tests below.


43-57: Strong positive-path test for context change invalidation.

Validates both VPN and config cache invalidation tasks with on-commit execution.


58-66: Creation path correctly asserts no invalidation.

This matches the intended semantics for new objects.


67-80: Inactive org path is validated correctly.

Ensures no invalidation when the organization is inactive.


81-95: No-op updates are safely covered.

The test verifies no cache churn when context is unchanged.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

@nemesifier
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@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

🧹 Nitpick comments (1)
openwisp_controller/config/tasks.py (1)

111-115: Move import outside the loop for efficiency.

The import statement is executed on every iteration, which is unnecessary overhead.

🔎 Proposed fix
     Vpn = load_model("config", "Vpn")
+    from .controller.views import GetVpnView

     for vpn in Vpn.objects.filter(organization_id=organization_id).iterator():
-        from .controller.views import GetVpnView
-
         GetVpnView.invalidate_get_vpn_cache(vpn)
         vpn.invalidate_checksum_cache()
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 828dfb3 and 30475e5.

📒 Files selected for processing (4)
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_handlers.py
💤 Files with no reviewable changes (1)
  • openwisp_controller/config/tests/test_handlers.py
🧰 Additional context used
🧬 Code graph analysis (3)
openwisp_controller/config/apps.py (2)
openwisp_controller/config/handlers.py (1)
  • organization_config_settings_change_handler (157-171)
openwisp_controller/config/base/vpn.py (1)
  • post_save (905-916)
openwisp_controller/config/handlers.py (2)
openwisp_controller/config/tasks.py (1)
  • invalidate_organization_vpn_cache (104-115)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
openwisp_controller/config/tasks.py (3)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
openwisp_controller/config/controller/views.py (2)
  • GetVpnView (485-516)
  • invalidate_get_vpn_cache (508-516)
openwisp_controller/config/base/base.py (2)
  • invalidate_checksum_cache (57-64)
  • invalidate_checksum_cache (92-94)
🪛 GitHub Actions: OpenWISP Controller CI Build
openwisp_controller/config/apps.py

[error] 342-342: AttributeError: 'ConfigConfig' object has no attribute 'organization_config_settings_model' during Django app ready initialization.

openwisp_controller/config/handlers.py

[error] 168-168: Black check failed. E501 line too long (100 > 88 characters).

🪛 Ruff (0.14.10)
openwisp_controller/config/handlers.py

157-157: Unused function argument: kwargs

(ARG001)

🔇 Additional comments (3)
openwisp_controller/config/handlers.py (1)

157-171: LGTM on the handler logic.

The implementation correctly:

  • Skips newly-added instances via _state.adding
  • Fetches only the context field from the DB for comparison
  • Uses transaction.on_commit to ensure the task runs after the transaction commits
  • Handles DoesNotExist gracefully

The **kwargs parameter is correctly included for signal handler forward compatibility (the Ruff warning is a false positive for signal handlers).

openwisp_controller/config/tasks.py (1)

103-116: LGTM on the task implementation.

The task correctly:

  • Uses soft_time_limit=7200 consistent with other cache invalidation tasks
  • Filters VPNs by organization using .iterator() for memory efficiency
  • Calls both invalidate_get_vpn_cache and invalidate_checksum_cache to fully refresh VPN caches

This aligns with the pattern used in invalidate_controller_views_cache (lines 197-215), with the additional invalidate_checksum_cache() call needed when organization config variables change.

openwisp_controller/config/apps.py (1)

269-275: LGTM on the import addition.

The handler import is correctly placed alongside the other cache invalidation handlers.

- Added missing model definition that was causing AttributeError
- Fixed line length for black formatting
- Actually implemented the VPN cache invalidation logic
- Moved import out of loop

Should fix the CI failures now.
Copy link

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

🧹 Nitpick comments (1)
openwisp_controller/config/tasks.py (1)

103-115: Consider adding .only("id") and SoftTimeLimitExceeded handling for consistency.

The task works correctly but could be improved for consistency with similar tasks in this file:

  1. The existing invalidate_controller_views_cache (line 212) uses .only("id").iterator() to reduce memory overhead.
  2. Other tasks with soft_time_limit=7200 (e.g., update_template_related_config_status) handle SoftTimeLimitExceeded to log meaningful errors.
🔎 Proposed improvements
 @shared_task(soft_time_limit=7200)
 def invalidate_organization_vpn_cache(organization_id):
     """
     Invalidates VPN cache for all VPNs in an organization when
     organization configuration variables change.
     """
     Vpn = load_model("config", "Vpn")
     from .controller.views import GetVpnView
-    
-    for vpn in Vpn.objects.filter(organization_id=organization_id).iterator():
-        GetVpnView.invalidate_get_vpn_cache(vpn)
-        vpn.invalidate_checksum_cache()
+
+    try:
+        for vpn in (
+            Vpn.objects.filter(organization_id=organization_id).only("id").iterator()
+        ):
+            GetVpnView.invalidate_get_vpn_cache(vpn)
+            vpn.invalidate_checksum_cache()
+    except SoftTimeLimitExceeded:
+        logger.error(
+            "soft time limit hit while executing "
+            f"invalidate_organization_vpn_cache for organization {organization_id}"
+        )
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 30475e5 and e77e6d3.

📒 Files selected for processing (3)
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tasks.py
🧰 Additional context used
🧬 Code graph analysis (3)
openwisp_controller/config/tasks.py (3)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
openwisp_controller/config/controller/views.py (2)
  • GetVpnView (485-516)
  • invalidate_get_vpn_cache (508-516)
openwisp_controller/config/base/base.py (2)
  • invalidate_checksum_cache (57-64)
  • invalidate_checksum_cache (92-94)
openwisp_controller/config/apps.py (2)
openwisp_controller/config/handlers.py (1)
  • organization_config_settings_change_handler (157-174)
openwisp_controller/config/base/vpn.py (1)
  • post_save (905-916)
openwisp_controller/config/handlers.py (2)
openwisp_controller/config/tasks.py (1)
  • invalidate_organization_vpn_cache (104-114)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
🪛 Ruff (0.14.10)
openwisp_controller/config/handlers.py

157-157: Unused function argument: kwargs

(ARG001)

⏰ 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.10 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.13 | 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~=5.1.0
🔇 Additional comments (3)
openwisp_controller/config/apps.py (3)

64-66: LGTM!

The model loading is correctly added in __setmodels__, following the existing pattern.


276-277: LGTM!

The handler import is properly added alongside related handlers.


343-347: Signal connection looks correct, but depends on handler fix.

The signal connection follows the established pattern in this file. However, this will need to be updated if the handler is split into pre_save and post_save handlers as suggested in the handlers.py review.

Copy link

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

🧹 Nitpick comments (1)
openwisp_controller/config/tasks.py (1)

118-122: Consider using logging.exception for better debugging.

When catching exceptions, logging.exception automatically includes the traceback, which provides more context for diagnosing soft time limit issues.

🔎 Proposed improvement
     except SoftTimeLimitExceeded:
-        logger.error(
+        logger.exception(
             "soft time limit hit while executing "
             f"invalidate_organization_vpn_cache for organization {organization_id}"
         )

Based on static analysis hints.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e77e6d3 and b48cac9.

📒 Files selected for processing (3)
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tasks.py
🧰 Additional context used
🧬 Code graph analysis (3)
openwisp_controller/config/handlers.py (2)
openwisp_controller/config/tasks.py (1)
  • invalidate_organization_vpn_cache (104-122)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
openwisp_controller/config/apps.py (2)
openwisp_controller/config/handlers.py (2)
  • organization_config_settings_pre_save_handler (157-167)
  • organization_config_settings_post_save_handler (170-180)
openwisp_controller/config/base/vpn.py (1)
  • post_save (905-916)
openwisp_controller/config/tasks.py (3)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
openwisp_controller/config/controller/views.py (2)
  • GetVpnView (485-516)
  • invalidate_get_vpn_cache (508-516)
openwisp_controller/config/base/base.py (2)
  • invalidate_checksum_cache (57-64)
  • invalidate_checksum_cache (92-94)
🪛 Ruff (0.14.10)
openwisp_controller/config/handlers.py

157-157: Unused function argument: kwargs

(ARG001)


170-170: Unused function argument: kwargs

(ARG001)

openwisp_controller/config/tasks.py

119-122: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

⏰ 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). (9)
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | 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.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🔇 Additional comments (4)
openwisp_controller/config/tasks.py (1)

103-117: LGTM! Efficient implementation following codebase patterns.

The task correctly:

  • Uses .only("id").iterator() for memory-efficient iteration
  • Calls both GetVpnView.invalidate_get_vpn_cache() and vpn.invalidate_checksum_cache() to ensure complete cache invalidation
  • Follows the same patterns as other tasks in this file
openwisp_controller/config/handlers.py (2)

157-167: Excellent implementation of the pre-save pattern.

The handler correctly:

  • Returns early for new objects (no original context to compare)
  • Uses .only("context") to efficiently load just the needed field
  • Stores the original context on the instance for comparison in post-save
  • Handles the edge case where the DB record doesn't exist

170-180: Correct post-save implementation with proper change detection.

The handler correctly:

  • Retrieves the original context stored by the pre-save handler
  • Only triggers invalidation when context actually changed (not None and differs)
  • Uses transaction.on_commit to ensure the task is only scheduled after a successful database commit
  • Converts organization_id to string for Celery serialization

This addresses the previous critical issue where the comparison logic was inverted.

openwisp_controller/config/apps.py (1)

64-66: Proper model loading and signal wiring.

The changes correctly:

  • Load OrganizationConfigSettings model in __setmodels__() (lines 64-66), addressing the previous AttributeError
  • Import both handlers in the appropriate location (lines 276-278)
  • Wire pre_save and post_save signals with descriptive dispatch_uid values (lines 344-353)

The signal connections follow the established patterns in this file and correctly link the handlers to the model.

Also applies to: 276-278, 344-353

@Srinath0916 Srinath0916 force-pushed the feature/vpn-cache-invalidation-1098 branch from b48cac9 to f6a46c7 Compare December 31, 2025 01:06
Copy link

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

🧹 Nitpick comments (1)
openwisp_controller/config/tasks.py (1)

118-122: Consider using logging.exception for better debugging context.

The static analysis tool suggests using logging.exception instead of logging.error when logging from an exception handler. This automatically includes the stack trace, which aids debugging.

🔎 Proposed fix
     except SoftTimeLimitExceeded:
-        logger.error(
+        logger.exception(
             "soft time limit hit while executing "
             f"invalidate_organization_vpn_cache for organization {organization_id}"
         )

Based on static analysis hints.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b48cac9 and f6a46c7.

📒 Files selected for processing (3)
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tasks.py
🧰 Additional context used
🧬 Code graph analysis (2)
openwisp_controller/config/handlers.py (2)
openwisp_controller/config/tasks.py (1)
  • invalidate_organization_vpn_cache (104-122)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
openwisp_controller/config/tasks.py (3)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
openwisp_controller/config/controller/views.py (2)
  • GetVpnView (485-516)
  • invalidate_get_vpn_cache (508-516)
openwisp_controller/config/base/base.py (2)
  • invalidate_checksum_cache (57-64)
  • invalidate_checksum_cache (92-94)
🪛 Ruff (0.14.10)
openwisp_controller/config/handlers.py

157-157: Unused function argument: kwargs

(ARG001)


170-170: Unused function argument: kwargs

(ARG001)

openwisp_controller/config/tasks.py

119-122: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

⏰ 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.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.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.12 | django~=5.1.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
🔇 Additional comments (6)
openwisp_controller/config/handlers.py (2)

157-167: LGTM! Pre-save handler correctly captures original context.

The handler properly:

  • Skips new instances using _state.adding
  • Efficiently fetches only the context field from the database
  • Stores the original value for comparison in post_save
  • Handles the edge case where the instance might not exist in the database

The kwargs parameter flagged by static analysis is required for Django signal handlers and can be safely ignored.


170-180: LGTM! Post-save handler correctly detects changes and schedules invalidation.

The handler properly:

  • Retrieves the original context stored by the pre_save handler
  • Only triggers invalidation when context actually changes
  • Uses transaction.on_commit to ensure the task runs after successful database commit
  • Passes the organization ID as a string for Celery serialization

The logic correctly avoids false triggers on new instances and unchanged updates. The kwargs parameter flagged by static analysis is required for Django signal handlers.

openwisp_controller/config/apps.py (3)

64-66: LGTM! Model loading follows established patterns.

The organization_config_settings_model is loaded correctly using load_model and assigned to the app config instance, consistent with other models in __setmodels__. This resolves the AttributeError mentioned in past reviews.


276-277: LGTM! Handler imports are correct.

Both organization_config_settings_pre_save_handler and organization_config_settings_post_save_handler are imported in the appropriate location within enable_cache_invalidation, following the existing pattern for other cache invalidation handlers.


344-353: LGTM! Signal connections properly wired.

The pre_save and post_save signals are correctly connected to their respective handlers with:

  • Proper sender model (self.organization_config_settings_model)
  • Unique, descriptive dispatch_uid values following the naming convention
  • Correct placement within enable_cache_invalidation alongside other cache invalidation signals

This completes the signal wiring required for VPN cache invalidation when organization config settings change.

openwisp_controller/config/tasks.py (1)

103-117: No changes needed. The .only("id") approach is correct and sufficient for cache invalidation.

The cache invalidation methods call get_cached_checksum.invalidate(instance) and get_cached_configuration.invalidate(instance), which only require the instance's pk to compute the cache key (via get_cached_args_rewrite(instance) that returns instance.pk.hex). No additional fields are accessed during invalidation. This pattern is already used elsewhere in the codebase—bulk_invalidate_get_cached_checksum() uses the same .only("id").iterator() approach, confirming it is the intended pattern.

Likely an incorrect or invalid review comment.

The post_save handler was comparing identical values so changes were never detected.
Split into pre_save (store original) and post_save (compare & invalidate) handlers.
Also added .only('id') optimization and SoftTimeLimitExceeded handling as suggested.
@Srinath0916 Srinath0916 force-pushed the feature/vpn-cache-invalidation-1098 branch from f6a46c7 to cf5582b Compare December 31, 2025 01:23
Copy link

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

🧹 Nitpick comments (1)
openwisp_controller/config/tasks.py (1)

103-123: Code duplication: Consider extracting VPN cache invalidation logic.

This task duplicates the VPN iteration and cache invalidation pattern from invalidate_controller_views_cache (lines 219-222). Both iterate over VPNs filtered by organization_id, use .only("id").iterator(), and call GetVpnView.invalidate_get_vpn_cache(vpn).

The duplication is minimal (~4 lines), but extracting a helper function would improve maintainability if these patterns are used elsewhere in the future.

Note: The checksum cache invalidation in this task is intentional and appropriate. This task is called when organization config settings change (actual data modification), whereas invalidate_controller_views_cache is called when an organization is disabled (only cache visibility concerns), so they have different invalidation requirements.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6a46c7 and cf5582b.

📒 Files selected for processing (3)
  • openwisp_controller/config/apps.py
  • openwisp_controller/config/handlers.py
  • openwisp_controller/config/tasks.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • openwisp_controller/config/apps.py
🧰 Additional context used
🧬 Code graph analysis (2)
openwisp_controller/config/tasks.py (3)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
openwisp_controller/config/controller/views.py (2)
  • GetVpnView (485-516)
  • invalidate_get_vpn_cache (508-516)
openwisp_controller/config/base/base.py (2)
  • invalidate_checksum_cache (57-64)
  • invalidate_checksum_cache (92-94)
openwisp_controller/config/handlers.py (2)
openwisp_controller/config/tasks.py (1)
  • invalidate_organization_vpn_cache (104-122)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
🪛 Ruff (0.14.10)
openwisp_controller/config/handlers.py

157-157: Unused function argument: kwargs

(ARG001)


170-170: Unused function argument: kwargs

(ARG001)

⏰ 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.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
🔇 Additional comments (2)
openwisp_controller/config/handlers.py (2)

157-167: LGTM! Pre-save handler correctly captures original context.

The handler properly:

  • Returns early for new instances
  • Efficiently fetches only the context field from the database
  • Stores the original value for comparison in post-save
  • Handles the DoesNotExist exception

170-180: LGTM! Post-save handler correctly triggers cache invalidation.

The handler properly:

  • Retrieves the stored original context with a safe fallback
  • Only triggers invalidation when context actually changed
  • Uses transaction.on_commit to ensure the task runs only after successful database commit
  • Converts organization_id to string as expected by the Celery task

The previous critical issue regarding comparing identical values has been successfully resolved using the pre-save/post-save pattern.

…nwisp#1098

- Use getattr for version-safe _initial_context access
- Add behavioral tests covering all invalidation scenarios

Fixes openwisp#1098
@Srinath0916 Srinath0916 force-pushed the feature/vpn-cache-invalidation-1098 branch from d1c2da8 to 87d7730 Compare January 29, 2026 13:41
Copy link

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

60-75: Guard change detection when update_fields omits context.

If save(update_fields=...) excludes context, _initial_context is still unconditionally updated, creating a desync with the database that can suppress cache invalidation on subsequent saves. Only compare and update _initial_context when context is actually being persisted.

Proposed fix
     def save(
         self, force_insert=False, force_update=False, using=None, update_fields=None
     ):
         context_changed = False
-        if not self._state.adding:
+        context_will_be_saved = update_fields is None or "context" in update_fields
+        if not self._state.adding and context_will_be_saved:
             initial_context = getattr(self, "_initial_context", None)
             if initial_context is not None:
                 context_changed = initial_context != self.context
         super().save(force_insert, force_update, using, update_fields)
         if context_changed and self.organization.is_active:
             transaction.on_commit(
                 lambda: tasks.invalidate_organization_vpn_cache.delay(
                     str(self.organization_id)
                 )
             )
-        self._initial_context = deepcopy(self.context)
+        if context_will_be_saved:
+            self._initial_context = deepcopy(self.context)
🤖 Fix all issues with AI agents
In `@openwisp_controller/config/tests/test_handlers.py`:
- Around line 37-69: Wrap the DB operations that call config_settings.save() in
each test inside a captureOnCommitCallbacks context so transaction.on_commit
callbacks execute; for the four tests
(test_vpn_cache_invalidated_on_context_change,
test_no_cache_invalidation_on_create,
test_no_cache_invalidation_for_inactive_org,
test_no_cache_invalidation_if_context_unchanged) change the code to use with
self.captureOnCommitCallbacks(execute=True): around the code that mutates and
saves config_settings (and object creation where needed) so
tasks.invalidate_organization_vpn_cache.delay (the on_commit queued task)
actually runs during the test.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb03988 and d1c2da8.

📒 Files selected for processing (2)
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/tests/test_handlers.py
🧰 Additional context used
🧠 Learnings (3)
📚 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/base/multitenancy.py
📚 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
  • openwisp_controller/config/tests/test_handlers.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
  • openwisp_controller/config/tests/test_handlers.py
🧬 Code graph analysis (1)
openwisp_controller/config/base/multitenancy.py (3)
openwisp_controller/config/exceptions.py (1)
  • OrganizationDeviceLimitExceeded (5-20)
openwisp_controller/config/tasks.py (1)
  • invalidate_organization_vpn_cache (104-122)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
⏰ 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). (8)
  • GitHub Check: Python==3.10 | django~=4.2.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~=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~=5.1.0
🔇 Additional comments (3)
openwisp_controller/config/base/multitenancy.py (2)

4-12: Import updates look good.

The additions align with the new task wiring.


50-53: Snapshotting _initial_context on load is solid.

This sets up reliable change detection.

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

21-27: Mock reset before the subtest is a good tweak.

Keeps call assertions isolated between subtests.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

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

🤖 Fix all issues with AI agents
In `@openwisp_controller/config/base/multitenancy.py`:
- Around line 65-75: The save() logic must ignore context change detection and
avoid overwriting _initial_context when the caller used update_fields that does
not include "context": modify the code in multitenancy.save() to first detect if
update_fields is not None and "context" not in update_fields, and in that case
skip computing context_changed, skip scheduling
tasks.invalidate_organization_vpn_cache.delay, and do not update
self._initial_context; otherwise (when context is being saved or update_fields
is None) compute context_changed against self._initial_context and, if changed
and self.organization.is_active, call transaction.on_commit(lambda:
tasks.invalidate_organization_vpn_cache.delay(str(self.organization_id)),
using=using) so the callback uses the same DB connection, and finally
deepcopy(self.context) into self._initial_context only in the branch where
context was part of the save.
🧹 Nitpick comments (1)
openwisp_controller/config/base/multitenancy.py (1)

50-53: No evidence of deferred context field in queries.

I scanned the codebase for .only() and .defer() patterns on OrganizationConfigSettings. The only explicit optimization found is .only("context") at config/views.py:194, which includes context rather than deferring it. The main query in controller/views.py:367 uses .select_related("organization") without any field filtering. No .defer("context") patterns exist in the codebase.

While the concern is theoretically sound—accessing a deferred field in __init__ would trigger an unnecessary query—there is no practical evidence that this model is instantiated with context deferred. If performance optimization patterns emerge in the future, this would become relevant.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1c2da8 and 87d7730.

📒 Files selected for processing (2)
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/tests/test_handlers.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • openwisp_controller/config/tests/test_handlers.py
🧰 Additional context used
🧠 Learnings (3)
📚 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/base/multitenancy.py
📚 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-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
🧬 Code graph analysis (1)
openwisp_controller/config/base/multitenancy.py (3)
openwisp_controller/config/base/device_group.py (1)
  • save (79-90)
openwisp_controller/config/tasks.py (1)
  • invalidate_organization_vpn_cache (104-122)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
⏰ 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~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.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.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | 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
🔇 Additional comments (1)
openwisp_controller/config/base/multitenancy.py (1)

5-12: Imports updated appropriately for the new task usage.

No concerns here.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@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

🤖 Fix all issues with AI agents
In `@openwisp_controller/config/tests/test_handlers.py`:
- Around line 33-41: The local variable named created returned from
OrganizationConfigSettings.objects.get_or_create in the _get_org_config_settings
function is unused and triggers a Ruff RUF059 warning; rename it to _created or
_ (e.g., change "config_settings, created =
OrganizationConfigSettings.objects.get_or_create(...)" to "config_settings,
_created = ..." or "config_settings, _ = ...") so the unused value is explicitly
ignored while leaving the rest of _get_org_config_settings unchanged.
- Around line 44-81: The test method signatures exceed the project's 88-char
line length limit (E501); wrap the long decorated function definitions (e.g.,
test_vpn_cache_invalidated_on_context_change,
test_no_cache_invalidation_on_create,
test_no_cache_invalidation_for_inactive_org,
test_no_cache_invalidation_if_context_unchanged) so each decorator stays on its
own line and the def line is broken across multiple lines for its parameters (or
put one decorator per line and break the parameter list), ensuring the full
definition lines are <=88 chars and no noqa is used.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 87d7730 and c4c5f14.

📒 Files selected for processing (2)
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/tests/test_handlers.py
🧰 Additional context used
🧠 Learnings (3)
📚 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/base/multitenancy.py
  • openwisp_controller/config/tests/test_handlers.py
📚 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
  • openwisp_controller/config/tests/test_handlers.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
  • openwisp_controller/config/tests/test_handlers.py
🧬 Code graph analysis (2)
openwisp_controller/config/base/multitenancy.py (3)
openwisp_controller/config/base/vpn.py (2)
  • save (244-293)
  • save (870-878)
openwisp_controller/config/base/device_group.py (1)
  • save (79-90)
openwisp_controller/config/tasks.py (2)
  • invalidate_organization_vpn_cache (104-122)
  • bulk_invalidate_config_get_cached_checksum (199-201)
openwisp_controller/config/tests/test_handlers.py (3)
openwisp_controller/config/tests/test_controller.py (1)
  • _create_org (44-49)
openwisp_controller/config/base/multitenancy.py (1)
  • save (60-82)
openwisp_controller/config/tasks.py (2)
  • invalidate_organization_vpn_cache (104-122)
  • bulk_invalidate_config_get_cached_checksum (199-201)
🪛 Ruff (0.14.14)
openwisp_controller/config/tests/test_handlers.py

38-38: Unpacked variable created is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

⏰ 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~=4.2.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.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
🔇 Additional comments (5)
openwisp_controller/config/base/multitenancy.py (3)

5-11: Imports for transaction + tasks are appropriate.

No concerns with these additions.


50-53: Baseline context snapshot is clear and safe.

Deepcopying the initial context provides a stable baseline for change detection.


60-82: Change detection + on_commit invalidation flow looks solid.

The update_fields guard and on_commit callback are well-placed, and updating _initial_context only when context is saved avoids false positives.

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

3-5: Test utility import looks fine.

No concerns here.


22-27: Subtest isolation is clearer now.

Resetting the mock before the inactive→active case keeps assertions independent.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@Srinath0916 Srinath0916 force-pushed the feature/vpn-cache-invalidation-1098 branch from c4c5f14 to 5bca3cd Compare January 29, 2026 14:53
Copy link

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

🤖 Fix all issues with AI agents
In `@openwisp_controller/config/base/multitenancy.py`:
- Around line 50-55: AbstractOrganizationConfigSettings.__init__ calls
self._is_deferred("context") but _is_deferred is only defined on AbstractDevice,
causing AttributeError; fix by either implementing _is_deferred on
AbstractOrganizationConfigSettings (copy the logic from
AbstractDevice._is_deferred) or refactor the init to check deferred state via
self.get_deferred_fields() (e.g., treat "context" as deferred if "context" in
self.get_deferred_fields()); update both occurrences (the checks at
initialization and the later use at line ~84) to use the new _is_deferred
implementation or the get_deferred_fields() approach so the deferred-field
detection works without relying on AbstractDevice.
🧹 Nitpick comments (2)
openwisp_controller/config/base/multitenancy.py (1)

73-82: Inconsistent transaction semantics between the two cache invalidation calls.

bulk_invalidate_config_get_cached_checksum.delay() is called immediately after super().save(), while invalidate_organization_vpn_cache is wrapped in transaction.on_commit. If an outer transaction wraps this save and later rolls back, the config cache will be invalidated but the VPN cache won't, leading to inconsistent state.

Consider wrapping both in on_commit for atomicity, or document why the asymmetry is intentional.

Suggested fix for consistent transaction handling
         if context_changed and self.organization.is_active:
-            tasks.bulk_invalidate_config_get_cached_checksum.delay(
-                {"device__organization_id": str(self.organization_id)}
-            )
-            transaction.on_commit(
-                lambda: tasks.invalidate_organization_vpn_cache.delay(
-                    str(self.organization_id)
-                ),
-                using=using,
-            )
+            org_id = str(self.organization_id)
+            transaction.on_commit(
+                lambda: tasks.bulk_invalidate_config_get_cached_checksum.delay(
+                    {"device__organization_id": org_id}
+                ),
+                using=using,
+            )
+            transaction.on_commit(
+                lambda: tasks.invalidate_organization_vpn_cache.delay(org_id),
+                using=using,
+            )
openwisp_controller/config/tests/test_handlers.py (1)

80-94: Consider adding a test for update_fields excluding context.

The implementation in multitenancy.py has special handling for when save(update_fields=...) is called without "context". Adding a test case would ensure this edge case is covered:

`@patch.object`(tasks.invalidate_organization_vpn_cache, "delay")
`@patch.object`(tasks.bulk_invalidate_config_get_cached_checksum, "delay")
def test_no_cache_invalidation_when_context_not_in_update_fields(
    self, config_cache_mock, vpn_cache_mock
):
    """Test no invalidation when save uses update_fields without context"""
    config_settings = self._get_org_config_settings()
    config_settings.context = {"new": "context"}
    with self.captureOnCommitCallbacks(execute=True):
        config_settings.save(update_fields=["registration_enabled"])
    vpn_cache_mock.assert_not_called()
    config_cache_mock.assert_not_called()
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4c5f14 and 5bca3cd.

📒 Files selected for processing (2)
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/tests/test_handlers.py
🧰 Additional context used
🧠 Learnings (3)
📚 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/base/multitenancy.py
📚 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
  • openwisp_controller/config/tests/test_handlers.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
  • openwisp_controller/config/tests/test_handlers.py
🧬 Code graph analysis (2)
openwisp_controller/config/base/multitenancy.py (3)
openwisp_controller/config/base/device.py (2)
  • _is_deferred (304-308)
  • save (270-288)
openwisp_controller/config/base/device_group.py (1)
  • save (79-90)
openwisp_controller/config/tasks.py (2)
  • bulk_invalidate_config_get_cached_checksum (199-201)
  • invalidate_organization_vpn_cache (104-122)
openwisp_controller/config/tests/test_handlers.py (3)
openwisp_controller/config/tests/test_controller.py (1)
  • _create_org (44-49)
openwisp_controller/config/base/multitenancy.py (1)
  • save (63-87)
openwisp_controller/config/tasks.py (2)
  • invalidate_organization_vpn_cache (104-122)
  • bulk_invalidate_config_get_cached_checksum (199-201)
🪛 Ruff (0.14.14)
openwisp_controller/config/tests/test_handlers.py

37-37: Unpacked variable created is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

🔇 Additional comments (5)
openwisp_controller/config/base/multitenancy.py (2)

50-55: Good use of __init__ for initial state tracking.

Storing _initial_context at initialization avoids the extra database query that DeviceGroup.save() uses (see base/device_group.py:78-89). The handling of models.DEFERRED state is correct for cases where .only() or .defer() excludes the context field.


66-71: Context change detection logic looks correct.

The guard for context_in_update properly handles update_fields scenarios, and the models.DEFERRED check prevents false positives when the field was never loaded. This addresses the concerns raised in previous reviews.

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

42-55: Good test coverage for context change invalidation.

The test correctly patches both task delays, uses captureOnCommitCallbacks(execute=True) to trigger the on_commit callback, and verifies both caches are invalidated with the correct organization ID.


57-64: Comprehensive negative test cases.

The tests properly cover:

  • No invalidation on creation (new objects shouldn't trigger cache invalidation)
  • No invalidation for inactive organizations
  • No invalidation when context remains unchanged

All use captureOnCommitCallbacks correctly to ensure proper transaction handling during tests.

Also applies to: 66-78, 80-94


21-26: Good improvement to test isolation.

Adding reset_mock() and the intermediate assertions for creating an inactive org ensures cleaner test state and more precise verification of when the task should/shouldn't be called.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@Srinath0916 Srinath0916 force-pushed the feature/vpn-cache-invalidation-1098 branch from 5bca3cd to 95a4c8d Compare January 30, 2026 05:21
Copy link

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

🤖 Fix all issues with AI agents
In `@openwisp_controller/config/base/multitenancy.py`:
- Around line 66-71: The current logic skips cache invalidation when
_initial_context == models.DEFERRED, which treats a deferred/unknown initial
state as "not changed"; update the check in the save/update path (the variables
context_changed, context_in_update, _initial_context, models.DEFERRED, and
self.context) so that if the instance is not adding and context_in_update is
True and _initial_context is models.DEFERRED you conservatively treat the
context as changed (i.e., set context_changed = True), otherwise keep the
existing comparison behavior (initial_context != self.context) for non-deferred
initial values; this ensures deferred fields trigger invalidation when updated.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5bca3cd and 95a4c8d.

📒 Files selected for processing (2)
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/tests/test_handlers.py
🧰 Additional context used
🧠 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
  • openwisp_controller/config/tests/test_handlers.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/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
  • openwisp_controller/config/tests/test_handlers.py
🧬 Code graph analysis (1)
openwisp_controller/config/tests/test_handlers.py (3)
openwisp_controller/config/tests/test_controller.py (1)
  • _create_org (44-49)
openwisp_controller/config/base/multitenancy.py (1)
  • save (63-87)
openwisp_controller/config/tasks.py (2)
  • invalidate_organization_vpn_cache (104-122)
  • bulk_invalidate_config_get_cached_checksum (199-201)
⏰ 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~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | 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.10 | django~=4.2.0
🔇 Additional comments (3)
openwisp_controller/config/base/multitenancy.py (1)

50-56: Baseline tracking looks good.
The deferred-field guard keeps _initial_context consistent.

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

21-27: Nice improvement to inactive-org coverage.
Resetting the mock before toggling activation makes the assertions clearer.


29-95: Solid test matrix for cache invalidation.
Covers change, create, inactive org, and unchanged context paths with clear mock assertions.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@Srinath0916 Srinath0916 force-pushed the feature/vpn-cache-invalidation-1098 branch from 95a4c8d to 82cfe2a Compare January 30, 2026 05:39
coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 30, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 30, 2026
@Srinath0916
Copy link
Contributor Author

@nemesifier @stktyagi Feedback addressed, please have a look and review it.

@Srinath0916 Srinath0916 force-pushed the feature/vpn-cache-invalidation-1098 branch from 81ccddf to dfc4ee7 Compare February 2, 2026 10:48
Copy link

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

🤖 Fix all issues with AI agents
In `@openwisp_controller/config/base/multitenancy.py`:
- Around line 77-86: The bulk_invalidate_config_get_cached_checksum task is
dispatched immediately while invalidate_organization_vpn_cache is deferred with
transaction.on_commit, causing inconsistent behavior on rollback; change the
call to tasks.bulk_invalidate_config_get_cached_checksum.delay(...) so it is
executed inside transaction.on_commit (like the
invalidate_organization_vpn_cache call), using the same transaction.on_commit
wrapper and preserving the filter payload {"device__organization_id":
str(self.organization_id)} and the using parameter, conditioned on
context_changed and self.organization.is_active.
🧹 Nitpick comments (1)
openwisp_controller/config/base/multitenancy.py (1)

72-75: Redundant context_in_update check.

The condition and context_in_update on line 72 is redundant since the outer if on line 68 already guarantees context_in_update is True. This doesn't affect correctness but reduces readability.

Proposed simplification
         if not self._state.adding and context_in_update:
             initial_context = getattr(self, "_initial_context", None)
             if initial_context is not None and initial_context != models.DEFERRED:
                 context_changed = initial_context != self.context
-            elif initial_context == models.DEFERRED and context_in_update:
+            elif initial_context == models.DEFERRED:
                 # Conservative: if we don't know initial state and context is
                 # being updated, assume it changed to avoid stale cache
                 context_changed = True
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81ccddf and dfc4ee7.

📒 Files selected for processing (3)
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/tasks.py
  • openwisp_controller/config/tests/test_handlers.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • openwisp_controller/config/tasks.py
🧰 Additional context used
🧠 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
  • openwisp_controller/config/tests/test_handlers.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/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
  • openwisp_controller/config/tests/test_handlers.py
🧬 Code graph analysis (2)
openwisp_controller/config/base/multitenancy.py (2)
openwisp_controller/config/tasks.py (2)
  • bulk_invalidate_config_get_cached_checksum (198-200)
  • invalidate_organization_vpn_cache (104-121)
openwisp_controller/geo/base/models.py (1)
  • organization_id (64-65)
openwisp_controller/config/tests/test_handlers.py (3)
openwisp_controller/config/tests/test_controller.py (1)
  • _create_org (44-49)
openwisp_controller/config/base/multitenancy.py (1)
  • save (63-91)
openwisp_controller/config/tasks.py (2)
  • invalidate_organization_vpn_cache (104-121)
  • bulk_invalidate_config_get_cached_checksum (198-200)
⏰ 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.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.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~=5.1.0
🔇 Additional comments (3)
openwisp_controller/config/base/multitenancy.py (1)

50-55: LGTM - Clean initialization with proper deferred field handling.

The __init__ method correctly snapshots the initial context, handling deferred fields via get_deferred_fields(). This avoids the previous _is_deferred method issue.

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

21-26: LGTM - Proper mock isolation for subtest.

Adding reset_mock() before creating inactive_org ensures clean state for the "inactive to active" test scenario. Good practice for accurate assertions.


29-95: Comprehensive test coverage for VPN cache invalidation.

The new test class correctly covers:

  • Context change triggers both cache invalidations
  • Creation does not trigger invalidation
  • Inactive organizations are skipped
  • Unchanged context does not trigger invalidation

The proper use of captureOnCommitCallbacks(execute=True) ensures transaction.on_commit callbacks execute during tests. Both config_cache_mock and vpn_cache_mock are verified in each test, aligning with the dual invalidation logic in multitenancy.py.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@Srinath0916 Srinath0916 force-pushed the feature/vpn-cache-invalidation-1098 branch from dfc4ee7 to 6542c80 Compare February 2, 2026 11:12
- Fixed transaction.on_commit callback structure
- Added proper config cache invalidation alongside VPN cache
- Updated tests to use captureOnCommitCallbacks for transaction testing
- Fixed update_fields handling to prevent unnecessary invalidations

Fixes openwisp#1098
@Srinath0916 Srinath0916 force-pushed the feature/vpn-cache-invalidation-1098 branch from 6542c80 to ed6beb7 Compare February 2, 2026 11:14
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.

[feature] Invalidate VPN cache when Organization Configuration variable changes

5 participants