Bound the Dell warranty sync per run

The sync route asked Dell about every pending service tag in one request.
bulk_lookup sends 100 tags per HTTP call at a 15 second timeout, so a
freshly-onboarded site was dozens of sequential calls inside a single web
request. A gunicorn worker timeout or a proxy would plausibly kill it first,
leaving the user with a failure after minutes and no way to tell what landed.
F3 released the pooled connection before the vendor call, which stopped one
sync from taking the app down, but did not make the request itself survivable.

Extract the sync into a service taking a per-run tag limit. The route now caps
at 200 tags - two Dell calls - and returns how many are still pending so the
caller can run it again. A flask warranty sync-dell command does the same work
from the command line for a large first-run backfill.

The chunk is sampled at random rather than taken off a stable order. A serial
Dell does not recognize never gains a warranty and so stays pending forever;
under a stable order those dead tags would occupy every chunk and the sync
would never reach the rest of the fleet.

Nothing runs this on a schedule. The operation is used a few times a year, so
a cron would idle-poll the warranty tables tens of thousands of times a year
for nothing and log a configuration failure on every run at sites without Dell
credentials.

The two existing sync tests patched get_provider on the route module; the
provider is now resolved in the service, so they patch it there.
This commit is contained in:
benjamin.baker
2026-09-19 14:21:41 -04:00
parent 530f6a60f8
commit 32f4ccdd48
6 changed files with 353 additions and 103 deletions

View File

@@ -14,6 +14,7 @@ continues to pass.
"""
from plugins.warranty.api import routes as warranty_routes
from plugins.warranty.services import dellsync as warranty_dellsync
from plugins.warranty.models import Warranty, WarrantyAsset
@@ -104,7 +105,7 @@ def test_sync_dell_releases_the_connection_before_the_bulk_call(
'startdate': '2023-05-02',
'enddate': '2026-05-02',
})
monkeypatch.setattr(warranty_routes, 'get_provider', lambda name: probe)
monkeypatch.setattr(warranty_dellsync, 'get_provider', lambda name: probe)
response = client.post('/api/warranty/sync/dell', headers=auth_headers)
@@ -147,7 +148,7 @@ def test_sync_dell_updates_instead_of_duplicating_on_a_recheck(
'startdate': '2024-02-02',
'enddate': '2028-02-02',
})
monkeypatch.setattr(warranty_routes, 'get_provider', lambda name: probe)
monkeypatch.setattr(warranty_dellsync, 'get_provider', lambda name: probe)
response = client.post('/api/warranty/sync/dell?all=true', headers=auth_headers)

View File

@@ -0,0 +1,116 @@
"""The Dell sync must be bounded per run and must make progress across runs.
`bulk_lookup` asks Dell about 100 tags per HTTP call at HTTP_TIMEOUT seconds
each, so an unbounded fleet-sized sync is dozens of sequential calls inside one
web request - long enough for a gunicorn worker timeout or a proxy to kill it
first. The fix is a per-run cap, with the operator re-running until nothing is
left pending.
What would silently regress: the cap being dropped (the route goes back to
unbounded), or the chunk being taken off a stable ordering. The second is
subtle - a serial Dell does not recognize never gains a warranty, so it stays
pending forever, and under a stable order those dead tags would fill every
chunk and the sync would never reach the rest of the fleet.
"""
from plugins.warranty.services import dellsync as warranty_dellsync
from plugins.warranty.services.dellsync import run_dell_sync
from plugins.warranty.models import Warranty, WarrantyAsset
def _assets(db, count, prefix='PC'):
from shopdb.core.models import AssetType, AssetStatus
from shopdb.core.models.asset import Asset
assettype = AssetType(assettype='computer')
status = AssetStatus(status='In Use')
db.session.add_all([assettype, status])
db.session.flush()
for index in range(count):
db.session.add(Asset(
assetnumber=f'{prefix}-{index:04d}',
name=f'Bench PC {index}',
assettypeid=assettype.assettypeid,
statusid=status.statusid,
serialnumber=f'TAG{index:04d}',
))
db.session.commit()
class _CountingProvider:
"""Records every tag it was asked about, and answers for none of them."""
def __init__(self, known=None):
self.seen_batches = []
self._known = known or {}
def bulk_lookup(self, servicetags):
self.seen_batches.append(list(servicetags))
return {tag: self._known[tag] for tag in servicetags if tag in self._known}
def test_sync_is_capped_at_the_limit_and_reports_the_remainder(app, db, monkeypatch):
with app.app_context():
_assets(db, 25)
provider = _CountingProvider()
monkeypatch.setattr(warranty_dellsync, 'get_provider', lambda name: provider)
summary = run_dell_sync(limit=10)
assert len(provider.seen_batches) == 1
assert len(provider.seen_batches[0]) == 10, 'more tags went to Dell than the limit'
assert summary['tags'] == 10
assert summary['remaining'] == 15
def test_no_limit_checks_everything(app, db, monkeypatch):
with app.app_context():
_assets(db, 12)
provider = _CountingProvider()
monkeypatch.setattr(warranty_dellsync, 'get_provider', lambda name: provider)
summary = run_dell_sync(limit=None)
assert summary['tags'] == 12
assert summary['remaining'] == 0
def test_repeated_runs_reach_tags_dell_does_not_recognize(app, db, monkeypatch):
"""Unmatched tags stay pending, so chunks must not keep re-picking the same ones.
Every asset here is unknown to Dell, which is the worst case: nothing ever
becomes covered, so the pending set never shrinks. A stable ordering would
send the same first chunk every run and the later tags would never be
looked up at all.
"""
with app.app_context():
_assets(db, 30)
provider = _CountingProvider()
monkeypatch.setattr(warranty_dellsync, 'get_provider', lambda name: provider)
for _ in range(12):
run_dell_sync(limit=5)
reached = {tag for batch in provider.seen_batches for tag in batch}
assert len(reached) > 5, (
'repeated runs never got past the first chunk - the sync cannot drain'
)
def test_covered_assets_are_skipped_so_runs_resume(app, db, monkeypatch):
with app.app_context():
_assets(db, 5)
known = {
'TAG0000': {'servicelevel': 'Basic Onsite',
'startdate': '2023-05-02', 'enddate': '2026-05-02'},
}
provider = _CountingProvider(known=known)
monkeypatch.setattr(warranty_dellsync, 'get_provider', lambda name: provider)
first = run_dell_sync(limit=None)
assert first['created'] == 1
second = run_dell_sync(limit=None)
assert second['tags'] == 4, 'a covered asset was looked up again'
assert 'TAG0000' not in provider.seen_batches[-1]