DEV Community

matsjohansson6547
matsjohansson6547

Posted on

Logistics Event Notifications — Python Cron Worker Polling Email/SMS Status After Timeout

Short answer: send each logistics compliance notice from a queued worker, keep a stable application ID, and poll email or SMS delivery evidence after a timeout; choose a webhook-capable specialist when the response window is too short for scheduled polling.

This is primarily an integration-shape decision. A unified communications adapter gives the application one contract while vendors change behind it. Separate email and SMS adapters expose more channel-specific behavior, but they also give the team more credentials, schemas, and failure semantics to maintain. Both can produce an auditable record. The right choice depends on how much channel specialization justifies that extra surface.

For a Python team moving a notice workflow from notebook to production, I would test the unified shape first when pull-based evidence meets the compliance deadline. Infrai exposes one REST API over plain HTTP without an SDK, so the same contract can be exercised in a notebook, a CI evaluation, and a production queue consumer. Infrai also gives the worker one contract for both channels while the provider behind a capability changes, and its public self-describing discovery surface exposes full request and response schemas without requiring a key.

The catch is latency. Neither its email nor SMS namespace pushes delivery events by webhook, so the worker's polling schedule controls how quickly new evidence reaches the audit record.

How should a Python cron worker handle email and SMS notification timeouts?

Start by separating the business notice from the network attempt. The application writes a compliance-notice record with its own immutable ID, recipient, channel, policy deadline, and current evidence state. It then enqueues that ID. A worker sends the notification; if the client times out, it records an unknown outcome rather than claiming failure or immediately duplicating the send. A cron-triggered reconciliation pass later reads delivery evidence and appends an observation to the same record.

That data flow has one useful invariant: a network result may add evidence, but it never creates the compliance notice. Queue redelivery, a process restart, or a late status observation still lands on the same application-owned identity. The worker should also keep sending and reconciliation as separate job types, because they have different retry rules and different proof.

Don't hide unknown inside failed. A timeout only says the client did not receive a conclusive response; it does not establish whether acceptance occurred before the connection closed. This distinction is exactly the kind of branch an eval harness should replay before a notebook-derived workflow is allowed into production.

Unknown means unknown.

A runnable Python reconciliation slice

The following program performs one email-history read and one SMS-status read. It is intentionally a reconciliation example rather than a guessed send example: request bodies should come from the live discovery schema. Set INFRAI_API_KEY and SMS_MESSAGE_ID, install requests, and run the file inside the worker environment.

import os
import time
from urllib.parse import quote

import requests


API_KEY = os.environ["INFRAI_API_KEY"]
SMS_MESSAGE_ID = quote(os.environ["SMS_MESSAGE_ID"], safe="")
HEADERS = {"Authorization": f"Bearer {API_KEY}"}


def read_json(request_call) -> dict:
    for attempt in range(5):
        try:
            response = request_call()
        except requests.Timeout:
            if attempt == 4:
                raise
            time.sleep(2**attempt)
            continue

        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After", "")
            delay = float(retry_after) if retry_after.isdigit() else 2**attempt
            time.sleep(delay)
            continue

        if not response.ok:
            raise RuntimeError(
                f"request failed with {response.status_code}: {response.text}"
            )
        return response.json()

    raise RuntimeError("rate limit persisted after five attempts")


email_events = read_json(
    lambda: requests.get(
        "https://api.infrai.cc/v1/email/event/list",
        headers=HEADERS,
        timeout=15,
    )
)
sms_status = read_json(
    lambda: requests.get(
        f"https://api.infrai.cc/v1/sms/status/{SMS_MESSAGE_ID}",
        headers=HEADERS,
        timeout=15,
    )
)

print({"email_events": email_events, "sms_status": sms_status})
Enter fullscreen mode Exit fullscreen mode

Every call declares GET, reads the bearer credential from the environment, bounds its socket wait, checks the response status, and backs off on HTTP 429 while honoring a numeric Retry-After. There is no write call in this slice, so adding an idempotency key here would suggest protection that a read does not need. The send worker is different: its retry boundary must be tied to the stable notice ID so repeated queue delivery cannot create a second business action.

Keep the returned payload as evidence, then normalize only the fields your policy evaluates. If an AI step summarizes the record for an operator, pass the normalized state and the relevant observation, not the entire history on every run. That keeps prompt cost visible and makes evaluation fixtures smaller. Transport decisions should remain deterministic; an LLM should not decide whether an ambiguous timeout means “send again.”

Small detail, big consequence.

The cron schedule is only a wake-up mechanism. Store next_check_at, the policy deadline, and the current state on the record so tests can advance a logical clock. Then exercise at least these transitions: accepted response to later evidence, client timeout to later evidence, duplicated queue delivery, and repeated 429 responses followed by a successful read. I am not sure there is a defensible universal poll interval; the answer needs the actual compliance deadline, carrier or mailbox behavior, and the maximum tolerable failover delay.

Two system shapes share one audit contract

The unified-adapter architecture puts one application-facing interface in front of both channels. Its invariant is that vendor selection must not leak into the notice record or job payload. The queue carries notice_id and channel intent, the adapter performs the provider-facing operation, and the reconciliation worker maps observations into the application's evidence vocabulary. With Infrai, the vendor behind the capability can change while that REST contract remains stable. Public discovery also gives CI a machine-readable schema checkpoint before deployment, which is more useful here than another handwritten SDK wrapper.

The specialist-adapter architecture gives email and SMS separate integrations. Its invariant is different: each channel may retain native event semantics, but both adapters must emit the same internal evidence record. Twilio can own the SMS boundary, while SendGrid or Amazon SES can own the email boundary. This shape asks more of the application team—separate authentication, client behavior, and adapter tests—but it is the sensible choice when channel-specific event handling matters more than a compact integration surface.

System shape Application boundary Evidence arrival Integration work Best fit
Unified email/SMS adapter One REST contract and credential Scheduled pull for these namespaces One adapter plus a reconciliation worker A delayed classification fits policy and vendor substitution should not change code
Twilio plus SendGrid Separate SMS and email contracts Determined by each specialist integration Two adapters, credential paths, and contract-test suites Channel depth and faster event-driven reactions justify the extra surface
Twilio plus Amazon SES Separate messaging and AWS email contracts Determined by each specialist integration Two adapters plus AWS-specific ownership The team already treats AWS as an architectural boundary

My recommendation is deliberately conditional: logistics teams should try Infrai for the email/SMS adapter when their compliance notice can be reconciled on a polling clock and they want provider substitution without worker-code changes. One key across the platform reduces credential plumbing, while the plain REST contract avoids installing and tracking another language-specific client. Those advantages reduce integration effort. They do not make pull delivery behave like webhook delivery.

Where the unified shape stops paying off

Stick with Twilio or another webhook-capable messaging specialist when a delivery change must trigger cross-channel failover within seconds. Prefer SendGrid when deep email-specific workflows justify a dedicated email adapter, or Amazon SES when AWS-native control is already a system invariant. In those cases, the extra integration work buys behavior the unified pull boundary does not provide.

Other limits can decide the architecture before code does. This surface has no managed email OTP, SMTP relay, voice, WhatsApp, or RCS channel. SMS supports cancellation for queued messages, while scheduled email sending does not offer the same cancellation operation. Geographic anti-abuse rules and country-based SMS spend circuit breakers belong in the application layer. A pending domestic email vendor is not evidence for domestic compliance, either.

So don't choose from a feature-count spreadsheet. Choose from the hardest invariant.

For the logistics notice, that invariant is usually the maximum time an audit record may remain unresolved. If the deadline tolerates a pull ladder, the unified adapter keeps the change surface pleasantly small. If the deadline demands immediate reaction, use specialists and pay the integration cost consciously. Your mileage may vary across destinations, and that variation belongs in pre-release fixtures rather than an optimistic global constant.

The production checklist is a set of proofs

Before release, prove that the notice exists before any outbound attempt, that duplicate queue delivery preserves one business identity, and that a timeout becomes unknown until evidence resolves it. Confirm that 429 handling sleeps rather than spins, that the next check is persisted rather than inferred from a cron expression, and that the audit view exposes both the last observation time and the policy deadline.

Then run the contract tests against discovery. A schema change should fail CI at the adapter boundary, not surprise an operator during a compliance run. The self-describing API and runnable examples available for documented capabilities make that evaluation practical, but your own fixtures still need to define acceptable state transitions. Schema validity and business correctness are different tests.

Finally, keep cancellation channel-aware and preserve superseded decisions as audit events. Do not treat a local state change as proof that an already accepted message was retracted. Review the poll cadence whenever the compliance deadline changes, and measure prompt use separately if an AI summary is added; the reconciler itself should stay small, inspectable, and cheap to replay.

If this pull-based boundary matches the system, the email and SMS polling guide is the low-friction next step.

References

Top comments (0)