This commit is contained in:
Jonas Bengtsson
2026-08-31 23:28:35 +02:00
parent b553a9bf86
commit 3e0379e001
4 changed files with 172 additions and 66 deletions
+8 -5
View File
@@ -109,7 +109,7 @@ def main() -> None:
topic = message.topic
with imei_lock:
current_map = imei_map
result = process_message(
results = process_message(
topic,
bytes(message.payload),
current_map,
@@ -117,15 +117,18 @@ def main() -> None:
device_key=device_key,
time_keys=time_keys,
)
if result is None:
if not results:
logger.warning(
"skipping message on %r: payload is not a JSON object or last topic "
"skipping message on %r: no JSON object(s) in payload or last topic "
"segment is not a 15-digit IMEI",
topic,
)
return
inserter.insert(topic, result.imei, result.device, result.payload, result.payload_time)
logger.info("stored %s/%s -> %s", result.imei, result.device, topic)
for result in results:
inserter.insert(
topic, result.imei, result.device, result.payload, result.payload_time
)
logger.info("stored %s/%s -> %s", result.imei, result.device, topic)
except Exception:
logger.exception("message handling failed for topic %s", getattr(message, "topic", "?"))
+43 -26
View File
@@ -97,16 +97,20 @@ def process_message(
imei_key: str = "imei",
device_key: str = "device",
time_keys: tuple[str, ...] = DEFAULT_TIME_KEYS,
) -> InjectionResult | None:
) -> list[InjectionResult]:
"""Parse a payload and inject IMEI + device into it, extracting the
device timestamp.
Returns None when the payload is not valid JSON or not a JSON object —
those messages are skipped (logged by the caller). The original payload's
own imei/device fields are overwritten by the values derived from the
topic, so the topic is always authoritative.
The payload may be a single JSON object or a JSON array of objects:
every object yields one result (the caller stores one database row per
result). Returns an empty list when the payload is not valid JSON,
contains no JSON object at all (a scalar, or an array without objects)
or the last topic segment is not a 15-digit IMEI — those messages are
skipped (logged by the caller). The payload's own imei/device fields
are overwritten by the values derived from the topic, so the topic is
always authoritative.
A missing or unparseable timestamp does NOT skip the message: the
A missing or unparseable timestamp does NOT skip the object: its
payload_time is simply None and only received_at (server receipt time)
exists for the row.
"""
@@ -116,29 +120,42 @@ def process_message(
normalized = normalize_imei(raw_imei)
if normalized is None:
return None
return []
try:
payload = json.loads(payload_bytes.decode("utf-8"))
parsed = json.loads(payload_bytes.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
return None
if not isinstance(payload, dict):
return None
return []
payload = dict(payload) # do not mutate shared objects
payload[imei_key] = normalized
payload[device_key] = imei_map.get(normalized, normalized)
if isinstance(parsed, dict):
items: list[dict] = [parsed]
elif isinstance(parsed, list):
# Arrays may mix objects with non-objects; only the objects are
# messages. Nested arrays are NOT flattened.
items = [item for item in parsed if isinstance(item, dict)]
else:
return []
payload_time = None
for key in time_keys:
if key in payload:
payload_time = parse_payload_time(payload[key])
if payload_time is not None:
break
device = imei_map.get(normalized, normalized)
results: list[InjectionResult] = []
for item in items:
payload = dict(item) # do not mutate shared objects
payload[imei_key] = normalized
payload[device_key] = device
return InjectionResult(
imei=normalized,
device=payload[device_key],
payload=payload,
payload_time=payload_time,
)
payload_time = None
for key in time_keys:
if key in payload:
payload_time = parse_payload_time(payload[key])
if payload_time is not None:
break
results.append(
InjectionResult(
imei=normalized,
device=payload[device_key],
payload=payload,
payload_time=payload_time,
)
)
return results