Guarantee idempotent transactional delivery and ambiguity-safe provider state
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 35/100
- Loại issue
- Tính năng
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Ít trao đổi
- Công nghệ
- postgresql, python
- Lĩnh vực
- backend-api-design, databases, testing
Hướng nghiên cứu
Bắt đầu với docs/PROCESS.md, baseline đã được kiểm toán và các contract hiện có trong mailing/models.py, mailing/services/transactional.py và mailing/services/transactional_sender.py. Chạy các bài kiểm thử tập trung cho transactional API, sender và SQS worker trên đường dẫn PostgreSQL được repository hỗ trợ, sau đó xác minh rằng các tiêu chí chấp nhận và tiêu chí lỗi bắt buộc đã được bao phủ mà không để lộ dữ liệu được bảo vệ hoặc thử lại công việc không rõ ràng của provider.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
External parent epic: DataTalksClub/website#21
Template/credential prerequisite: #1
Audited baseline: c0fdfac632f057f1e6a467111613928c4899b13b
Outcome
Make one accepted Relay transactional-send command converge to one immutable Relay message and at most one automatic provider submission across request replay, concurrent first insert, worker crash, lease expiry, provider timeout, and response loss.
Relay must require request-hash idempotency, use guarded lease/fencing state transitions, distinguish provider acceptance from delivery, and quarantine uncertain provider acknowledgement as ambiguous. It must recheck suppression and sender eligibility atomically at claim immediately before provider submission.
Baseline gaps
At the audited commit:
idempotency_keyis optional and an empty key is replaced internally;- an existing client/key returns without comparing delivery-affecting request content;
- the check-before-insert path does not recover a concurrent first-insert uniqueness race;
- messages identify mutable template keys rather than immutable versions;
- status is limited to
queued,sending,sent,failed,skipped,bounced, andcomplained; sendinghas no lease owner/token/expiry or recorded attempt boundary;- uncertain provider calls return rows to
queuedand may be retried blindly; sentconflates provider acceptance with later delivery; and- bounce/complaint/category suppression is checked before queueing, not atomically at claim.
Scope
Mandatory request-hash idempotency
- Require a non-empty caller
idempotency_keyfor every real transactional send. - Compute a versioned canonical SHA-256 request hash over tenant/client plus every delivery-affecting field:
- normalized primary recipient;
- purpose/classification;
- immutable
template_keyandtemplate_versionfrom #1; - canonical typed context;
- resolved sender ID/address and reply-to;
- normalized CC/BCC;
- normalized headers and structured message parts/options.
- Persist the hash with the message and record the hash-contract version.
- Exact tenant/key/hash replay returns the original message/status and marks the response as an idempotent replay.
- Same tenant/key with a different hash returns
409 idempotency_conflictand creates/enqueues nothing. - Concurrent first submissions converge through the database uniqueness constraint: the loser reloads the winner, compares the canonical hash, and returns replay or conflict. Integrity errors must not escape as
500. - Response loss after commit is recoverable by exact replay of the original command.
Immutable preparation
- Require the immutable versioned template contract from #1.
- Validate/render/resolve sender and persist the recipient, version, rendered subject/plain/HTML, sender, options, and request hash before queueing provider work.
- A queued message never re-renders from a mutable draft/current version.
- Queue payloads contain scalar identifiers and an immutable/fenced message reference, not raw protected bodies or credentials.
Guarded lifecycle and attempts
Implement explicit guarded states:
queued: committed and eligible for claim;leased: held by one owner/token until a bounded expiry;provider_accepted: provider returned an accepted message identifier;delivered: authoritative later delivery event;retryable: a known pre-provider/transient failure eligible after bounded backoff;ambiguous: provider may have accepted but Relay cannot prove the outcome;suppressed: claim-time policy prevented submission;dead: permanent failure or retry exhaustion;hard_bouncedandcomplained: terminal deliverability outcomes.
Record append-only attempts with start/end, lease owner/token, outcome, safe reason/error code, provider correlation, and timestamps. Do not store raw provider payloads or credentials.
- Claim with row locking plus owner/token/expiry fencing.
- Only the current lease token may transition or complete the attempt.
- An expired lease may be reclaimed only when the attempt is provably pre-provider.
- Known failures before provider invocation may become
retryablewith bounded exponential backoff. - Any timeout/connection loss/crash during or after the provider request without a definitive result becomes
ambiguousand is never automatically retried. - Permanent validation/provider rejection or exhausted retries becomes
dead. - Provider acceptance is
provider_accepted, never displayed or serialized asdelivered. - Guard against status regression and duplicate terminal transitions.
Claim-time suppression and sender eligibility
Within the claim transaction immediately before provider invocation:
- reload current hard-bounce, complaint, applicable transactional/category suppression, sender eligibility, and tenant/client state;
- if blocked, record
suppressedplus a safe reason and make no provider call; - prove a suppression or credential/client-state change after queueing but before claim prevents submission; and
- avoid holding a database transaction open across the network request while preserving the fenced decision/attempt boundary.
Redacted status contract
Update GET /api/transactional/messages/{message_id} and OpenAPI to return:
- authoritative current state;
- immutable template key/version and safe sender ID;
- request/correlation identifiers safe for client use;
- ordered redacted transition/attempt timeline;
- lease/attempt summary without worker secrets;
- provider-accepted versus delivered timestamps;
- safe reason codes and retry/ambiguity eligibility.
Do not expose bodies, context, full provider payload, credentials, or unrelated tenant identifiers.
Expected files
Implementation should remain within Relay-owned delivery surfaces such as:
mailing/models.pyand new forward-only migrations;mailing/services/transactional.py;mailing/services/transactional_sender.py;mailing/queue_contracts.pyandmailing/enqueue.py;mailing/services/contacts.py,mailing/services/senders.py, and focused transition helpers;mailing/views.py,mailing/services/api_docs.py, anddocs/api.md;mailing/tests/test_transactional_api.py;mailing/tests/test_transactional_sender.py;- queue/worker and PostgreSQL concurrency tests under
mailing/tests/.
Do not redesign campaigns, inbound mail, Mailchimp, Taskdeck, website jobs, or infrastructure.
Acceptance criteria
- A caller idempotency key is mandatory and one canonical versioned request hash covers every delivery-affecting field.
- Exact replay returns the original message; same key/different hash returns
409with no new row, event, queue item, attempt, or provider call. - Concurrent first insert returns replay/conflict deterministically and produces one message plus one queue item without a
500. - Immutable template/version, rendered content, sender, recipient/options, and request hash are snapshotted before queueing.
- State transitions are guarded and provider acceptance is never reported as delivery.
- Claims have owner/token/expiry fencing; expired provably pre-provider work can recover, while uncertain provider work cannot re-enter automatic send.
- Before-provider transient failures use bounded backoff; exhausted/permanent failures become
dead. - During/after-provider timeout, response loss, or crash becomes
ambiguousand no automatic retry can submit it again. - Claim-time hard-bounce/complaint/category suppression and sender/client eligibility prevent a provider call when state changes after queueing.
- Ordered attempts/transitions and the message status response are redacted, tenant-scoped, and safe.
- Queue payloads, logs, errors, audit, test output, and OpenAPI examples contain no credentials, protected body/context, or full provider payload.
- Focused unit, migration, queue/worker, and real PostgreSQL concurrency tests pass with deterministic provider fakes and no external network/AWS credentials.
Mandatory failure tests
- Exact replay, changed recipient/version/context/sender/options, and canonical JSON/key ordering.
- Two concurrent first requests with the same key/same hash and same key/different hash.
- Crash before claim, after claim/before provider, during provider call, after provider acceptance/before local acknowledgement, and after local acknowledgement.
- Lease expiry before provider, stale lease-token completion, competing claims, retry exhaustion, and forbidden transition from
ambiguousto automatic send. - Hard bounce, complaint, category suppression, client disablement, sender disablement, or credential policy change after queue and before claim.
- Provider throttle/transient failure, permanent rejection, accepted response, delivered event, late bounce/complaint, and duplicate/out-of-order terminal events.
- Tenant isolation plus redaction canaries in responses, logs, errors, events, queue payloads, and test artifacts.
Validation
Use uv and provider fakes/stubs only:
git diff --check
uv run python manage.py makemigrations --check --dry-run
uv run pytest mailing/tests/test_transactional_api.py mailing/tests/test_transactional_sender.py mailing/tests/test_sqs_worker.py
make lint
make test
The tester must run the concurrency cases against PostgreSQL in the repository-supported isolated test path. No live Relay sandbox call, AWS credential, SES request, or email send is permitted.
Browser and screenshots
This is a backend delivery/API contract. No visible Relay page is required to change, so browser testing and screenshots are not applicable unless implementation deliberately expands UI scope and obtains PM approval.
Dependencies
- External ownership and consumer requirements: website #21.
- Depends on #1 for scoped credentials, immutable
template_key+template_version, safe rendering, and snapshot inputs. - The generic callback/reconciliation issue depends on this lifecycle's states, attempts, and redacted transition events.
- Website real-send remains disabled until #1, this issue, and the callback/reconciliation issue are deployed at exact pinned commit/OpenAPI versions and pass consumer tests.
Non-goals
- No website
EmailDeliverymodel, job, callback endpoint, Studio/admin API, or Datamailer migration. - No template authoring/version/rendering implementation beyond consuming #1's contract.
- No generic outbound callback signing/delivery or unmatched provider-event reconciliation; owned by the callback/reconciliation issue.
- No automatic operator resolution or resend from
ambiguous. - No live credential provisioning, infrastructure mutation, deployment, production sender/domain, broad recipient access, or external email.
- No campaign/newsletter, tracking-pixel, or engagement-analytics redesign.
Process handoff
Follow docs/PROCESS.md in an isolated Relay issue worktree. The engineer implements and tests without committing; a separate tester verifies exact failure/concurrency criteria; a product manager accepts; only then is the issue committed with Closes #N and moved through the repository lifecycle without a pull request.
- Ngôn ngữ chính
- Python
- Star
- 0
- Fork
- 0
- Merge trung bình
- 8 giờ 34 phút
- Pull request đã merge (30 ngày)
- 10
Chuẩn bị môi trường
- Có Dockerfile hoặc tệp Docker Compose
- Không có mẫu pull request
- Không có hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của DataTalksClub/relay
-
DataTalksClub/relay#48 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 20/100
DataTalksClub/relay#42 · 7 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 15/100
DataTalksClub/relay#41 · 9 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
community-base enhancement
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 25/100
DataTalksClub/relay#39 · 6 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
community-base enhancement
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 15/100
DataTalksClub/relay#37 · 15 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của DataTalksClub/relay
Issue tương tự
-
Project submission:Đang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 72/100
slavakurilyak/awesome-ai-agents#845 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
tests: pin that the atomic writers in utils/paths.py stage the temporary file in the target's directoryCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mởdifficulty:2 enhancement good first issue
Độ khó 1/5 1-3 giờ Mức phù hợp với người mới 92/100
MakazhanAlpamys/Soup#2178 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[BUG] dcg.waggr permutation p-values are folded twice, so near-zero scores get the smallest p-valuesĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
scverse/rapids-singlecell#854 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Slang input: int8_t4_packed and uint8_t4_packed are copied into CrossGLCó thể đã có người làm @ninglinLiu đã nhận 1 ngày trước. Đang mởarea:frontend backend:slang bug direction:input
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
Maintainer thường phản hồi trong vòng 1 ngày
-
random_elements(unique=True) on an OrderedDict: ZeroDivisionError once only zero weights remain, AssertionError with use_weighting=FalseCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
Maintainer thường phản hồi trong vòng 13 ngày