Campaign Schedule Dispatch (process_campaign)
Overview
A campaign scheduled through the CMS is stored as a row in the campaign table with status scheduled and a start_date.
This feature is the scanner that runs every minute, finds campaigns whose send time has arrived, claims them atomically, and then enqueues the real work into either line_broadcast_rich_message or line_multicast_rich_message depending on cast_type.
The central concern here is preventing duplicate sends. Whether several worker replicas scan at the same time or a message is redelivered, the end recipient must never receive the campaign twice.
Business Flow
- Tick — a
*/1 * * * *cron on thecron-schedulerprofile (CampaignScheduleScannerService) publishes a single empty message to theprocess_campaignqueue.- A Redis
SET NXon the keyCAMPAIGN_SCHEDULE_TICK:<yyyy-MM-ddTHH:mm>(TTL 55 seconds) ensures only one replica publishes. - If Redis is down, the tick still publishes, because the database-level claim already guarantees correctness.
- This tick previously came from external infrastructure and has since been moved into the repository.
- A Redis
- Reaper — the
mainprofile receives the tick and first recovers any campaign stuck insendingwhoseclaimed_atis older than 15 minutes, resetting it toscheduledon the assumption that the claiming worker has died. - Atomic claim — issue
UPDATE campaign SET status='sending', claimed_at=NOW()guarded bystatus='scheduled' AND start_date <= NOW() AND deleted_date IS NULLwith aRETURNINGclause. Only rows this tick actually claimed come back, so no other scanner can pick them up. - Enqueue — for each claimed campaign, publish a payload containing
campaignId,audienceId,organizationId, andlineOaIdto the queue matchingcast_type:broadcastgoes toline_broadcast_rich_message, everything else toline_multicast_rich_message. - Roll back on publish failure — the status is reverted to
scheduledimmediately on a best-effort basis, so the next tick can retry it without waiting the full 15 minutes for the reaper. - Heartbeat while sending — the delivery consumer refreshes
claimed_atevery 5 minutes during the send so the reaper does not reclaim the campaign mid-flight (see Multicast / Targeted Campaign Delivery). - Re-check at the destination — the delivery consumer verifies the status once more. If the campaign is no longer
sending— for instance it is alreadysent— the message is discarded quietly as a permanent error without flipping the campaign to failed.
Key Files & Functions
internal/campaign/campaign.goService.ProcessCampaign(ctx)— reaper, atomic claim, and enqueue in one pass- The
campaignStatusScheduled,campaignStatusSending,campaignStatusSent,campaignStatusCancel,campaignStatusDraft,campaignStatusFailed,castTypeBroadcast, andcastTypeMulticastconstants
internal/campaign/consumer.go—Consumer.HandleProcessCampaignfor theprocess_campaignqueue, plusConsumer.Register()internal/cronscheduler/campaign_schedule_scanner.go—CampaignScheduleScannerService.Run(ctx)andcampaignTickKeyPrefixcmd/worker/integration.go—campaignForLineMessageApi.GetCampaignAndValidate(), the point where thesendingstatus is verified and duplicate delivery is blocked- Queues: consumes
process_campaignon themainprofile and publishes toline_broadcast_rich_messageorline_multicast_rich_message
Connections to Other Services
- The
campaigntable — reads and writesstatus,claimed_at,start_date,cast_type,audience_id,organization_id, andline_oa_id - Redis — the
CAMPAIGN_SCHEDULE_TICK:*keys used to deduplicate ticks across replicas - RabbitMQ — consumes
process_campaignand publishes to the actual delivery queues - cms-api-go — creates and edits campaigns and sets
start_datein the campaign-management domain. When a user chooses to send immediately, cms-api-go flips the status tosendingand enqueues the job itself without waiting for the scanner. - This feature makes no direct LINE API calls; those happen during delivery