chore: allow for aborting loading campaigns

This commit is contained in:
Chris Anderson 2025-07-23 07:29:37 -05:00
parent c17e3458b1
commit a8f42ddd58
6 changed files with 32 additions and 16 deletions

View file

@ -51,6 +51,11 @@ export default class Campaign extends Model {
eventName(action: string) {
return `${this.channel}_${action}`
}
get isAborted() { return this.state === 'aborted' || this.state === 'aborting' }
get isAbortedOrDraft() {
return this.isAborted || this.state === 'draft'
}
}
export type CampaignPopulationProgress = {

View file

@ -1,5 +1,4 @@
import { logger } from '../config/logger'
import { releaseLock } from '../core/Lock'
import { Job } from '../queue'
import { CampaignJobParams, SentCampaign } from './Campaign'
import CampaignEnqueueSendsJob from './CampaignEnqueueSendsJob'
@ -13,13 +12,11 @@ export default class CampaignGenerateListJob extends Job {
}
static async handler({ id, project_id }: CampaignJobParams) {
const key = `campaign_generate_${id}`
logger.info({ campaign_id: id }, 'campaign:generate:loading')
const campaign = await getCampaign(id, project_id) as SentCampaign
if (!campaign) return
if (campaign.state === 'aborted' || campaign.state === 'draft') return
if (campaign.isAbortedOrDraft) return
try {
logger.info({ campaignId: id }, 'campaign:generate:populating')
@ -30,14 +27,9 @@ export default class CampaignGenerateListJob extends Job {
id: campaign.id,
project_id: campaign.project_id,
}).queue()
await releaseLock(key)
} catch (error) {
logger.info({ campaignId: id, error }, 'campaign:generate:failed')
throw error
} finally {
logger.info({ campaignId: id }, 'campaign:generate:lock_released')
await releaseLock(key)
}
}
}

View file

@ -23,7 +23,6 @@ import Template from '../render/Template'
import { differenceInDays, subDays } from 'date-fns'
import { cacheBatchHash, cacheBatchReadHashAndDelete, cacheDel, cacheGet, cacheHashExists, cacheIncr, cacheSet, DataPair, HashScanCallback } from '../config/redis'
import App from '../app'
import { releaseLock } from '../core/Lock'
import CampaignAbortJob from './CampaignAbortJob'
import { getRuleQuery } from '../rules/RuleEngine'
import { getJourneysForCampaign } from '../journey/JourneyService'
@ -343,20 +342,27 @@ const generateSendList = async (project: Project, campaign: SentCampaign, callba
await cacheSet(redis, CacheKeys.populationTotal(campaign), count, 86400)
await cacheSet(redis, CacheKeys.generateReady(campaign), 1, 86400)
// Double check that the campaign hasn't been aborted
const updatedCampaign = await getCampaign(campaign.id, campaign.project_id) as SentCampaign
if (updatedCampaign.isAborted) return
// Now that we have results, pass them back to the callback
return await cacheBatchReadHashAndDelete(redis, hashKey, callback)
}
const cleanupSendListGeneration = async (campaign: Campaign) => {
const redis = App.main.redis
const { pending, ...delivery } = await campaignDeliveryProgress(campaign.id)
// Update the state & count of the campaign
await Campaign.update(qb => qb.where('id', campaign.id).where('project_id', campaign.project_id), { state: 'scheduled', delivery })
// Clear out all the keys related to the generation
await cacheDel(redis, CacheKeys.generateReady(campaign))
await cleanupGenerationCacheKeys(campaign)
}
const cleanupGenerationCacheKeys = async (campaign: Campaign) => {
const redis = App.main.redis
await cacheDel(redis, CacheKeys.generate(campaign))
await cacheDel(redis, CacheKeys.populationTotal(campaign))
await cacheDel(redis, CacheKeys.populationProgress(campaign))
}
@ -478,7 +484,7 @@ export const abortCampaign = async (campaign: Campaign) => {
.where('campaign_id', campaign.id)
.where('state', 'pending')
.update({ state: 'aborted' })
await releaseLock(`campaign_generate_${campaign.id}`)
await cleanupGenerationCacheKeys(campaign)
}
export const clearCampaign = async (campaign: Campaign) => {

View file

@ -39,7 +39,7 @@
"campaign_form_lists": "Select what lists to send this campaign to and what user lists you want to exclude from getting the campaign.",
"campaign_form_select_list": "Select one or more lists using the button above.",
"campaign_form_type": "Should a campaign be sent as a blast to a list of users or triggered individually via API.",
"campaign_list_generating": "This list is still generating. Sending before it has completed could result in this campaign not sending to all users who will enter the list. Are you sure you want to continue?",
"campaign_list_generating": "This list is still generating or has not been published. Sending before it has completed could result in this campaign not sending to all users who will enter the list. Are you sure you want to continue?",
"campaign_name": "Campaign Name",
"campaigns": "Campaigns",
"campaign": "Campaign",

View file

@ -58,4 +58,11 @@
.heading label, .heading input {
margin: 0;
}
@media only screen and (max-width: 600px) {
.heading {
flex-direction: column;
gap: 10px;
}
}

View file

@ -120,7 +120,13 @@ export default function CampaignDetail() {
isLoading={true}
>{t('abort_campaign')}</Button>
),
loading: <></>,
loading: (
<Button
icon={<ForbiddenIcon />}
isLoading={isLoading}
onClick={async () => await handleAbort()}
>{t('abort_campaign')}</Button>
),
scheduled: (
<>
<Button