Skip to content

[fix] Prevent update_config task from detecting itself as duplicate#1278

Closed
mn-ram wants to merge 1 commit intoopenwisp:masterfrom
mn-ram:fix/update-config-self-detection
Closed

[fix] Prevent update_config task from detecting itself as duplicate#1278
mn-ram wants to merge 1 commit intoopenwisp:masterfrom
mn-ram:fix/update-config-self-detection

Conversation

@mn-ram
Copy link

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

Closes #1277

Fix self-detection in _is_update_in_progress causing skipped config pushes

Problem

The _is_update_in_progress() helper checks Celery's inspect().active() to detect if an update_config task is already running for the same device.
However, this list includes the currently executing task itself. Since the function does not exclude the current task ID, it always detects the running task as a duplicate and returns True.

As a result, the update_config task exits early:

if _is_update_in_progress(device_id):
    return

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2bb995d8-aa23-41bf-a52f-d15972f7770c

📥 Commits

Reviewing files that changed from the base of the PR and between f3c99c4 and 0952f5b.

📒 Files selected for processing (3)
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.py
📜 Recent 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/connection/tests/test_models.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/connection/tasks.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/connection/tests/test_models.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/connection/tasks.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/connection/tests/test_models.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/connection/tasks.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/connection/tests/test_models.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/connection/tasks.py
🧠 Learnings (8)
📓 Common learnings
Learnt from: pandafy
Repo: openwisp/openwisp-controller PR: 1292
File: openwisp_controller/connection/tasks.py:27-31
Timestamp: 2026-03-17T09:20:04.547Z
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.
📚 Learning: 2026-03-17T09:20:04.547Z
Learnt from: pandafy
Repo: openwisp/openwisp-controller PR: 1292
File: openwisp_controller/connection/tasks.py:27-31
Timestamp: 2026-03-17T09:20:04.547Z
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/connection/tests/test_models.py
  • openwisp_controller/connection/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/connection/tests/test_models.py
📚 Learning: 2026-02-05T20:06:01.589Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.

Applied to files:

  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/connection/tasks.py
📚 Learning: 2026-03-14T19:34:33.773Z
Learnt from: CR
Repo: openwisp/openwisp-controller PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-03-14T19:34:33.773Z
Learning: Applies to **/*{.test,.spec,_test,test_}.{js,ts,tsx,jsx,py,java,go,cs,rb} : Tests must be updated to cover non-trivial changes and ensure proper validation of modified behavior

Applied to files:

  • openwisp_controller/connection/tests/test_models.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/connection/tests/test_models.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/connection/tasks.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/connection/tests/test_models.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/connection/tasks.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/connection/tests/test_models.py
  • openwisp_controller/config/tests/test_config.py
  • openwisp_controller/connection/tasks.py
🔇 Additional comments (9)
openwisp_controller/connection/tasks.py (4)

18-21: LGTM!

The lock key pattern and timeout constants are well-documented. The 5-minute TTL provides a reasonable safety net for crash recovery without being so short that it causes issues with longer-running updates.


24-36: LGTM!

The lock acquisition using cache.add() is atomic and correctly implements the lock-or-fail pattern. The UUID token ensures only the lock owner can release it.


39-47: LGTM!

The token ownership validation correctly prevents a late-finishing task from releasing another task's lock after TTL expiry. The silent no-op on token mismatch is appropriate since this is an expected race condition, not an error.


70-89: LGTM!

The lock placement after device validation is a good optimization—avoiding unnecessary lock acquisition for non-existent or non-updatable devices. The finally block ensures proper cleanup, and the logging follows the coding guidelines (info for normal flow, warning for connection issues).

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

987-988: LGTM!

The updated assertion and comment correctly account for the additional cache.delete call from _release_update_config_lock in the new locking mechanism.

openwisp_controller/connection/tests/test_models.py (4)

24-28: LGTM!

Imports correctly added for the new locking functions being tested.


1029-1045: LGTM!

The test correctly validates that when _acquire_update_config_lock returns None (lock held by another task), the update is skipped without attempting to get a connection or update the config.


1047-1071: LGTM!

Excellent test coverage for the happy path. The assertion on mocked_release.assert_called_once_with(str(conf.device.pk), "fake-lock-token") validates both that the lock is released and that the correct token is passed, ensuring the finally block contract is verified.


1073-1097: LGTM!

Comprehensive tests for the lock mechanism:

  • Tests real cache behavior (not just mocks)
  • addCleanup ensures proper cleanup even on test failure
  • Covers acquire, re-acquire failure, release, and wrong-token scenarios

The token ownership test at lines 1088-1097 is particularly important for validating the security property that prevents one task from releasing another's lock.


📝 Walkthrough

Walkthrough

Replaced in-flight Celery Inspect checks with a per-device lock stored in Django cache in openwisp_controller/connection/tasks.py. Added _acquire_update_config_lock(device_id) which returns a token when lock acquired (or None), and _release_update_config_lock(device_id, token) to release it. update_config now tries to acquire the lock, skips if unavailable, performs the existing device lookup and update when locked, and always releases the lock in a finally block. Tests were updated and new tests added for lock acquire/release semantics.

Sequence Diagram(s)

sequenceDiagram
    participant Worker as "Celery Worker\n(update_config)"
    participant Cache as "Django Cache\n(per-device lock)"
    participant DB as "Database\n(Device)"
    rect rgba(0,128,0,0.5)
    Worker->>Cache: _acquire_update_config_lock(device_id)
    end
    alt lock acquired
        Worker->>DB: fetch device & perform update_config(device_id)
        DB-->>Worker: update result
        rect rgba(0,0,255,0.5)
        Worker->>Cache: _release_update_config_lock(device_id, token)
        end
    else lock not acquired
        Worker->>Worker: log "skipping update - lock held"
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (3 errors, 1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❌ Error The PR addresses fixing self-detection in update_config tasks, but the linked issue #1277 focuses on VpnClient post_delete signals during template removal, which are unrelated topics. Verify the correct linked issue is referenced. The PR appears to address a different bug than #1277 (update_config self-detection vs VpnClient signal handling).
Out of Scope Changes check ❌ Error The changes implement a per-device lock mechanism for update_config tasks and update related tests, which aligns with the PR objective of fixing self-detection, though this differs from the linked issue scope. Address the mismatch between the PR objective (update_config self-detection fix) and the linked issue #1277 (VpnClient post_delete signal fix) by either updating the linked issue reference or verifying both fixes are included.
Bug Fixes ❌ Error Regression tests mock lock functions instead of testing with real locks, preventing verification that the original bug where tasks detect themselves as duplicates is actually fixed. Replace mocked lock assertions with tests using real lock mechanisms, or add integration test invoking update_config.delay() multiple times to verify only one task executes while others are skipped.
Description check ❓ Inconclusive The description covers the problem statement and relates to the linked issue, though the description appears incomplete as it cuts off mid-code. Complete the description by explaining the solution and verifying all template sections are properly filled out.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main fix: replacing the duplicate detection mechanism to prevent update_config from detecting itself.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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
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/connection/tasks.py`:
- Around line 55-56: Add an info-level log when the task returns early due to an
in-progress update: inside the branch that calls
_is_update_in_progress(device_id, current_task_id=self.request.id) and returns,
emit logger.info with the device_id and self.request.id (e.g., "Skipping
firmware update for device=%s: update in progress (task_id=%s)"). If logger is
not already defined in this module, ensure you obtain one via
logging.getLogger(__name__) before using it.

In `@openwisp_controller/connection/tests/test_models.py`:
- Around line 1033-1042: Add a test case that verifies a task with the same id
as the current task is ignored: when mocking celery.app.control.Inspect.active
(mocked_active) return value, include a task dict with "name": _TASK_NAME and
"args": [str(conf.device.pk)] but set "id" to the current task id (simulate
current_task_id) and assert the code path that checks for in-progress tasks does
NOT treat it as in-progress; update the existing test(s) that currently use
"other-task-id" (the block using mocked_active around _TASK_NAME and
conf.device.pk) and add the new assertion so the self-id branch is covered (also
apply the same addition to the similar case at lines 1061-1066).
🪄 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: 4805ecff-4012-40ec-b375-1caf68853b02

📥 Commits

Reviewing files that changed from the base of the PR and between f3c99c4 and 41bc1c9.

📒 Files selected for processing (2)
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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.1.0
  • GitHub Check: Python==3.12 | 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~=5.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | 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~=5.1.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/connection/tests/test_models.py
  • openwisp_controller/connection/tasks.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/connection/tests/test_models.py
  • openwisp_controller/connection/tasks.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/connection/tests/test_models.py
  • openwisp_controller/connection/tasks.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/connection/tests/test_models.py
  • openwisp_controller/connection/tasks.py
🧠 Learnings (5)
📚 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/connection/tests/test_models.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/connection/tests/test_models.py
  • openwisp_controller/connection/tasks.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/connection/tests/test_models.py
  • openwisp_controller/connection/tasks.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/connection/tests/test_models.py
  • openwisp_controller/connection/tasks.py
📚 Learning: 2026-02-05T20:06:01.589Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.

Applied to files:

  • openwisp_controller/connection/tasks.py
🔇 Additional comments (1)
openwisp_controller/connection/tasks.py (1)

19-30: Self-detection fix looks correct.

Binding the task and passing self.request.id into _is_update_in_progress correctly avoids treating the currently running update_config task as a duplicate.

Also applies to: 35-36, 55-55

@openwisp-companion
Copy link

CI Failures Detected

Hello @prakash-kalwaniya,
(Analysis for commit 41bc1c9)

There are Code Style/QA failures detected in the CI logs.

Failures & Remediation

  1. Code Style/QA: Black check failed!

    • Explanation: The black code formatter has found style violations in the codebase. The CI script run-qa-checks includes a check for black formatting, and it failed because the code does not adhere to the expected style.
    • Remediation: To fix this, please run the openwisp-qa-format command in your local environment. This command will automatically format your code according to the project's style guidelines, resolving the black check failures.
    openwisp-qa-format

@coveralls
Copy link

coveralls commented Mar 15, 2026

Coverage Status

coverage: 98.66% (+0.002%) from 98.658%
when pulling 0952f5b on prakash-kalwaniya:fix/update-config-self-detection
into f3c99c4 on openwisp:master.

@mn-ram mn-ram force-pushed the fix/update-config-self-detection branch from 77e7300 to f3832c5 Compare March 15, 2026 11:35
Copy link
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: 1

🤖 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/connection/tasks.py`:
- Around line 19-31: The current _is_update_in_progress(device_id,
current_task_id=None) uses a non-atomic snapshot of Celery active tasks and can
return True for both overlapping update_config tasks; replace this dedup logic
with an atomic per-device lock (e.g., a Redis/Django cache lock or database row
lock) that is acquired at task start and released at task end to prevent
concurrent updates for the same device. Update the task entrypoint
(update_config) to attempt to acquire a lock keyed by device_id before
proceeding, fail/exit if the lock cannot be acquired, and ensure the lock is
always released (or has a TTL) even on errors; you can keep _TASK_NAME and
current_task_id but remove reliance on inspect().active() and use the lock for
mutual exclusion.
🪄 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: c0792b9a-6c0c-4fbe-961d-e0c12d0707a6

📥 Commits

Reviewing files that changed from the base of the PR and between 77e7300 and f3832c5.

📒 Files selected for processing (2)
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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.11 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | 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.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=4.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~=5.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.py
🧠 Learnings (6)
📚 Learning: 2026-02-05T20:06:01.589Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.

Applied to files:

  • openwisp_controller/connection/tasks.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tests/test_models.py
📚 Learning: 2026-03-14T19:34:33.761Z
Learnt from: CR
Repo: openwisp/openwisp-controller PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-03-14T19:34:33.761Z
Learning: Applies to **/*{.test,.spec,_test,test_}.{js,ts,tsx,jsx,py,java,go,cs,rb} : Tests must be updated to cover non-trivial changes and ensure proper validation of modified behavior

Applied to files:

  • openwisp_controller/connection/tests/test_models.py
🔇 Additional comments (1)
openwisp_controller/connection/tests/test_models.py (1)

1072-1107: Good regression coverage for the self-id fix.

These tests cover both branches of the new current_task_id handling and would catch the original self-detection bug.

@mn-ram

This comment was marked as spam.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

🧩 Analysis chain

Let me fetch the current state of the code before reviewing.


🏁 Script executed:

#!/bin/bash
# Get current state of tasks.py
cat openwisp_controller/connection/tasks.py

echo "---SEPARATOR---"

# Get relevant test sections
cat openwisp_controller/connection/tests/test_models.py | grep -n "_is_update_in_progress\|cache\|update_config\|inspect\|active\|TTL\|lock" | head -80

Length of output: 5662


@prakash-kalwaniya: The new cache-lock approach looks solid. Here's a thorough assessment:


✅ What's well done

Aspect Assessment
Atomicity cache.add() is atomic — only one task wins the lock per device
Race condition eliminated Two overlapping tasks can no longer both skip each other
Safety TTL 5-minute timeout prevents stale lock if the process is killed hard (e.g. OOM, SIGKILL)
finally block Guarantees release on exceptions, SoftTimeLimitExceeded, etc.
Lock scope Acquired after time.sleep(2) and pre-checks — no unnecessary lock holding
Test coverage test_acquire_update_config_lock, test_device_update_config_in_progress, test_device_update_config_not_in_progress cover the key paths

🔍 One minor cleanup

@shared_task(bind=True) and the self parameter on update_config are now unused leftovers from the previous approach — self.request.id is no longer needed:

# Before (current state — bind=True is now unused)
`@shared_task`(bind=True)
def update_config(self, device_id):

# After (cleaner)
`@shared_task`
def update_config(device_id):

This is purely cosmetic but avoids confusing future readers into thinking self is used somewhere.


🤔 One edge case worth verifying

If the cache backend is unavailable (e.g. Redis is down), cache.add() may raise an exception or silently return False, causing every update_config call to be skipped. You may want to confirm what behaviour is acceptable for your deployment — but this is likely already a known trade-off with cache-based locking in general.


Overall, this is a meaningful improvement over the inspect().active() snapshot approach. The single actionable item is removing bind=True / self. Would you like me to trigger a fresh full review?

✅ 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.

@mn-ram mn-ram force-pushed the fix/update-config-self-detection branch from fbd3f5c to 09208c6 Compare March 15, 2026 15:42
@mn-ram

This comment was marked as spam.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

@prakash-kalwaniya: Noted — bind=True and self are cleaned up, and the cache-unavailability trade-off is understood for your deployment. Triggering a fresh full review now.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

✅ Actions performed

Full review triggered.

Copy link
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: 3

🤖 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/connection/tasks.py`:
- Around line 23-37: The current lock functions (_acquire_update_config_lock and
_release_update_config_lock) use a boolean value and unconditionally delete the
key, allowing one task to delete a lock another task owns; change to a tokenized
ownership lock: when acquiring in _acquire_update_config_lock generate a unique
token (UUID), store that token as the cache value via cache.add with the
configured timeout (ensure the timeout equals or exceeds the worst-case task
duration), and return the token to the caller; update callers to pass that token
into _release_update_config_lock; in _release_update_config_lock check the
current cache value and only delete the key if it matches the token (to avoid
deleting a newer lock owned by another task), otherwise leave the key intact and
return a failure/OK status accordingly.
- Around line 67-68: In the except NoWorkingDeviceConnectionError block add a
warning-level log before returning: call the module logger (use the existing
logger or logging.getLogger(__name__)) and emit logger.warning(...) with a short
message including the exception details and available context identifiers (e.g.,
device_id, connection_id or task id variables in this scope) so the silent
return is recorded; keep the return behavior unchanged.

In `@openwisp_controller/connection/tests/test_models.py`:
- Around line 1058-1069: The test currently patches
"_release_update_config_lock" but never asserts it was invoked; update the test
in test_models.py so after calling conf.full_clean() and conf.save() you assert
the release mock was called (e.g., assert_called_once()) to ensure the
finally/release path for _release_update_config_lock is exercised; reference the
patched symbol "_release_update_config_lock" (or capture it as a mocked_release
variable in the same mock.patch context) and add a single assertion after
mocked_update_config.assert_called_once() to validate the lock release.
🪄 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: bd1bc3ac-cf66-4921-94bc-004c6aa6f44b

📥 Commits

Reviewing files that changed from the base of the PR and between f3832c5 and fbd3f5c.

📒 Files selected for processing (2)
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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~=4.2.0
  • GitHub Check: Python==3.13 | 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.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.py
🧠 Learnings (7)
📓 Common learnings
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.
📚 Learning: 2026-02-05T20:06:01.589Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.

Applied to files:

  • openwisp_controller/connection/tasks.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tests/test_models.py
📚 Learning: 2026-03-14T19:34:33.761Z
Learnt from: CR
Repo: openwisp/openwisp-controller PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-03-14T19:34:33.761Z
Learning: Applies to **/*{.test,.spec,_test,test_}.{js,ts,tsx,jsx,py,java,go,cs,rb} : Tests must be updated to cover non-trivial changes and ensure proper validation of modified behavior

Applied to files:

  • openwisp_controller/connection/tests/test_models.py
🔇 Additional comments (3)
openwisp_controller/connection/tasks.py (1)

59-63: Good observability on duplicate-skip path.

The info log added when lock acquisition fails makes background task behavior traceable and debuggable.

openwisp_controller/connection/tests/test_models.py (2)

1037-1046: Good lock-contention regression coverage.

This correctly simulates lock acquisition failure and verifies that connection lookup and config push are both skipped.


1070-1081: Nice direct coverage for lock acquire/release semantics.

The new test validates first-acquire success, duplicate rejection, and reacquisition after release.

Copy link
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.

♻️ Duplicate comments (3)
openwisp_controller/connection/tasks.py (2)

65-68: ⚠️ Potential issue | 🟠 Major

Log the no-working-connection early exit at warning level.

This branch returns silently, which makes production diagnosis harder.

🪵 Proposed logging addition
-        except NoWorkingDeviceConnectionError:
+        except NoWorkingDeviceConnectionError as exc:
+            logger.warning(
+                'update_config("%s") skipped: no working connection (%s)',
+                device_id,
+                exc,
+            )
             return

As per coding guidelines: "New code must handle errors properly: log unusual conditions with warning level, log important background actions with info level".

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

In `@openwisp_controller/connection/tasks.py` around lines 65 - 68, The code
silently returns when DeviceConnection.get_working_connection(device) raises
NoWorkingDeviceConnectionError; update the except block to log a warning before
returning (e.g., call logger.warning or the module's logger) including
identifying context such as device (id or name) and the exception message, then
keep the early return; modify the except for NoWorkingDeviceConnectionError in
tasks.py accordingly.

23-37: ⚠️ Potential issue | 🔴 Critical

Make the cache lock ownership-aware to prevent deleting another task’s lock.

The current boolean lock + unconditional delete allows a stale task to remove a newer lock owner after TTL expiry, re-opening overlap.

🔐 Proposed fix (tokenized lock ownership)
+import uuid
@@
 def _acquire_update_config_lock(device_id):
@@
-    return cache.add(lock_key, True, timeout=_UPDATE_CONFIG_LOCK_TIMEOUT)
+    token = uuid.uuid4().hex
+    acquired = cache.add(lock_key, token, timeout=_UPDATE_CONFIG_LOCK_TIMEOUT)
+    return token if acquired else None
@@
-def _release_update_config_lock(device_id):
+def _release_update_config_lock(device_id, token):
     """Releases the per-device update_config lock."""
     lock_key = _UPDATE_CONFIG_LOCK_KEY.format(device_id=device_id)
-    cache.delete(lock_key)
+    if cache.get(lock_key) == token:
+        cache.delete(lock_key)
@@
-    if not _acquire_update_config_lock(device_id):
+    lock_token = _acquire_update_config_lock(device_id)
+    if not lock_token:
         logger.info(
             f"update_config for device {device_id} is already in progress, skipping"
         )
         return
@@
-        _release_update_config_lock(device_id)
+        _release_update_config_lock(device_id, lock_token)

Also applies to: 72-73

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

In `@openwisp_controller/connection/tasks.py` around lines 23 - 37, Current
boolean lock allows a caller to unconditionally delete a lock; change to
tokenized ownership: in _acquire_update_config_lock(device_id) generate a unique
token (uuid4), use cache.add(lock_key, token,
timeout=_UPDATE_CONFIG_LOCK_TIMEOUT) and return the token on success (or
False/None on failure), and change _release_update_config_lock(device_id, token)
to fetch the current value (cache.get(lock_key)) and only call
cache.delete(lock_key) if the stored token equals the provided token; update all
call sites that call _acquire_update_config_lock and _release_update_config_lock
(e.g., the update_config task) to accept and pass the returned token so only the
lock owner can release it.
openwisp_controller/connection/tests/test_models.py (1)

1058-1068: ⚠️ Potential issue | 🟡 Minor

Assert the mocked release call to protect the finally contract.

_release_update_config_lock is patched but never asserted; a regression in release would still pass.

✅ Proposed assertion update
         with mock.patch(
             "openwisp_controller.connection.tasks._acquire_update_config_lock",
             return_value=True,
         ), mock.patch(
             "openwisp_controller.connection.tasks._release_update_config_lock",
-        ):
+        ) as mocked_release:
             conf.config = {"general": {"timezone": "UTC"}}
             conf.full_clean()
             conf.save()
             mocked_get_working_connection.assert_called_once()
             mocked_update_config.assert_called_once()
+            mocked_release.assert_called_once()

Based on learnings: "Tests must be updated to cover non-trivial changes and ensure proper validation of modified behavior".

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

In `@openwisp_controller/connection/tests/test_models.py` around lines 1058 -
1068, The test patches _acquire_update_config_lock and
_release_update_config_lock but never asserts the release call; update the test
to capture the patched release as a mock (e.g., mock.patch(..., autospec=True)
as mocked_release) and add an assertion like mocked_release.assert_called_once()
after the save block to ensure the finally contract is honored; keep the
existing assertions for mocked_get_working_connection and mocked_update_config
and place the new mocked_release.assert_called_once() alongside them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@openwisp_controller/connection/tasks.py`:
- Around line 65-68: The code silently returns when
DeviceConnection.get_working_connection(device) raises
NoWorkingDeviceConnectionError; update the except block to log a warning before
returning (e.g., call logger.warning or the module's logger) including
identifying context such as device (id or name) and the exception message, then
keep the early return; modify the except for NoWorkingDeviceConnectionError in
tasks.py accordingly.
- Around line 23-37: Current boolean lock allows a caller to unconditionally
delete a lock; change to tokenized ownership: in
_acquire_update_config_lock(device_id) generate a unique token (uuid4), use
cache.add(lock_key, token, timeout=_UPDATE_CONFIG_LOCK_TIMEOUT) and return the
token on success (or False/None on failure), and change
_release_update_config_lock(device_id, token) to fetch the current value
(cache.get(lock_key)) and only call cache.delete(lock_key) if the stored token
equals the provided token; update all call sites that call
_acquire_update_config_lock and _release_update_config_lock (e.g., the
update_config task) to accept and pass the returned token so only the lock owner
can release it.

In `@openwisp_controller/connection/tests/test_models.py`:
- Around line 1058-1068: The test patches _acquire_update_config_lock and
_release_update_config_lock but never asserts the release call; update the test
to capture the patched release as a mock (e.g., mock.patch(..., autospec=True)
as mocked_release) and add an assertion like mocked_release.assert_called_once()
after the save block to ensure the finally contract is honored; keep the
existing assertions for mocked_get_working_connection and mocked_update_config
and place the new mocked_release.assert_called_once() alongside them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b4dc6f12-d05a-44b5-ae09-e12c73030fff

📥 Commits

Reviewing files that changed from the base of the PR and between fbd3f5c and 8ff37b8.

📒 Files selected for processing (2)
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.py
🧠 Learnings (7)
📓 Common learnings
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.
📚 Learning: 2026-02-05T20:06:01.589Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.

Applied to files:

  • openwisp_controller/connection/tasks.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tests/test_models.py
📚 Learning: 2026-03-14T19:34:33.761Z
Learnt from: CR
Repo: openwisp/openwisp-controller PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-03-14T19:34:33.761Z
Learning: Applies to **/*{.test,.spec,_test,test_}.{js,ts,tsx,jsx,py,java,go,cs,rb} : Tests must be updated to cover non-trivial changes and ensure proper validation of modified behavior

Applied to files:

  • openwisp_controller/connection/tests/test_models.py
🔇 Additional comments (1)
openwisp_controller/connection/tests/test_models.py (1)

1070-1081: Good lock behavior regression coverage.

This test clearly validates acquire → block duplicate → release → reacquire, which matches the new synchronization model.

@mn-ram

This comment was marked as spam.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

Tip

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

🧩 Analysis chain

Let me pull the current state of the code before responding.


🏁 Script executed:

#!/bin/bash
cat openwisp_controller/connection/tasks.py

echo "===SEPARATOR==="

grep -n "token\|UUID\|uuid\|lock\|acquire\|release\|wrong_token\|assert_called" openwisp_controller/connection/tests/test_models.py | head -100

echo "===SEPARATOR2==="

# Get the full test methods related to lock
python3 - <<'EOF'
import re

with open('openwisp_controller/connection/tests/test_models.py', 'r') as f:
    content = f.read()

# Find all test methods related to lock
pattern = r'(def test_(?:acquire|release)_update_config_lock[^\n]*\n(?:(?!    def |\nclass ).)*)'
matches = re.findall(pattern, content, re.DOTALL)
for m in matches:
    print(m)
    print("---")
EOF

Length of output: 8978


@prakash-kalwaniya: The UUID token ownership pattern is well implemented. Here's a final assessment:


✅ What's well done

Aspect Assessment
Token ownership cache.add() stores the UUID; _release_update_config_lock validates stored_token == token before deleting — prevents a late-arriving task from releasing a fresh lock after TTL expiry
update_config threading lock_token is returned from acquire and passed through to the finally block correctly
test_release_update_config_lock_wrong_token Confirms the no-op behaviour with a wrong token, then verifies the lock is still held — exactly the right assertion
test_acquire_update_config_lock Full acquire → fail-re-acquire → release → re-acquire cycle is covered

🔍 Two minor nits worth noting

1. mocked_release.assert_called_once() could be stronger (line 1069)

The current assertion only checks the call count, not the arguments. Since lock_token is "fake-lock-token" (the mocked return value), you could assert the exact call:

mocked_release.assert_called_once_with(str(device.pk), "fake-lock-token")

This makes the test more precise and would catch a regression where the token is not passed through correctly.

2. Missing cleanup guard in lock tests

In both test_acquire_update_config_lock and test_release_update_config_lock_wrong_token, if an intermediate assertion fails, the _release_update_config_lock cleanup at the end of the method is never reached, leaving a stale "ow_update_config_test-device-id" key in the cache. This could affect other tests sharing the same cache. A simple fix:

def test_release_update_config_lock_wrong_token(self):
    device_id = "test-device-id"
    token = _acquire_update_config_lock(device_id)
    self.addCleanup(_release_update_config_lock, device_id, token)  # always runs
    self.assertIsNotNone(token)
    _release_update_config_lock(device_id, "wrong-token")
    self.assertIsNone(_acquire_update_config_lock(device_id))

Both nits are minor. The core logic — atomic acquire, UUID-gated release, TTL safety net, and finally cleanup — is solid and correct. This is ready to merge from a logic standpoint.

@mn-ram

This comment was marked as spam.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the current state of the test methods related to locks and the release assertion
python3 - <<'EOF'
import re

with open('openwisp_controller/connection/tests/test_models.py', 'r') as f:
    content = f.read()

# Find all test methods related to lock
pattern = r'(    def test_(?:acquire|release|device_update_config)[^\n]*\n(?:(?!    def |\nclass ).)*)'
matches = re.findall(pattern, content, re.DOTALL)
for m in matches:
    print(m)
    print("---")
EOF

Length of output: 3054


@prakash-kalwaniya: Both nits are confirmed fixed in the current code:

  1. Exact-args assertionmocked_release.assert_called_once_with(str(conf.device.pk), "fake-lock-token") is in place in test_device_update_config_not_in_progress. ✅
  2. addCleanup guards — both test_acquire_update_config_lock (including the token2 re-acquisition path) and test_release_update_config_lock_wrong_token use self.addCleanup(...) to ensure the cache key is always cleaned up. ✅

The implementation is solid end-to-end — atomic acquire, UUID-gated release, TTL safety net, finally cleanup, and now properly guarded tests. This is ready to merge. 🎉

Copy link
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.

♻️ Duplicate comments (1)
openwisp_controller/connection/tasks.py (1)

77-80: ⚠️ Potential issue | 🟡 Minor

Add warning log before returning on missing working connection.

The NoWorkingDeviceConnectionError path silently returns, making failed background updates hard to trace. Per coding guidelines, unusual conditions should be logged at warning level.

Suggested logging tweak
         try:
             device_conn = DeviceConnection.get_working_connection(device)
         except NoWorkingDeviceConnectionError:
+            logger.warning(
+                'update_config("%s") skipped: no working connection available',
+                device_id,
+            )
             return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/connection/tasks.py` around lines 77 - 80, The except
branch for NoWorkingDeviceConnectionError currently returns silently in
DeviceConnection.get_working_connection usage; add a warning log before
returning to surface missing working connections (e.g., use the module logger to
call logger.warning and include the device id/name and exception context).
Update the except block that catches NoWorkingDeviceConnectionError in the task
handling code in openwisp_controller/connection/tasks.py so it logs a
descriptive warning mentioning DeviceConnection.get_working_connection and the
device variable, then returns as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@openwisp_controller/connection/tasks.py`:
- Around line 77-80: The except branch for NoWorkingDeviceConnectionError
currently returns silently in DeviceConnection.get_working_connection usage; add
a warning log before returning to surface missing working connections (e.g., use
the module logger to call logger.warning and include the device id/name and
exception context). Update the except block that catches
NoWorkingDeviceConnectionError in the task handling code in
openwisp_controller/connection/tasks.py so it logs a descriptive warning
mentioning DeviceConnection.get_working_connection and the device variable, then
returns as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 35ad059b-b35a-4857-9239-b81329929e63

📥 Commits

Reviewing files that changed from the base of the PR and between f3c99c4 and fc10369.

📒 Files selected for processing (2)
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.py
🧠 Learnings (7)
📓 Common learnings
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.
📚 Learning: 2026-02-05T20:06:01.589Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.

Applied to files:

  • openwisp_controller/connection/tasks.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tasks.py
  • openwisp_controller/connection/tests/test_models.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/connection/tests/test_models.py
📚 Learning: 2026-03-14T19:34:33.761Z
Learnt from: CR
Repo: openwisp/openwisp-controller PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-03-14T19:34:33.761Z
Learning: Applies to **/*{.test,.spec,_test,test_}.{js,ts,tsx,jsx,py,java,go,cs,rb} : Tests must be updated to cover non-trivial changes and ensure proper validation of modified behavior

Applied to files:

  • openwisp_controller/connection/tests/test_models.py
🔇 Additional comments (7)
openwisp_controller/connection/tasks.py (3)

18-21: LGTM on lock constants.

The lock key pattern and 5-minute TTL as a safety net is appropriate. The comment clearly explains the timeout's purpose.


24-36: LGTM on lock acquisition.

The use of cache.add() provides atomic lock acquisition, and the UUID token enables ownership-aware release. The docstring clearly documents the contract.


39-47: LGTM on lock release with ownership check.

The token comparison ensures only the lock owner can release. While there's a theoretical TOCTOU window between cache.get() and cache.delete(), the window is very small and the trade-off was accepted per PR discussion.

openwisp_controller/connection/tests/test_models.py (4)

24-28: LGTM on updated imports.

The imports correctly reference the new lock functions for testing.


1032-1045: LGTM on in-progress test.

The test correctly validates that when lock acquisition fails (returns None), the update is skipped and neither connection nor config update is attempted.


1047-1069: LGTM on not-in-progress test.

The test properly validates the happy path: lock acquired, connection obtained, update performed, and lock released. The mocked_release.assert_called_once() assertion at line 1069 ensures the finally block contract is tested.


1071-1096: LGTM on lock unit tests.

test_acquire_update_config_lock validates the full lifecycle: acquisition, duplicate prevention, release, and re-acquisition. test_release_update_config_lock_wrong_token confirms ownership-aware release prevents unauthorized lock deletion. Both tests properly clean up state.

@mn-ram

This comment was marked as spam.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat openwisp_controller/connection/tasks.py

Length of output: 4590


@prakash-kalwaniya: The warning log in the NoWorkingDeviceConnectionError except block looks good. Here's a quick confirmation:

except NoWorkingDeviceConnectionError as e:
    logger.warning(
        f"update_config for device {device_id}: "
        f"DeviceConnection.get_working_connection failed: {e}"
    )
    return
Aspect Assessment
Log level logger.warning is appropriate — it's a recoverable condition, not an unhandled crash ✅
Device ID Included for traceability in log aggregators ✅
Operation context DeviceConnection.get_working_connection failed clearly identifies where the failure occurred ✅
Exception details {e} surfaces the underlying reason (e.g. no credentials, no reachable connection) ✅

This is a clean, informative addition that will make these failures much easier to diagnose in production. The PR looks solid end-to-end — ready to merge. 🎉

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 15, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

@prakash-kalwaniya: All findings have been confirmed addressed across the review cycle:

  • ✅ Atomic per-device cache lock via cache.add() replacing the flawed inspect().active() snapshot approach
  • bind=True and unused self parameter removed from update_config
  • ✅ UUID token ownership ensures only the acquiring task can release its lock
  • ✅ Exact-args assertion mocked_release.assert_called_once_with(str(conf.device.pk), "fake-lock-token") in place
  • addCleanup guards prevent stale cache keys on assertion failures in lock tests
  • ✅ Warning log in NoWorkingDeviceConnectionError except block for production traceability

Approving the PR now. 🎉

✅ Actions performed

Comments resolved and changes approved.

@openwisp-companion
Copy link

Test Failure in test_certificate_renew_invalidates_checksum_cache

Hello @prakash-kalwaniya,
(Analysis for commit 7e95edd)

There is one test failure in the provided logs:

  1. Test Failure: The test test_certificate_renew_invalidates_checksum_cache in openwisp_controller.config.tests.test_config.TestTransactionConfig failed with an AssertionError: 4 != 3. This indicates that the mocked_delete object was expected to be called 3 times, but it was called 4 times.

    • Explanation: The assertion self.assertEqual(mocked_delete.call_count, 3) failed because mocked_delete.call_count was 4. This suggests that a function or method that was supposed to be called three times was actually called four times during the execution of the test. The exact reason for the extra call count cannot be determined without examining the test logic and the mocked delete function.

    • Remediation: To fix this, you should investigate the test_certificate_renew_invalidates_checksum_cache test case in openwisp_controller/config/tests/test_config.py. Specifically, examine the part of the test where mocked_delete is used and identify why it's being called an extra time. Adjust the test logic or the code being tested to ensure mocked_delete is called exactly three times as expected by the assertion.

@mn-ram mn-ram force-pushed the fix/update-config-self-detection branch from 7e95edd to bc41f8d Compare March 15, 2026 17:05
@openwisp-companion
Copy link

Test Failure in test_certificate_renew_invalidates_checksum_cache

Hello @prakash-kalwaniya,
(Analysis for commit bc41f8d)

There is a test failure in your CI build.

  • Explanation: The test test_certificate_renew_invalidates_checksum_cache in openwisp_controller.config.tests.test_config failed because it expected the mocked_delete call count to be 3, but it was 4. This indicates an unexpected number of calls to the delete method, likely due to an issue in how the cache is being invalidated or how the test is set up.

  • Remediation: Review the logic within the test_certificate_renew_invalidates_checksum_cache test case. Specifically, examine where mocked_delete is being called and why it's being called an extra time. Ensure that the test accurately reflects the expected behavior of the cache invalidation mechanism, especially when dealing with certificate renewals. You might need to adjust the mock or the test's setup to match the intended call count.

@openwisp-companion
Copy link

Commit Message Format Failure

Hello @prakash-kalwaniya,
(Analysis for commit 691541c)

Failure: Commit Message Validation Failed

Explanation:
The CI pipeline failed because the commit message does not adhere to the OpenWISP commit message conventions. Specifically, the error message "Issue referenced in title but not in body. If you reference an issue in the title, you must also reference it in the body using Fixes/Closes/Related to #" indicates that while the issue number #1277 is present in the commit title, it is not explicitly linked in the commit body with a closing keyword.

Remediation:
Please update your commit message to include a closing keyword that links the issue number to the commit body.

Here is an example of the correct format:

[fix] Prevent update_config task from detecting itself as duplicate #1277

The _is_update_in_progress() helper was detecting the currently executing
task as a duplicate because Celery's inspect().active() snapshot included
the running task itself.

Replaced snapshot-based detection with an atomic per-device cache lock
using Django's cache framework.

Fixes #1277

…penwisp#1277

The _is_update_in_progress() helper was detecting the currently executing
task as a duplicate because Celery's inspect().active() snapshot included
the running task itself.

Replaced snapshot-based detection with an atomic per-device cache lock
using Django's cache framework.

Fixes openwisp#1277
@mn-ram mn-ram force-pushed the fix/update-config-self-detection branch from 691541c to 0952f5b Compare March 15, 2026 18:34
@mn-ram

This comment was marked as spam.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat openwisp_controller/connection/tasks.py

echo "===SEPARATOR==="

python3 - <<'EOF'
import re

with open('openwisp_controller/connection/tests/test_models.py', 'r') as f:
    content = f.read()

# Find all test methods related to lock and update_config
pattern = r'(    def test_(?:acquire|release|device_update_config)[^\n]*\n(?:(?!    def |\nclass ).)*)'
matches = re.findall(pattern, content, re.DOTALL)
for m in matches:
    print(m)
    print("---")
EOF

Length of output: 7661


@prakash-kalwaniya: Verified all items against the current code — everything is confirmed in place:

Item Status
Atomic per-device cache lock via cache.add()
UUID token ownership — only acquiring task can release
5-minute TTL safety timeout (_UPDATE_CONFIG_LOCK_TIMEOUT = 300)
finally block releases lock on all exit paths
bind=True / self removed from update_config
logger.warning in NoWorkingDeviceConnectionError block
test_acquire_update_config_lock with full cycle + addCleanup
test_release_update_config_lock_wrong_token with addCleanup
mocked_release.assert_called_once_with(str(conf.device.pk), "fake-lock-token")

The implementation is solid end-to-end. Approving now. 🎉

[approve]

@mn-ram

This comment was marked as spam.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat openwisp_controller/connection/tasks.py

echo "===SEPARATOR==="

python3 - <<'EOF'
import re

with open('openwisp_controller/connection/tests/test_models.py', 'r') as f:
    content = f.read()

# Find all test methods related to lock and update_config
pattern = r'(    def test_(?:acquire|release|device_update_config)[^\n]*\n(?:(?!    def |\nclass ).)*)'
matches = re.findall(pattern, content, re.DOTALL)
for m in matches:
    print(m)
    print("---")
EOF

Length of output: 7661


@prakash-kalwaniya: Verified all items against the current code — everything is confirmed in place:

Item Status
Atomic per-device cache lock via cache.add()
UUID token ownership — only acquiring task can release
5-minute TTL safety timeout (_UPDATE_CONFIG_LOCK_TIMEOUT = 300)
finally block releases lock on all exit paths
bind=True / self removed from update_config
logger.warning in NoWorkingDeviceConnectionError block
test_acquire_update_config_lock with full cycle + addCleanup
test_release_update_config_lock_wrong_token with addCleanup
mocked_release.assert_called_once_with(str(conf.device.pk), "fake-lock-token")

The implementation is solid end-to-end. Approving now. 🎉

[approve]

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

@prakash-kalwaniya: All previously raised findings remain confirmed addressed. Approving the PR now. 🎉

✅ Actions performed

Comments resolved and changes approved.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

@prakash-kalwaniya: Noted — bind=True and self are cleaned up, and the cache-unavailability trade-off is understood for your deployment. Triggering a fresh full review now.

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python3 - <<'EOF'
import re

with open('openwisp_controller/connection/tests/test_models.py', 'r') as f:
    content = f.read()

# Find all test methods related to lock and update_config
pattern = r'(    def test_(?:acquire|release|device_update_config)[^\n]*\n(?:(?!    def |\nclass ).)*)'
matches = re.findall(pattern, content, re.DOTALL)
for m in matches:
    print(m)
    print("---")
EOF

Length of output: 3054


@prakash-kalwaniya: Both nits are confirmed fixed in the current code:

  1. Exact-args assertionmocked_release.assert_called_once_with(str(conf.device.pk), "fake-lock-token") is in place in test_device_update_config_not_in_progress. ✅
  2. addCleanup guards — both test_acquire_update_config_lock (including the token2 re-acquisition path) and test_release_update_config_lock_wrong_token use self.addCleanup(...) to ensure the cache key is always cleaned up. ✅

The implementation is solid end-to-end — atomic acquire, UUID-gated release, TTL safety net, finally cleanup, and now properly guarded tests. This is ready to merge. 🎉

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat openwisp_controller/connection/tasks.py

echo "===SEPARATOR==="

python3 - <<'EOF'
import re

with open('openwisp_controller/connection/tests/test_models.py', 'r') as f:
    content = f.read()

# Find all test methods related to lock
pattern = r'(    def test_(?:acquire|release)_update_config_lock[^\n]*\n(?:(?!    def |\nclass ).)*)'
matches = re.findall(pattern, content, re.DOTALL)
for m in matches:
    print(m)
    print("---")
EOF

echo "===SEPARATOR2==="

grep -n "token\|UUID\|uuid\|assert_called\|mocked_release" openwisp_controller/connection/tests/test_models.py | head -60

Length of output: 8578


@prakash-kalwaniya: The UUID token ownership pattern is well implemented. Here's a final assessment:


✅ What's well done

Aspect Assessment
Token ownership cache.add() stores the UUID; _release_update_config_lock validates stored_token == token before deleting — prevents a late-arriving task from releasing a fresh lock after TTL expiry
update_config threading lock_token is returned from acquire and passed through to the finally block correctly
test_release_update_config_lock_wrong_token Confirms the no-op behaviour with a wrong token, then verifies the lock is still held — exactly the right assertion
test_acquire_update_config_lock Full acquire → fail-re-acquire → release → re-acquire cycle is covered

🔍 Two minor nits worth noting

1. mocked_release.assert_called_once() could be stronger (line 1069)

The current assertion only checks the call count, not the arguments. Since lock_token is "fake-lock-token" (the mocked return value), you could assert the exact call:

mocked_release.assert_called_once_with(str(device.pk), "fake-lock-token")

This makes the test more precise and would catch a regression where the token is not passed through correctly.

2. Missing cleanup guard in lock tests

In both test_acquire_update_config_lock and test_release_update_config_lock_wrong_token, if an intermediate assertion fails, the _release_update_config_lock cleanup at the end of the method is never reached, leaving a stale "ow_update_config_test-device-id" key in the cache. This could affect other tests sharing the same cache. A simple fix:

def test_release_update_config_lock_wrong_token(self):
    device_id = "test-device-id"
    token = _acquire_update_config_lock(device_id)
    self.addCleanup(_release_update_config_lock, device_id, token)  # always runs
    self.assertIsNotNone(token)
    _release_update_config_lock(device_id, "wrong-token")
    self.assertIsNone(_acquire_update_config_lock(device_id))

Both nits are minor. The core logic — atomic acquire, UUID-gated release, TTL safety net, and finally cleanup — is solid and correct. This is ready to merge from a logic standpoint.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat openwisp_controller/connection/tasks.py

echo "---SEPARATOR---"

# Get relevant test sections
cat openwisp_controller/connection/tests/test_models.py | grep -n "_is_update_in_progress\|cache\|update_config\|inspect\|active\|TTL\|lock" | head -80

Length of output: 6863


@prakash-kalwaniya: The new cache-lock approach looks solid. Here's a thorough assessment:


✅ What's well done

Aspect Assessment
Atomicity cache.add() is atomic — only one task wins the lock per device
Race condition eliminated Two overlapping tasks can no longer both skip each other
Safety TTL 5-minute timeout (_UPDATE_CONFIG_LOCK_TIMEOUT = 300) prevents stale lock if the process is killed hard (e.g. OOM, SIGKILL)
finally block Guarantees release on exceptions, SoftTimeLimitExceeded, etc.
UUID token ownership _release_update_config_lock validates stored_token == token before deleting — prevents a late-arriving task from releasing a fresh lock after TTL expiry
Lock scope Acquired after time.sleep(2) and pre-checks — no unnecessary lock holding
Test coverage test_acquire_update_config_lock, test_device_update_config_in_progress, test_device_update_config_not_in_progress, test_release_update_config_lock_wrong_token cover the key paths

🔍 One minor cleanup

@shared_task(bind=True) and the self parameter on update_config are now unused leftovers from the previous approach — self.request.id is no longer needed:

# Before (current state — bind=True is now unused)
`@shared_task`(bind=True)
def update_config(self, device_id):

# After (cleaner)
`@shared_task`
def update_config(device_id):

This is purely cosmetic but avoids confusing future readers into thinking self is used somewhere.


🤔 One edge case worth verifying

If the cache backend is unavailable (e.g. Redis is down), cache.add() may raise an exception or silently return False, causing every update_config call to be skipped. You may want to confirm what behaviour is acceptable for your deployment — but this is likely already a known trade-off with cache-based locking in general.


Overall, this is a meaningful improvement over the inspect().active() snapshot approach. The single actionable item is removing bind=True / self. Would you like me to trigger a fresh full review?

@nemesifier
Copy link
Member

We're already working on it: #1292
Last warning to stop spamming us.

@nemesifier nemesifier closed this Mar 17, 2026
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.

3 participants