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:
@@ -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)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user