-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Closes 20817 fix datasource sync broken when cron is set #20837
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Closes 20817 fix datasource sync broken when cron is set #20837
Conversation
This reverts commit b4160ad.
|
@ifoughal, CI appears to be failing with a test failure. |
|
Initially I thought there was an issue with the view rendering class, but after some further TS, and checking the actual value of status on the DB entry, it seems the status field got overwritten somehow. To circumvent this, I run the instance.save() method in the save override of dataSourcesForm. but this generates 2 commits and breaks the test. I'll continue looking into a fix. |
|
The issue was coming from : The enqueue method was overriding the state, I have removed |
arthanson
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ifoughal Is QUEUED status used anymore? I don't see it ever getting set anywhere.
The QUEUED state was that it was submitted to be run (so pending being run). I think this is should still be a valid status, i.e. get set when it is pending being run (enqueued) and cleared once it is run.
The description for the PR says that two new states are added, but only one is added here, is the description old?
I will be reverting the removed of queued as it was indeed needed for when the rq workers as busy. It should then reflect the queued state. The two new states are |
…t' of https://github.com/ifoughal/netbox into closes-20817-Fix-datasource-sync-broken-when-cron-is-set
…-sync-broken-when-cron-is-set
| (SYNCING, _('Syncing'), 'cyan'), | ||
| (COMPLETED, _('Completed'), 'green'), | ||
| (FAILED, _('Failed'), 'red'), | ||
| (READY, _('Ready'), 'green'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You say SCHEDULED status is added but there is no scheduled in the status or code anywhere? Still confused as you state "The two new states are READY & SCHEDULED."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up removing it, so there is only one state that has been added in the end, which is the READY state.
The reasoning behind was that scheduled would interfere with the post sync action trigger status (completed/failed/syncing). Therefore, I decided to keep the ready state only, which has the same effect as new, the only difference is that new is only applied when the object gets created, whereas ready is applied when there there is no effective sync interval.
| DataSourceStatusChoices.QUEUED, | ||
| DataSourceStatusChoices.SYNCING | ||
| ) | ||
| return self.enabled and self.status != DataSourceStatusChoices.SYNCING |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: If we are adding the READY state and other changes here do we still need to skip QUEUED check here? Just wondering if your other changes will allow it to be kept as I think if it is actually queued and hasn't finished we probably don't want to allow sync again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's why I had initially added the scheduled state initially, so that when it's queued, I would disable the syncing button, but allow it only when the state is scheduled. The issue was that scheduled was completely redundant with Queued. As they could be set only during the save action of the data-source object. They would also get overwritten when the job finishes. so queued, just as before reflects when the job is queued when the user hits sync, but also when the sync_interval is set.
- The proper way to fix it would be to add a new field, for example
last-statusand use that for the latest job status. - keep the current status field, and use it only for the workers Q state (new, ready, queued) and maybe re-add the
scheduledstate as well.
what do you think?
…-sync-broken-when-cron-is-set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR attempts to fix an issue where DataSource synchronization was broken when a sync interval (cron schedule) was set. The proposed solution introduces a new READY status and modifies the ready_for_sync property to allow manual syncing even when a recurring sync job is queued.
Key Changes
- Removes QUEUED status from the
ready_for_synccheck to allow manual sync triggers - Adds READY status to DataSourceStatusChoices
- Implements form-level status management in DataSourceForm's clean() and save() methods
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
| netbox/core/models/data.py | Modified ready_for_sync property to exclude only SYNCING status instead of both QUEUED and SYNCING |
| netbox/core/forms/model_forms.py | Added status management logic in form's clean() and save() methods to set status based on instance state and sync_interval |
| netbox/core/choices.py | Added new READY status choice to DataSourceStatusChoices |
Critical Issues Identified:
The implementation has several fundamental design flaws that need to be addressed:
-
Missing SCHEDULED Status: The PR description mentions introducing both "Ready" and "Scheduled" states, but only READY is implemented. The logic to set status to SCHEDULED when sync_interval is configured is missing.
-
Status Field Design Violation: The DataSource.status field is marked as
editable=False, indicating it should be managed programmatically by the sync system, not by forms. The current implementation has the form directly manipulating this field, which conflicts with the intended API design. -
Status Override Issue: The form unconditionally overwrites the DataSource status on every save, which will destroy sync operation results (COMPLETED/FAILED) set by the sync process. This breaks the ability to track sync history.
-
Race Condition Risk: Removing QUEUED from
ready_for_synccould allow multiple simultaneous sync operations since QUEUED is set bySyncDataSourceJob.enqueue()before the job runs. -
Signal Interference: The form's status changes conflict with the
enqueue_sync_jobsignal handler that sets status to QUEUED when a sync job is enqueued. -
Implementation Logic Errors: The form checks
self.instance.pkwhich will always be truthy for updates (should check if pk is None), and uses rawself.datainstead ofself.cleaned_data.
The core issue appears to be that the fix tries to solve a coordination problem between recurring scheduled syncs and manual syncs by manipulating status at the form level, when this should be handled by the job scheduling system and sync operations themselves. A better approach would be to keep status management entirely within the sync process and job system, and use the existing QUEUED/SYNCING/COMPLETED/FAILED states appropriately.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| else: | ||
| if not self.data.get('sync_interval'): | ||
| self.cleaned_data['status'] = DataSourceStatusChoices.READY | ||
|
|
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for setting status based on sync_interval is incomplete. According to the PR description, when a user sets a sync interval, the status should be set to "Scheduled". However, this code only handles the case when sync_interval is removed (setting to READY), but doesn't set the status to SCHEDULED when sync_interval is present. There's also no SCHEDULED state defined, so you should add an else branch to handle when sync_interval is set (e.g., set status to QUEUED or implement SCHEDULED).
| else: | |
| self.cleaned_data['status'] = DataSourceStatusChoices.QUEUED |
netbox/core/forms/model_forms.py
Outdated
| if not self.instance.pk: | ||
| self.cleaned_data['status'] = DataSourceStatusChoices.NEW | ||
| else: | ||
| if not self.data.get('sync_interval'): |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using self.data.get('sync_interval') directly accesses raw form data instead of the cleaned and validated data. This could lead to inconsistencies since self.data contains the raw POST/GET data (strings), while self.cleaned_data['sync_interval'] contains the properly validated and converted value. Use self.cleaned_data.get('sync_interval') instead for consistency and reliability.
| if not self.data.get('sync_interval'): | |
| if not self.cleaned_data.get('sync_interval'): |
netbox/core/forms/model_forms.py
Outdated
| def clean(self): | ||
| super().clean() | ||
| if not self.instance.pk: | ||
| self.cleaned_data['status'] = DataSourceStatusChoices.NEW | ||
| else: | ||
| if not self.data.get('sync_interval'): | ||
| self.cleaned_data['status'] = DataSourceStatusChoices.READY | ||
|
|
||
| def save(self, *args, **kwargs): | ||
| parameters = {} | ||
| for name in self.fields: | ||
| if name.startswith('backend_'): | ||
| parameters[name[8:]] = self.cleaned_data[name] | ||
| self.instance.parameters = parameters | ||
|
|
||
| # update status | ||
| self.instance.status = self.cleaned_data.get('status', self.instance.status) | ||
|
|
||
| return super().save(*args, **kwargs) |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The status modification in clean() will interfere with the enqueue_sync_job signal handler in signals.py (line 259-275). When saving a DataSource with a sync_interval, the sequence will be: 1) Form sets status to READY (or NEW), 2) Form saves the instance, 3) The post_save signal fires and enqueues a job, 4) The SyncDataSourceJob.enqueue() sets status to QUEUED. This creates an inconsistent state where the form's status is immediately overwritten. The form should not manage status at all, or should coordinate with the signal/job system.
netbox/core/forms/model_forms.py
Outdated
| def clean(self): | ||
| super().clean() | ||
| if not self.instance.pk: | ||
| self.cleaned_data['status'] = DataSourceStatusChoices.NEW | ||
| else: | ||
| if not self.data.get('sync_interval'): | ||
| self.cleaned_data['status'] = DataSourceStatusChoices.READY | ||
|
|
||
| def save(self, *args, **kwargs): | ||
| parameters = {} | ||
| for name in self.fields: | ||
| if name.startswith('backend_'): | ||
| parameters[name[8:]] = self.cleaned_data[name] | ||
| self.instance.parameters = parameters | ||
|
|
||
| # update status | ||
| self.instance.status = self.cleaned_data.get('status', self.instance.status) | ||
|
|
||
| return super().save(*args, **kwargs) |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DataSource.status field is marked as editable=False in the model (data.py line 56), indicating it's intended to be managed programmatically, not by user input. Having the form manipulate this field directly violates this design principle. The status should be managed exclusively by the sync process (DataSource.sync(), SyncDataSourceJob) and the signal handlers, not by the form. This creates a conflict between the intended API design and the form implementation.
| (SYNCING, _('Syncing'), 'cyan'), | ||
| (COMPLETED, _('Completed'), 'green'), | ||
| (FAILED, _('Failed'), 'red'), | ||
| (READY, _('Ready'), 'green'), |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The READY status uses the same color ('green') as COMPLETED. This could be confusing for users since these statuses represent different states: COMPLETED means a sync operation finished successfully, while READY means the DataSource is ready to be synced but hasn't completed a sync yet. Consider using a different color like 'blue' or 'gray' for READY to better differentiate it visually from COMPLETED.
| (READY, _('Ready'), 'green'), | |
| (READY, _('Ready'), 'gray'), |
netbox/core/forms/model_forms.py
Outdated
| self.instance.parameters = parameters | ||
|
|
||
| # update status | ||
| self.instance.status = self.cleaned_data.get('status', self.instance.status) |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic unconditionally overwrites the DataSource status from the form's cleaned_data. This will break the intended behavior where sync operations set the status to SYNCING, COMPLETED, or FAILED (as seen in DataSource.sync() method and SyncDataSourceJob). After a sync completes and sets status to COMPLETED or FAILED, if a user edits the DataSource, this form will reset the status to READY or NEW, losing the sync result information. The form should only update the status when explicitly changing sync_interval settings, not on every save.
| DataSourceStatusChoices.QUEUED, | ||
| DataSourceStatusChoices.SYNCING | ||
| ) | ||
| return self.enabled and self.status != DataSourceStatusChoices.SYNCING |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While removing QUEUED from the ready_for_sync check allows users to manually trigger syncs even when a recurring job is queued, this change could allow multiple simultaneous sync operations for the same DataSource. The SyncDataSourceJob.enqueue() method sets status to QUEUED before the job runs (in jobs.py line 34). If a user manually triggers a sync while a job is queued but not yet running, both could execute. Consider whether QUEUED should be excluded or if additional logic is needed to prevent race conditions.
| return self.enabled and self.status != DataSourceStatusChoices.SYNCING | |
| return self.enabled and self.status not in ( | |
| DataSourceStatusChoices.SYNCING, | |
| DataSourceStatusChoices.QUEUED, | |
| ) |
| DataSourceStatusChoices.QUEUED, | ||
| DataSourceStatusChoices.SYNCING | ||
| ) | ||
| return self.enabled and self.status != DataSourceStatusChoices.SYNCING |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The modified ready_for_sync logic lacks test coverage. Given that this property is critical for determining when a DataSource can be synced and is a key part of the bug fix, tests should be added to verify the behavior with different status values (NEW, QUEUED, READY, COMPLETED, FAILED) and the enabled flag. Consider adding tests to netbox/core/tests/test_models.py to cover this property.
netbox/core/forms/model_forms.py
Outdated
| def clean(self): | ||
| super().clean() | ||
| if not self.instance.pk: | ||
| self.cleaned_data['status'] = DataSourceStatusChoices.NEW | ||
| else: | ||
| if not self.data.get('sync_interval'): | ||
| self.cleaned_data['status'] = DataSourceStatusChoices.READY | ||
|
|
||
| def save(self, *args, **kwargs): | ||
| parameters = {} | ||
| for name in self.fields: | ||
| if name.startswith('backend_'): | ||
| parameters[name[8:]] = self.cleaned_data[name] | ||
| self.instance.parameters = parameters | ||
|
|
||
| # update status | ||
| self.instance.status = self.cleaned_data.get('status', self.instance.status) | ||
|
|
||
| return super().save(*args, **kwargs) |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new status transition logic in the form's clean() and save() methods lacks test coverage. Tests should be added to verify that: 1) New DataSources get status=NEW, 2) Updating an existing DataSource with sync_interval removed sets status=READY, 3) Updating an existing DataSource with sync_interval set behaves correctly, and 4) Status from completed sync operations (COMPLETED/FAILED) is preserved appropriately. Consider adding tests to verify this form behavior.
| SYNCING = 'syncing' | ||
| COMPLETED = 'completed' | ||
| FAILED = 'failed' | ||
| READY = 'ready' |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR description mentions introducing two new states: "Ready" and "Scheduled". However, only the READY state is added here. The SCHEDULED state is missing from the DataSourceStatusChoices. This means the logic described in the PR description for setting status to "Scheduled" when a user sets a sync interval cannot be implemented.
…et' of https://github.com/ifoughal/netbox into closes-20817-Fix-datasource-sync-broken-when-cron-is-set
Fixes: #20817
Datasources sync was broken when the user sets a sync interval.
The fix introduces 2 new states:
Ready state is set when the data-source object is not new. ie: either updated or changed state recently.
Scheduled is set when the user sets a sync interval.
This PR includes the following logic enhancements:
by removing
DataSourceStatusChoices.QUEUEDfrom theready_for_syncmethod, we let the user sync the data-source anytime even when a daily interval is set, as long as the data-source is enabled.Include the 2 new states (Scheduled, Ready) in the clean & save properties of data-source form model, to set the status correctly depending on the user inputs/