2018-09-02 12:59:02 +00:00
'use strict' ;
2018-09-02 18:17:42 +00:00
const config = require ( 'config' ) ;
2019-05-25 19:57:11 +00:00
const fork = require ( '../lib/fork' ) . fork ;
2018-09-27 19:32:35 +00:00
const log = require ( '../lib/log' ) ;
2018-09-02 12:59:02 +00:00
const path = require ( 'path' ) ;
2018-09-02 18:17:42 +00:00
const knex = require ( '../lib/knex' ) ;
2018-11-18 14:38:52 +00:00
const { CampaignStatus , CampaignType } = require ( '../../shared/campaigns' ) ;
2018-09-09 22:55:44 +00:00
const campaigns = require ( '../models/campaigns' ) ;
2018-12-23 19:27:29 +00:00
const builtinZoneMta = require ( '../lib/builtin-zone-mta' ) ;
2019-02-07 14:38:32 +00:00
const { CampaignActivityType } = require ( '../../shared/activity-log' ) ;
const activityLog = require ( '../lib/activity-log' ) ;
2019-06-29 21:19:56 +00:00
const { MessageType } = require ( '../lib/message-sender' ) ;
2019-05-25 19:57:11 +00:00
require ( '../lib/fork' ) ;
2019-02-07 14:38:32 +00:00
2019-06-25 05:18:06 +00:00
class Notifications {
constructor ( ) {
this . conts = new Map ( ) ;
}
notify ( id ) {
const cont = this . conts . get ( id ) ;
if ( cont ) {
for ( const cb of cont ) {
setImmediate ( cb ) ;
}
this . conts . delete ( id ) ;
}
}
async waitFor ( id ) {
let cont = this . conts . get ( id ) ;
if ( ! cont ) {
cont = [ ] ;
}
const notified = new Promise ( resolve => {
cont . push ( resolve ) ;
} ) ;
this . conts . set ( id , cont ) ;
await notified ;
}
}
const notifier = new Notifications ( ) ;
2018-09-02 12:59:02 +00:00
let messageTid = 0 ;
2018-09-09 22:55:44 +00:00
const workerProcesses = new Map ( ) ;
2018-09-02 12:59:02 +00:00
2019-06-25 05:18:06 +00:00
const workersCount = config . queue . processes ;
2018-09-09 22:55:44 +00:00
const idleWorkers = [ ] ;
let campaignSchedulerRunning = false ;
2018-11-20 22:41:10 +00:00
let queuedSchedulerRunning = false ;
2018-09-09 22:55:44 +00:00
2019-06-29 21:19:56 +00:00
const checkPeriod = 30 * 1000 ;
2018-09-09 22:55:44 +00:00
const retrieveBatchSize = 1000 ;
2019-06-25 05:18:06 +00:00
const workerBatchSize = 10 ;
2018-09-09 22:55:44 +00:00
2019-06-29 21:19:56 +00:00
const sendConfigurationIdByCampaignId = new Map ( ) ; // campaignId -> sendConfigurationId
const sendConfigurationStatuses = new Map ( ) ; // sendConfigurationId -> {retryCount, postponeTill}
2019-06-25 05:18:06 +00:00
const sendConfigurationMessageQueue = new Map ( ) ; // sendConfigurationId -> [{queuedMessage}]
const campaignMessageQueue = new Map ( ) ; // campaignId -> [{listId, email}]
2018-09-27 19:32:35 +00:00
2019-06-29 21:19:56 +00:00
const workAssignment = new Map ( ) ; // workerId -> { type: WorkAssignmentType.CAMPAIGN, campaignId, messages: [{listId, email} } / { type: WorkAssignmentType.QUEUED, sendConfigurationId, messages: [{queuedMessage}] }
2018-09-09 22:55:44 +00:00
2019-06-29 21:19:56 +00:00
const WorkAssignmentType = {
CAMPAIGN : 0 ,
QUEUED : 1
} ;
2018-09-09 22:55:44 +00:00
2019-06-29 21:19:56 +00:00
const retryBackoff = [ 10 , 20 , 30 , 30 , 60 , 60 , 120 , 120 , 300 ] ; // in seconds
2018-12-23 19:27:29 +00:00
2019-06-29 21:19:56 +00:00
function getSendConfigurationStatus ( sendConfigurationId ) {
let status = sendConfigurationStatuses . get ( sendConfigurationId ) ;
if ( ! status ) {
status = {
retryCount : 0 ,
postponeTill : 0
} ;
2018-09-09 22:55:44 +00:00
2019-06-29 21:19:56 +00:00
sendConfigurationStatuses . set ( sendConfigurationId , status ) ;
}
return status ;
2018-09-09 22:55:44 +00:00
}
2019-06-29 21:19:56 +00:00
function setSendConfigurationRetryCount ( sendConfigurationStatus , newRetryCount ) {
sendConfigurationStatus . retryCount = newRetryCount ;
2019-06-25 05:18:06 +00:00
2019-06-29 21:19:56 +00:00
let next = 0 ;
if ( newRetryCount > 0 ) {
let backoff ;
if ( newRetryCount > retryBackoff . length ) {
backoff = retryBackoff [ retryBackoff . length - 1 ] ;
} else {
backoff = retryBackoff [ newRetryCount - 1 ] ;
2019-06-25 05:18:06 +00:00
}
2018-09-09 22:55:44 +00:00
2019-06-29 21:19:56 +00:00
next = Date . now ( ) + backoff * 1000 ;
setTimeout ( scheduleCheck , backoff * 1000 ) ;
2019-06-25 05:18:06 +00:00
}
2018-09-09 22:55:44 +00:00
2019-06-29 21:19:56 +00:00
sendConfigurationStatus . postponeTill = next ;
}
function isSendConfigurationPostponed ( sendConfigurationId ) {
const now = Date . now ( ) ;
const sendConfigurationStatus = getSendConfigurationStatus ( sendConfigurationId ) ;
return sendConfigurationStatus . postponeTill > now ;
}
2019-06-25 05:18:06 +00:00
2019-06-29 21:19:56 +00:00
function getPostponedSendConfigurationIds ( ) {
const result = [ ] ;
const now = Date . now ( ) ;
2019-06-25 05:18:06 +00:00
2019-06-29 21:19:56 +00:00
for ( const entry of sendConfigurationStatuses . entries ( ) ) {
if ( entry [ 1 ] . postponeTill > now ) {
result . push ( entry [ 0 ] ) ;
2018-09-09 22:55:44 +00:00
}
2019-06-29 21:19:56 +00:00
}
2019-06-25 05:18:06 +00:00
2019-06-29 21:19:56 +00:00
return result ;
}
function getExpirationThresholds ( ) {
const now = Date . now ( ) ;
return {
[ MessageType . TRIGGERED ] : {
threshold : now - config . queue . retention . triggered * 1000 ,
title : 'triggered campaign'
} ,
[ MessageType . TEST ] : {
threshold : now - config . queue . retention . test * 1000 ,
title : 'test campaign'
} ,
[ MessageType . SUBSCRIPTION ] : {
threshold : now - config . queue . retention . subscription * 1000 ,
title : 'subscription and password-related'
}
} ;
}
function messagesProcessed ( workerId , withErrors ) {
const wa = workAssignment . get ( workerId ) ;
const sendConfigurationStatus = getSendConfigurationStatus ( wa . sendConfigurationId ) ;
if ( withErrors ) {
if ( sendConfigurationStatus . retryCount === wa . sendConfigurationRetryCount ) { // This is to avoid multiple increments when more workers simultaneously fail to send messages ot the same send configuration
setSendConfigurationRetryCount ( sendConfigurationStatus , sendConfigurationStatus . retryCount + 1 ) ;
}
} else {
setSendConfigurationRetryCount ( sendConfigurationStatus , 0 ) ;
2018-09-09 22:55:44 +00:00
}
2019-06-29 21:19:56 +00:00
workAssignment . delete ( workerId ) ;
idleWorkers . push ( workerId ) ;
notifier . notify ( 'workerFinished' ) ;
}
2019-06-25 05:18:06 +00:00
2019-06-29 21:19:56 +00:00
async function workersLoop ( ) {
async function getAvailableWorker ( ) {
while ( idleWorkers . length === 0 ) {
await notifier . waitFor ( 'workerFinished' ) ;
2019-06-25 05:18:06 +00:00
}
2019-06-29 21:19:56 +00:00
return idleWorkers . shift ( ) ;
2018-09-09 22:55:44 +00:00
}
2019-06-30 08:47:09 +00:00
function cancelWorker ( workerId ) {
idleWorkers . push ( workerId ) ;
}
2019-06-25 05:18:06 +00:00
function selectNextTask ( ) {
const allocationMap = new Map ( ) ;
const allocation = [ ] ;
2018-09-09 22:55:44 +00:00
2019-06-29 21:19:56 +00:00
function initAllocation ( waType , attrName , queues , workerMsg , getSendConfigurationId , getQueueEmptyEvent ) {
2019-06-25 05:18:06 +00:00
for ( const id of queues . keys ( ) ) {
2019-06-29 21:19:56 +00:00
const sendConfigurationId = getSendConfigurationId ( id ) ;
2019-06-25 05:18:06 +00:00
const key = attrName + ':' + id ;
2018-09-09 22:55:44 +00:00
2019-06-25 05:18:06 +00:00
const queue = queues . get ( id ) ;
2018-09-09 22:55:44 +00:00
2019-06-29 21:19:56 +00:00
const postponed = isSendConfigurationPostponed ( sendConfigurationId ) ;
2019-06-25 05:18:06 +00:00
const task = {
2019-06-29 21:19:56 +00:00
type : waType ,
id ,
2019-06-25 05:18:06 +00:00
existingWorkers : 0 ,
2019-06-29 21:19:56 +00:00
isValid : queue . length > 0 && ! postponed ,
2019-06-25 05:18:06 +00:00
queue ,
2019-06-29 21:19:56 +00:00
workerMsg ,
attrName ,
getQueueEmptyEvent ,
sendConfigurationId
2019-06-25 05:18:06 +00:00
} ;
2018-09-09 22:55:44 +00:00
2019-06-25 05:18:06 +00:00
allocationMap . set ( key , task ) ;
allocation . push ( task ) ;
2019-06-29 21:19:56 +00:00
if ( postponed && queue . length > 0 ) {
queue . splice ( 0 ) ;
notifier . notify ( task . getQueueEmptyEvent ( task ) ) ;
}
2019-06-25 05:18:06 +00:00
}
2018-09-09 22:55:44 +00:00
2019-06-25 05:18:06 +00:00
for ( const wa of workAssignment . values ( ) ) {
2019-06-29 21:19:56 +00:00
if ( wa . type === waType ) {
2019-06-25 05:18:06 +00:00
const key = attrName + ':' + wa [ attrName ] ;
const task = allocationMap . get ( key ) ;
task . existingWorkers += 1 ;
2018-09-09 22:55:44 +00:00
}
2019-06-25 05:18:06 +00:00
}
}
2018-09-09 22:55:44 +00:00
2019-06-29 21:19:56 +00:00
initAllocation (
WorkAssignmentType . QUEUED ,
'sendConfigurationId' ,
sendConfigurationMessageQueue ,
'process-queued-messages' ,
id => id ,
task => ` sendConfigurationMessageQueueEmpty: ${ task . id } `
) ;
initAllocation (
WorkAssignmentType . CAMPAIGN ,
'campaignId' ,
campaignMessageQueue ,
'process-campaign-messages' ,
id => sendConfigurationIdByCampaignId . get ( id ) ,
task => ` campaignMessageQueueEmpty: ${ task . id } `
) ;
2018-09-09 22:55:44 +00:00
2019-06-25 05:18:06 +00:00
let minTask = null ;
let minExistingWorkers ;
for ( const task of allocation ) {
2019-06-29 21:19:56 +00:00
if ( task . isValid && ( minTask === null || minExistingWorkers > task . existingWorkers ) ) {
2019-06-25 05:18:06 +00:00
minTask = task ;
minExistingWorkers = task . existingWorkers ;
2018-09-09 22:55:44 +00:00
}
}
2019-06-25 05:18:06 +00:00
return minTask ;
2018-09-09 22:55:44 +00:00
}
2019-06-25 05:18:06 +00:00
while ( true ) {
2019-06-30 08:47:09 +00:00
const workerId = await getAvailableWorker ( ) ;
2019-06-25 05:18:06 +00:00
const task = selectNextTask ( ) ;
if ( task ) {
2019-06-29 21:19:56 +00:00
const attrName = task . attrName ;
const sendConfigurationId = task . sendConfigurationId ;
const sendConfigurationStatus = getSendConfigurationStatus ( sendConfigurationId ) ;
const sendConfigurationRetryCount = sendConfigurationStatus . retryCount ;
const queue = task . queue ;
const messages = queue . splice ( 0 , workerBatchSize ) ;
workAssignment . set ( workerId , {
type : task . type ,
[ attrName ] : task . id ,
sendConfigurationId ,
sendConfigurationRetryCount ,
messages
} ) ;
if ( queue . length === 0 ) {
notifier . notify ( task . getQueueEmptyEvent ( task ) ) ;
}
sendToWorker ( workerId , task . workerMsg , {
[ attrName ] : task . id ,
messages
} ) ;
2019-06-30 08:47:09 +00:00
2019-06-25 05:18:06 +00:00
} else {
2019-06-30 08:47:09 +00:00
cancelWorker ( workerId ) ;
2019-06-25 05:18:06 +00:00
await notifier . waitFor ( 'workAvailable' ) ;
}
}
2018-09-09 22:55:44 +00:00
}
2018-09-02 12:59:02 +00:00
2018-09-02 18:17:42 +00:00
async function processCampaign ( campaignId ) {
2019-06-25 05:18:06 +00:00
const msgQueue = campaignMessageQueue . get ( campaignId ) ;
2019-06-29 21:19:56 +00:00
async function finish ( clearMsgQueue , newStatus ) {
if ( clearMsgQueue ) {
msgQueue . splice ( 0 ) ;
}
2019-06-25 05:18:06 +00:00
const isCompleted = ( ) => {
if ( msgQueue . length > 0 ) return false ;
let workerRunning = false ;
for ( const wa of workAssignment . values ( ) ) {
2019-06-29 21:19:56 +00:00
if ( wa . type === WorkAssignmentType . CAMPAIGN && wa . campaignId === campaignId ) {
2019-06-25 05:18:06 +00:00
workerRunning = true ;
}
2018-12-23 19:27:29 +00:00
}
2019-06-25 05:18:06 +00:00
return ! workerRunning ;
} ;
2018-12-23 19:27:29 +00:00
2019-06-25 05:18:06 +00:00
while ( ! isCompleted ( ) ) {
await notifier . waitFor ( 'workerFinished' ) ;
2018-12-23 19:27:29 +00:00
}
2019-06-25 05:18:06 +00:00
campaignMessageQueue . delete ( campaignId ) ;
2019-02-07 14:38:32 +00:00
2019-06-25 05:18:06 +00:00
await knex ( 'campaigns' ) . where ( 'id' , campaignId ) . update ( { status : newStatus } ) ;
await activityLog . logEntityActivity ( 'campaign' , CampaignActivityType . STATUS _CHANGE , campaignId , { status : newStatus } ) ;
2018-09-09 22:55:44 +00:00
}
2018-11-17 01:54:23 +00:00
try {
while ( true ) {
const cpg = await knex ( 'campaigns' ) . where ( 'id' , campaignId ) . first ( ) ;
2018-09-02 18:17:42 +00:00
2019-06-25 05:18:06 +00:00
if ( cpg . status === CampaignStatus . PAUSING ) {
2019-06-29 21:19:56 +00:00
return await finish ( true , CampaignStatus . PAUSED ) ;
}
2019-07-09 22:06:56 +00:00
const expirationThreshold = Date . now ( ) - config . queue . retention . campaign * 1000 ;
if ( cpg . start _at && cpg . start _at . valueOf ( ) < expirationThreshold ) {
return await finish ( true , CampaignStatus . FINISHED ) ;
}
2019-06-29 21:19:56 +00:00
sendConfigurationIdByCampaignId . set ( cpg . id , cpg . send _configuration ) ;
if ( isSendConfigurationPostponed ( cpg . send _configuration ) ) {
// postpone campaign if its send configuration is problematic
return await finish ( true , CampaignStatus . SCHEDULED ) ;
2018-11-17 01:54:23 +00:00
}
2018-09-09 22:55:44 +00:00
2018-11-17 01:54:23 +00:00
let qryGen ;
await knex . transaction ( async tx => {
2018-12-23 19:27:29 +00:00
qryGen = await campaigns . getSubscribersQueryGeneratorTx ( tx , campaignId ) ;
2018-11-17 01:54:23 +00:00
} ) ;
if ( qryGen ) {
2019-06-25 05:18:06 +00:00
let messagesInProcessing = [ ... msgQueue ] ;
2018-11-17 01:54:23 +00:00
for ( const wa of workAssignment . values ( ) ) {
2019-06-29 21:19:56 +00:00
if ( wa . type === WorkAssignmentType . CAMPAIGN && wa . campaignId === campaignId ) {
2019-06-25 05:18:06 +00:00
messagesInProcessing = messagesInProcessing . concat ( wa . messages ) ;
2018-11-17 01:54:23 +00:00
}
2018-09-27 19:32:35 +00:00
}
2018-11-17 01:54:23 +00:00
const qry = qryGen ( knex )
2019-06-25 05:18:06 +00:00
. whereNotIn ( 'pending_subscriptions.email' , messagesInProcessing . map ( x => x . email ) )
2018-11-17 01:54:23 +00:00
. select ( [ 'pending_subscriptions.email' , 'campaign_lists.list' ] )
. limit ( retrieveBatchSize ) ;
const subs = await qry ;
2018-09-09 22:55:44 +00:00
2018-11-17 01:54:23 +00:00
if ( subs . length === 0 ) {
2019-06-29 21:19:56 +00:00
return await finish ( false , CampaignStatus . FINISHED ) ;
2018-11-17 01:54:23 +00:00
}
2018-09-09 22:55:44 +00:00
2018-11-17 01:54:23 +00:00
for ( const sub of subs ) {
msgQueue . push ( {
listId : sub . list ,
email : sub . email
} ) ;
}
2018-09-09 22:55:44 +00:00
2019-06-25 05:18:06 +00:00
notifier . notify ( 'workAvailable' ) ;
2018-09-09 22:55:44 +00:00
2019-06-25 05:18:06 +00:00
while ( msgQueue . length > 0 ) {
await notifier . waitFor ( ` campaignMessageQueueEmpty: ${ campaignId } ` ) ;
}
2018-09-09 22:55:44 +00:00
2018-11-17 01:54:23 +00:00
} else {
2019-06-29 21:19:56 +00:00
return await finish ( false , CampaignStatus . FINISHED ) ;
2018-11-17 01:54:23 +00:00
}
2018-09-09 22:55:44 +00:00
}
2018-11-17 01:54:23 +00:00
} catch ( err ) {
2019-06-25 05:18:06 +00:00
log . error ( 'Senders' , ` Sending campaign ${ campaignId } failed with error: ${ err . message } ` ) ;
2018-12-16 21:35:21 +00:00
log . verbose ( err . stack ) ;
2018-09-09 22:55:44 +00:00
}
2018-09-02 18:17:42 +00:00
}
2018-09-09 22:55:44 +00:00
async function scheduleCampaigns ( ) {
if ( campaignSchedulerRunning ) {
return ;
}
campaignSchedulerRunning = true ;
2018-11-20 22:41:10 +00:00
try {
2019-06-30 08:47:09 +00:00
// Finish old campaigns
2019-06-29 21:19:56 +00:00
const nowDate = new Date ( ) ;
const now = nowDate . valueOf ( ) ;
const expirationThreshold = new Date ( now - config . queue . retention . campaign * 1000 ) ;
const expiredCampaigns = await knex ( 'campaigns' )
. whereIn ( 'campaigns.type' , [ CampaignType . REGULAR , CampaignType . RSS _ENTRY ] )
. whereIn ( 'campaigns.status' , [ CampaignStatus . SCHEDULED , CampaignStatus . PAUSED ] )
. where ( 'campaigns.start_at' , '<' , expirationThreshold )
. update ( { status : CampaignStatus . FINISHED } ) ;
2019-06-30 08:47:09 +00:00
// Empty message queues for PAUSING campaigns. A pausing campaign typically waits for campaignMessageQueueEmpty before it can check for PAUSING
// We speed this up by discarding messages in the message queue of the campaign.
const pausingCampaigns = await knex ( 'campaigns' )
. whereIn ( 'campaigns.type' , [ CampaignType . REGULAR , CampaignType . RSS _ENTRY ] )
. where ( 'campaigns.status' , CampaignStatus . PAUSING )
. select ( [ 'id' ] )
. forUpdate ( ) ;
for ( const cpg of pausingCampaigns ) {
const campaignId = cpg . id ;
const queue = campaignMessageQueue . get ( campaignId ) ;
queue . splice ( 0 ) ;
notifier . notify ( ` campaignMessageQueueEmpty: ${ campaignId } ` ) ;
}
2018-11-20 22:41:10 +00:00
while ( true ) {
let campaignId = 0 ;
2019-06-29 21:19:56 +00:00
const postponedSendConfigurationIds = getPostponedSendConfigurationIds ( ) ;
2018-11-20 22:41:10 +00:00
await knex . transaction ( async tx => {
const scheduledCampaign = await tx ( 'campaigns' )
. whereIn ( 'campaigns.type' , [ CampaignType . REGULAR , CampaignType . RSS _ENTRY ] )
2019-06-29 21:19:56 +00:00
. whereNotIn ( 'campaigns.send_configuration' , postponedSendConfigurationIds )
2018-11-20 22:41:10 +00:00
. where ( 'campaigns.status' , CampaignStatus . SCHEDULED )
2019-06-29 21:19:56 +00:00
. where ( 'campaigns.start_at' , '<=' , nowDate )
2018-11-20 22:41:10 +00:00
. select ( [ 'id' ] )
2019-06-29 21:19:56 +00:00
. forUpdate ( )
2018-11-20 22:41:10 +00:00
. first ( ) ;
if ( scheduledCampaign ) {
await tx ( 'campaigns' ) . where ( 'id' , scheduledCampaign . id ) . update ( { status : CampaignStatus . SENDING } ) ;
2019-02-07 14:38:32 +00:00
await activityLog . logEntityActivity ( 'campaign' , CampaignActivityType . STATUS _CHANGE , scheduledCampaign . id , { status : CampaignStatus . SENDING } ) ;
2018-11-20 22:41:10 +00:00
campaignId = scheduledCampaign . id ;
}
} ) ;
2018-09-09 22:55:44 +00:00
2018-11-20 22:41:10 +00:00
if ( campaignId ) {
2019-06-25 05:18:06 +00:00
campaignMessageQueue . set ( campaignId , [ ] ) ;
2018-11-20 22:41:10 +00:00
// noinspection JSIgnoredPromiseFromCall
processCampaign ( campaignId ) ;
2018-09-09 22:55:44 +00:00
2018-11-20 22:41:10 +00:00
} else {
break ;
2018-09-09 22:55:44 +00:00
}
2018-11-20 22:41:10 +00:00
}
} catch ( err ) {
2019-06-25 05:18:06 +00:00
log . error ( 'Senders' , ` Scheduling campaigns failed with error: ${ err . message } ` ) ;
2018-12-16 21:35:21 +00:00
log . verbose ( err . stack ) ;
2018-11-20 22:41:10 +00:00
}
2018-09-09 22:55:44 +00:00
2018-11-20 22:41:10 +00:00
campaignSchedulerRunning = false ;
}
2019-06-25 05:18:06 +00:00
async function processQueuedBySendConfiguration ( sendConfigurationId ) {
const msgQueue = sendConfigurationMessageQueue . get ( sendConfigurationId ) ;
2018-11-20 22:41:10 +00:00
2019-06-25 05:18:06 +00:00
const isCompleted = ( ) => {
if ( msgQueue . length > 0 ) return false ;
let workerRunning = false ;
for ( const wa of workAssignment . values ( ) ) {
2019-06-29 21:19:56 +00:00
if ( wa . type === WorkAssignmentType . QUEUED && wa . sendConfigurationId === sendConfigurationId ) {
2019-06-25 05:18:06 +00:00
workerRunning = true ;
}
}
return ! workerRunning ;
} ;
2018-11-20 22:41:10 +00:00
2019-06-29 21:19:56 +00:00
async function finish ( clearMsgQueue , deleteMsgQueue ) {
if ( clearMsgQueue ) {
msgQueue . splice ( 0 ) ;
}
while ( ! isCompleted ( ) ) {
await notifier . waitFor ( 'workerFinished' ) ;
}
if ( deleteMsgQueue ) {
sendConfigurationMessageQueue . delete ( sendConfigurationId ) ;
}
}
2018-11-20 22:41:10 +00:00
try {
while ( true ) {
2019-06-29 21:19:56 +00:00
if ( isSendConfigurationPostponed ( sendConfigurationId ) ) {
return finish ( true , true ) ;
}
2019-06-25 05:18:06 +00:00
let messagesInProcessing = [ ... msgQueue ] ;
for ( const wa of workAssignment . values ( ) ) {
2019-06-29 21:19:56 +00:00
if ( wa . type === WorkAssignmentType . QUEUED && wa . sendConfigurationId === sendConfigurationId ) {
2019-06-25 05:18:06 +00:00
messagesInProcessing = messagesInProcessing . concat ( wa . messages ) ;
}
}
2019-06-29 21:19:56 +00:00
const messageIdsInProcessing = messagesInProcessing . map ( x => x . queuedMessage . id ) ;
2018-11-20 22:41:10 +00:00
const rows = await knex ( 'queued' )
2019-06-29 21:19:56 +00:00
. orderByRaw ( ` FIELD(type, ${ MessageType . TRIGGERED } , ${ MessageType . TEST } , ${ MessageType . SUBSCRIPTION } ) DESC, id ASC ` ) // This orders messages in the following order MessageType.SUBSCRIPTION, MessageType.TEST and MessageType.TRIGGERED
2019-06-25 05:18:06 +00:00
. where ( 'send_configuration' , sendConfigurationId )
2019-06-29 21:19:56 +00:00
. whereNotIn ( 'id' , messageIdsInProcessing )
2018-11-20 22:41:10 +00:00
. limit ( retrieveBatchSize ) ;
if ( rows . length === 0 ) {
2019-06-25 05:18:06 +00:00
if ( isCompleted ( ) ) {
sendConfigurationMessageQueue . delete ( sendConfigurationId ) ;
return ;
2018-11-20 22:41:10 +00:00
2019-06-25 05:18:06 +00:00
} else {
2019-06-29 21:19:56 +00:00
finish ( false , false ) ;
2019-06-25 05:18:06 +00:00
// At this point, there might be new messages in the queued that could belong to us. Thus we have to try again instead for returning.
continue ;
2018-11-20 22:41:10 +00:00
}
2019-06-25 05:18:06 +00:00
}
2018-11-20 22:41:10 +00:00
2019-06-29 21:19:56 +00:00
const expirationThresholds = getExpirationThresholds ( ) ;
const expirationCounters = { } ;
2019-06-30 08:47:09 +00:00
for ( const type in expirationThresholds ) {
2019-06-29 21:19:56 +00:00
expirationCounters [ type ] = 0 ;
}
2019-06-25 05:18:06 +00:00
for ( const row of rows ) {
2019-06-30 08:47:09 +00:00
const expirationThreshold = expirationThresholds [ row . type ] ;
if ( row . created < expirationThreshold . threshold ) {
expirationCounters [ row . type ] += 1 ;
await knex ( 'queued' ) . where ( 'id' , row . id ) . del ( ) ;
} else {
row . data = JSON . parse ( row . data ) ;
msgQueue . push ( {
queuedMessage : row
} ) ;
2019-06-29 21:19:56 +00:00
}
}
2019-06-30 08:47:09 +00:00
for ( const type in expirationThresholds ) {
2019-06-29 21:19:56 +00:00
const expirationThreshold = expirationThresholds [ type ] ;
if ( expirationCounters [ type ] > 0 ) {
log . warn ( 'Senders' , ` Discarded ${ expirationCounters [ type ] } expired ${ expirationThreshold . title } message(s). ` ) ;
}
2018-11-20 22:41:10 +00:00
}
2019-06-25 05:18:06 +00:00
notifier . notify ( 'workAvailable' ) ;
while ( msgQueue . length > 0 ) {
await notifier . waitFor ( ` sendConfigurationMessageQueueEmpty: ${ sendConfigurationId } ` ) ;
}
}
} catch ( err ) {
log . error ( 'Senders' , ` Sending queued messages for send configuration ${ sendConfigurationId } failed with error: ${ err . message } ` ) ;
log . verbose ( err . stack ) ;
}
}
async function scheduleQueued ( ) {
if ( queuedSchedulerRunning ) {
return ;
}
queuedSchedulerRunning = true ;
2018-11-20 22:41:10 +00:00
2019-06-25 05:18:06 +00:00
try {
2019-06-29 21:19:56 +00:00
const sendConfigurationsIdsInProcessing = [ ... sendConfigurationMessageQueue . keys ( ) ] ;
const postponedSendConfigurationIds = getPostponedSendConfigurationIds ( ) ;
// prune old messages
const expirationThresholds = getExpirationThresholds ( ) ;
2019-06-30 08:47:09 +00:00
for ( const type in expirationThresholds ) {
2019-06-29 21:19:56 +00:00
const expirationThreshold = expirationThresholds [ type ] ;
const expiredCount = await knex ( 'queued' )
. whereNotIn ( 'send_configuration' , sendConfigurationsIdsInProcessing )
. where ( 'type' , type )
. where ( 'created' , '<' , expirationThreshold . threshold )
. del ( ) ;
if ( expiredCount ) {
log . warn ( 'Senders' , ` Discarded ${ expiredCount } expired ${ expirationThreshold . title } message(s). ` ) ;
}
}
2019-06-25 05:18:06 +00:00
2019-06-29 21:19:56 +00:00
const rows = await knex ( 'queued' )
. whereNotIn ( 'send_configuration' , [ ... sendConfigurationsIdsInProcessing , ... postponedSendConfigurationIds ] )
. groupBy ( 'send_configuration' )
. select ( [ 'send_configuration' ] ) ;
2019-06-25 05:18:06 +00:00
2019-06-29 21:19:56 +00:00
for ( const row of rows ) {
const sendConfigurationId = row . send _configuration ;
sendConfigurationMessageQueue . set ( sendConfigurationId , [ ] ) ;
2019-06-25 05:18:06 +00:00
2019-06-29 21:19:56 +00:00
// noinspection JSIgnoredPromiseFromCall
processQueuedBySendConfiguration ( sendConfigurationId ) ;
2018-09-09 22:55:44 +00:00
}
2018-11-20 22:41:10 +00:00
} catch ( err ) {
2019-06-25 05:18:06 +00:00
log . error ( 'Senders' , ` Scheduling queued messages failed with error: ${ err . message } ` ) ;
2018-12-16 21:35:21 +00:00
log . verbose ( err . stack ) ;
2018-09-09 22:55:44 +00:00
}
2018-11-20 22:41:10 +00:00
queuedSchedulerRunning = false ;
2018-09-09 22:55:44 +00:00
}
2018-09-02 12:59:02 +00:00
async function spawnWorker ( workerId ) {
return await new Promise ( ( resolve , reject ) => {
2018-09-02 18:17:42 +00:00
log . verbose ( 'Senders' , ` Spawning worker process ${ workerId } ` ) ;
2018-09-02 12:59:02 +00:00
const senderProcess = fork ( path . join ( _ _dirname , 'sender-worker.js' ) , [ workerId ] , {
cwd : path . join ( _ _dirname , '..' ) ,
2018-12-23 19:27:29 +00:00
env : {
NODE _ENV : process . env . NODE _ENV ,
BUILTIN _ZONE _MTA _PASSWORD : builtinZoneMta . getPassword ( )
}
2018-09-02 12:59:02 +00:00
} ) ;
senderProcess . on ( 'message' , msg => {
if ( msg ) {
if ( msg . type === 'worker-started' ) {
log . info ( 'Senders' , ` Worker process ${ workerId } started ` ) ;
return resolve ( ) ;
2018-09-09 22:55:44 +00:00
} else if ( msg . type === 'messages-processed' ) {
2019-06-29 21:19:56 +00:00
messagesProcessed ( workerId , msg . data . withErrors ) ;
2018-09-02 12:59:02 +00:00
}
2018-09-09 22:55:44 +00:00
2018-09-02 12:59:02 +00:00
}
} ) ;
senderProcess . on ( 'close' , ( code , signal ) => {
log . error ( 'Senders' , ` Worker process ${ workerId } exited with code %s signal %s ` , code , signal ) ;
} ) ;
workerProcesses . set ( workerId , senderProcess ) ;
2018-09-09 22:55:44 +00:00
idleWorkers . push ( workerId ) ;
2018-09-02 12:59:02 +00:00
} ) ;
}
function sendToWorker ( workerId , msgType , data ) {
workerProcesses . get ( workerId ) . send ( {
type : msgType ,
data ,
tid : messageTid
} ) ;
messageTid ++ ;
}
2018-09-09 22:55:44 +00:00
2019-06-29 21:19:56 +00:00
function scheduleCheck ( ) {
2018-09-09 22:55:44 +00:00
// noinspection JSIgnoredPromiseFromCall
scheduleCampaigns ( ) ;
2018-11-20 22:41:10 +00:00
// noinspection JSIgnoredPromiseFromCall
2019-06-25 05:18:06 +00:00
scheduleQueued ( ) ;
2019-06-29 21:19:56 +00:00
}
2018-11-20 22:41:10 +00:00
2019-06-29 21:19:56 +00:00
function periodicCheck ( ) {
// noinspection JSIgnoredPromiseFromCall
scheduleCheck ( ) ;
setTimeout ( periodicCheck , checkPeriod ) ;
2018-09-09 22:55:44 +00:00
}
2019-06-29 21:19:56 +00:00
2018-09-02 12:59:02 +00:00
async function init ( ) {
const spawnWorkerFutures = [ ] ;
let workerId ;
2019-06-25 05:18:06 +00:00
for ( workerId = 0 ; workerId < workersCount ; workerId ++ ) {
2018-09-02 12:59:02 +00:00
spawnWorkerFutures . push ( spawnWorker ( workerId ) ) ;
}
await Promise . all ( spawnWorkerFutures ) ;
process . on ( 'message' , msg => {
if ( msg ) {
const type = msg . type ;
2018-09-09 22:55:44 +00:00
if ( type === 'schedule-check' ) {
// noinspection JSIgnoredPromiseFromCall
2019-06-29 21:19:56 +00:00
scheduleCheck ( ) ;
2018-09-02 12:59:02 +00:00
2018-09-09 22:55:44 +00:00
} else if ( type === 'reload-config' ) {
2019-06-29 21:19:56 +00:00
const sendConfigurationStatus = getSendConfigurationStatus ( msg . data . sendConfigurationId ) ;
if ( sendConfigurationStatus . retryCount > 0 ) {
const sendConfigurationStatus = getSendConfigurationStatus ( msg . data . sendConfigurationId )
setSendConfigurationRetryCount ( sendConfigurationStatus , 0 ) ;
// noinspection JSIgnoredPromiseFromCall
scheduleCheck ( ) ;
}
2018-12-16 21:35:21 +00:00
for ( const workerId of workerProcesses . keys ( ) ) {
2018-09-09 22:55:44 +00:00
sendToWorker ( workerId , 'reload-config' , msg . data ) ;
2018-09-02 12:59:02 +00:00
}
}
}
} ) ;
2018-12-21 18:09:18 +00:00
if ( config . title ) {
process . title = config . title + ': sender/master' ;
}
2018-09-02 12:59:02 +00:00
process . send ( {
2018-09-27 19:32:35 +00:00
type : 'master-sender-started'
2018-09-02 12:59:02 +00:00
} ) ;
2019-06-29 21:19:56 +00:00
periodicCheck ( ) ;
2019-06-25 05:18:06 +00:00
setImmediate ( workersLoop ) ;
2018-09-02 12:59:02 +00:00
}
2018-11-20 22:41:10 +00:00
// noinspection JSIgnoredPromiseFromCall
2018-09-02 12:59:02 +00:00
init ( ) ;