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

@@ -23,7 +23,10 @@ from shopdb.api import (
)
from ..models import Warranty, WarrantyAsset
from ..services import get_provider, ProviderNotConfigured, WarrantyLookupError
from ..services import (
get_provider, ProviderNotConfigured, WarrantyLookupError,
run_dell_sync, ROUTE_SYNC_LIMIT,
)
warranty_bp = Blueprint('warranty', __name__)
@@ -320,118 +323,36 @@ def refresh_warranty(warrantyid):
@warranty_bp.route('/sync/dell', methods=['POST'])
@require_permission('warranty.edit')
def sync_dell():
"""Look up every asset serial (= Dell service tag) that has no warranty yet
"""Look up asset serials (= Dell service tags) that have no warranty yet
and create warranties for the ones Dell recognizes.
Bounded to ROUTE_SYNC_LIMIT tags per call. Dell is queried 100 tags at a
time, so an unbounded fleet-sized sync is dozens of sequential HTTP calls
inside one request and a worker timeout or proxy kills it first. The
response carries `remaining` so the caller knows to run it again; nothing
drains it in the background. `flask warranty sync-dell` does the same work
from the command line for a large first-run backfill.
Only assets that lack a dated warranty are looked up, so re-running is cheap
and non-Dell serials are filtered out by Dell (they return no coverage).
"""
provider = get_provider('dell')
# ?all=true re-checks assets that already have a dated warranty too.
recheck_all = request.args.get('all', 'false').lower() == 'true'
# Assets already covered by a dated warranty - skipped unless recheck_all.
covered = set()
if not recheck_all:
for link in WarrantyAsset.query.all():
w = db.session.get(Warranty, link.warrantyid)
if w and w.isactive and w.enddate:
covered.add(link.assetid)
# Candidate assets: active, have a serial, not already covered.
candidates = (Asset.query
.filter(Asset.isactive.is_(True),
Asset.serialnumber.isnot(None),
Asset.serialnumber != '')
.all())
by_tag = {}
for asset in candidates:
if asset.assetid in covered:
continue
by_tag.setdefault(asset.serialnumber.strip().upper(), []).append(asset.assetid)
if not by_tag:
return success_response({
'candidates': 0, 'matched': 0, 'created': 0, 'updated': 0,
}, message='No PCs need a warranty lookup.')
# by_tag is plain strings and ints, so it survives the commit. Release the
# connection before bulk_lookup: Dell is called 100 tags at a time and a
# fleet-sized sync would otherwise pin a pooled connection for minutes.
db.session.commit()
try:
results = provider.bulk_lookup(list(by_tag.keys()))
summary = run_dell_sync(limit=ROUTE_SYNC_LIMIT, recheck_all=recheck_all)
except (ProviderNotConfigured, WarrantyLookupError) as exc:
return error_response(ErrorCodes.VALIDATION_ERROR, str(exc), http_code=400)
# Preload every warranty already linked to a candidate asset in one query.
# Done per asset this was a query per asset plus one per link, which would
# just trade the long network hold for a long write transaction.
matched_assetids = [assetid
for tag, assetids in by_tag.items() if results.get(tag)
for assetid in assetids]
existing_by_assetid = {}
if matched_assetids:
rows = (db.session.query(WarrantyAsset.assetid, Warranty)
.join(Warranty, Warranty.warrantyid == WarrantyAsset.warrantyid)
.filter(WarrantyAsset.assetid.in_(matched_assetids))
.all())
for assetid, warranty in rows:
existing_by_assetid.setdefault(assetid, []).append(warranty)
if not summary['tags']:
return success_response(summary, message='No PCs need a warranty lookup.')
created = updated = matched = 0
now = datetime.now(timezone.utc).replace(tzinfo=None)
for tag, assetids in by_tag.items():
found = results.get(tag)
if not found:
continue
matched += 1
for assetid in assetids:
# Reuse an existing Dell warranty for this asset if there is one.
existing = None
for candidate in existing_by_assetid.get(assetid, []):
# Reuse a warranty that is Dell by ANY signal, not just
# provider: a manually-added or imported Dell warranty carries
# provider 'manual' (create default) but the same service tag or
# a "Dell" vendor. Matching only provider=='dell' made re-check
# duplicate every one of those instead of updating it.
if (candidate.provider == 'dell'
or (candidate.servicetag or '').strip().upper() == tag
or (candidate.vendor or '').strip().lower() == 'dell'):
existing = candidate
break
if existing:
# Canonicalize to Dell so later re-checks match by provider and
# never fall through to a duplicate.
existing.provider = 'dell'
existing.vendor = existing.vendor or 'Dell'
existing.servicelevel = found.get('servicelevel')
existing.startdate = _vendor_date(found.get('startdate'))
existing.enddate = _vendor_date(found.get('enddate'))
existing.servicetag = tag
existing.lastcheckeddate = now
updated += 1
else:
warranty = Warranty(
vendor='Dell', provider='dell', servicetag=tag,
servicelevel=found.get('servicelevel'),
startdate=_vendor_date(found.get('startdate')),
enddate=_vendor_date(found.get('enddate')),
lastcheckeddate=now,
)
warranty.links.append(WarrantyAsset(assetid=assetid))
db.session.add(warranty)
created += 1
db.session.commit()
return success_response({
'candidates': sum(len(v) for v in by_tag.values()),
'tags': len(by_tag),
'matched': matched,
'created': created,
'updated': updated,
}, message=f'Dell sync: {created} added, {updated} updated, {matched} tags matched.')
message = (f"Dell sync: {summary['created']} added, {summary['updated']} updated, "
f"{summary['matched']} tags matched.")
if summary['remaining']:
message += (f" {summary['remaining']} still to check - run the sync again "
f"to continue.")
return success_response(summary, message=message)
# =============================================================================