Bugfixes in sending campaigns

This commit is contained in:
Tomas Bures 2018-09-27 21:32:35 +02:00
parent 2d667523a1
commit 1448d9e914
34 changed files with 95 additions and 55 deletions

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const config = require('config'); const config = require('config');
const log = require('npmlog'); const log = require('./lib/log');
const express = require('express'); const express = require('express');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const config = require('config'); const config = require('config');
const log = require('npmlog'); const log = require('./lib/log');
const appBuilder = require('./app-builder'); const appBuilder = require('./app-builder');
const http = require('http'); const http = require('http');
const triggers = require('./services/triggers'); const triggers = require('./services/triggers');
@ -29,8 +29,6 @@ if (config.title) {
process.title = config.title; process.title = config.title;
} }
log.level = config.log.level;
function startHTTPServer(appType, appName, port, callback) { function startHTTPServer(appType, appName, port, callback) {
const app = appBuilder.createApp(appType); const app = appBuilder.createApp(appType);
@ -82,6 +80,15 @@ dbcheck(err => { // Check if database needs upgrading before starting the server
.then(() => shares.regenerateRoleNamesTable()) .then(() => shares.regenerateRoleNamesTable())
.then(() => shares.rebuildPermissions()) .then(() => shares.rebuildPermissions())
/*
.then(() =>
testServer(() => {
senders.spawn(() => {
});
})
);
*/
.then(() => .then(() =>
executor.spawn(() => { executor.spawn(() => {
testServer(() => { testServer(() => {

View file

@ -148,7 +148,7 @@ class CampaignSender {
return; return;
} }
const list = this.listsById.get(list.id); const list = this.listsById.get(listId);
const subscriptionGrouped = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, email); const subscriptionGrouped = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, email);
const flds = this.listsFieldsGrouped.get(listId); const flds = this.listsFieldsGrouped.get(listId);
const campaign = this.campaign; const campaign = this.campaign;
@ -171,7 +171,7 @@ class CampaignSender {
if (!list.listunsubscribe_disabled) { if (!list.listunsubscribe_disabled) {
listUnsubscribe = campaign.unsubscribe_url listUnsubscribe = campaign.unsubscribe_url
? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, campaign.unsubscribe_url) ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, campaign.unsubscribe_url)
: getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscriptionGrouped.subscription.cid); : getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscriptionGrouped.cid);
} }
const mailer = await mailers.getOrCreateMailer(sendConfiguration.id); const mailer = await mailers.getOrCreateMailer(sendConfiguration.id);
@ -257,7 +257,7 @@ class CampaignSender {
await knex('campaign_messages').insert({ await knex('campaign_messages').insert({
campaign: this.campaign.id, campaign: this.campaign.id,
list: listId, list: listId,
subscriptions: subscriptionGrouped.id, subscription: subscriptionGrouped.id,
send_configuration: sendConfiguration.id, send_configuration: sendConfiguration.id,
status, status,
response, response,

View file

@ -4,20 +4,20 @@
This module handles Mailtrain database initialization and upgrades This module handles Mailtrain database initialization and upgrades
*/ */
let config = require('config'); const config = require('config');
let mysql = require('mysql2'); const mysql = require('mysql2');
let log = require('npmlog'); const log = require('./log');
let fs = require('fs'); const fs = require('fs');
let pathlib = require('path'); const pathlib = require('path');
let Handlebars = require('handlebars'); const Handlebars = require('handlebars');
const highestLegacySchemaVersion = 29; const highestLegacySchemaVersion = 29;
let mysqlConfig = { const mysqlConfig = {
multipleStatements: true multipleStatements: true
}; };
Object.keys(config.mysql).forEach(key => mysqlConfig[key] = config.mysql[key]); Object.keys(config.mysql).forEach(key => mysqlConfig[key] = config.mysql[key]);
let db = mysql.createPool(mysqlConfig); const db = mysql.createPool(mysqlConfig);
function listTables(callback) { function listTables(callback) {
db.getConnection((err, connection) => { db.getConnection((err, connection) => {

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const fork = require('child_process').fork; const fork = require('child_process').fork;
const log = require('npmlog'); const log = require('./log');
const path = require('path'); const path = require('path');
const requestCallbacks = {}; const requestCallbacks = {};

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const fork = require('child_process').fork; const fork = require('child_process').fork;
const log = require('npmlog'); const log = require('./log');
const path = require('path'); const path = require('path');
let feedcheckProcess; let feedcheckProcess;

View file

@ -2,7 +2,7 @@
const knex = require('./knex'); const knex = require('./knex');
const fork = require('child_process').fork; const fork = require('child_process').fork;
const log = require('npmlog'); const log = require('./log');
const path = require('path'); const path = require('path');
const {ImportStatus, RunStatus} = require('../shared/imports'); const {ImportStatus, RunStatus} = require('../shared/imports');

8
lib/log.js Normal file
View file

@ -0,0 +1,8 @@
'use strict';
const config = require('config');
const log = require('npmlog');
log.level = config.log.level;
module.exports = log;

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const log = require('npmlog'); const log = require('./log');
const config = require('config'); const config = require('config');
const Handlebars = require('handlebars'); const Handlebars = require('handlebars');

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const config = require('config'); const config = require('config');
const log = require('npmlog'); const log = require('./log');
const _ = require('./translate')._; const _ = require('./translate')._;
const util = require('util'); const util = require('util');

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const log = require('npmlog'); const log = require('./log');
const config = require('config'); const config = require('config');
const fs = require('fs'); const fs = require('fs');

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const log = require('npmlog'); const log = require('./log');
const reports = require('../models/reports'); const reports = require('../models/reports');
const executor = require('./executor'); const executor = require('./executor');
const contextHelpers = require('../lib/context-helpers'); const contextHelpers = require('../lib/context-helpers');

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const fork = require('child_process').fork; const fork = require('child_process').fork;
const log = require('npmlog'); const log = require('./log');
const path = require('path'); const path = require('path');
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const {CampaignStatus} = require('../shared/campaigns'); const {CampaignStatus} = require('../shared/campaigns');
@ -12,10 +12,8 @@ let senderProcess;
function spawn(callback) { function spawn(callback) {
log.verbose('Senders', 'Spawning master sender process'); log.verbose('Senders', 'Spawning master sender process');
knex.transaction(async tx => { knex('campaigns').where('status', CampaignStatus.SENDING).update({status: CampaignStatus.SCHEDULED})
await tx('campaigns').where('status', CampaignStatus.SENDING).update({status: CampaignStatus.SCHEDULED}); .then(() => {
}).then(() => {
senderProcess = fork(path.join(__dirname, '..', 'services', 'sender-master.js'), [], { senderProcess = fork(path.join(__dirname, '..', 'services', 'sender-master.js'), [], {
cwd: path.join(__dirname, '..'), cwd: path.join(__dirname, '..'),
env: {NODE_ENV: process.env.NODE_ENV} env: {NODE_ENV: process.env.NODE_ENV}

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const log = require('npmlog'); const log = require('./log');
const fields = require('../models/fields'); const fields = require('../models/fields');
const settings = require('../models/settings'); const settings = require('../models/settings');
const {getTrustedUrl} = require('./urls'); const {getTrustedUrl} = require('./urls');

View file

@ -6,7 +6,7 @@ const Gettext = require('node-gettext');
const gt = new Gettext(); const gt = new Gettext();
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const log = require('npmlog'); const log = require('./log');
const gettextParser = require('gettext-parser'); const gettextParser = require('gettext-parser');
const fakelang = require('./fakelang'); const fakelang = require('./fakelang');

View file

@ -599,7 +599,7 @@ async function updateMessageResponse(context, message, response, responseId) {
}); });
} }
async function getSubscribersQueryGeneratorTx(tx, campaignId, onlyUnsent, batchSize) { async function getSubscribersQueryGeneratorTx(tx, campaignId, onlyUnsent) {
/* /*
This is supposed to produce queries like this: This is supposed to produce queries like this:

View file

@ -130,7 +130,7 @@ fieldTypes['checkbox-grouped'] = {
cardinality: Cardinality.MULTIPLE, cardinality: Cardinality.MULTIPLE,
getHbsType: field => 'typeCheckboxGrouped', getHbsType: field => 'typeCheckboxGrouped',
render: (field, value) => { render: (field, value) => {
const subItems = value.map(col => field.groupedOptions[col].name); const subItems = (value || []).map(col => field.groupedOptions[col].name);
if (field.settings.groupTemplate) { if (field.settings.groupTemplate) {
return render(field.settings.groupTemplate, { return render(field.settings.groupTemplate, {
@ -149,7 +149,10 @@ fieldTypes['radio-grouped'] = {
enumerated: false, enumerated: false,
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeRadioGrouped', getHbsType: field => 'typeRadioGrouped',
render: (field, value) => field.groupedOptions[value].name render: (field, value) => {
const fld = field.groupedOptions[value];
return fld ? fld.name : '';
}
}; };
fieldTypes['dropdown-grouped'] = { fieldTypes['dropdown-grouped'] = {
@ -159,7 +162,10 @@ fieldTypes['dropdown-grouped'] = {
enumerated: false, enumerated: false,
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeDropdownGrouped', getHbsType: field => 'typeDropdownGrouped',
render: (field, value) => field.groupedOptions[value].name render: (field, value) => {
const fld = field.groupedOptions[value];
return fld ? fld.name : '';
}
}; };
fieldTypes['radio-enum'] = { fieldTypes['radio-enum'] = {
@ -173,7 +179,10 @@ fieldTypes['radio-enum'] = {
enumerated: true, enumerated: true,
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeRadioEnum', getHbsType: field => 'typeRadioEnum',
render: (field, value) => field.groupedOptions[value].name render: (field, value) => {
const fld = field.groupedOptions[value];
return fld ? fld.name : '';
}
}; };
fieldTypes['dropdown-enum'] = { fieldTypes['dropdown-enum'] = {
@ -187,7 +196,10 @@ fieldTypes['dropdown-enum'] = {
enumerated: true, enumerated: true,
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeDropdownEnum', getHbsType: field => 'typeDropdownEnum',
render: (field, value) => field.groupedOptions[value].name render: (field, value) => {
const fld = field.groupedOptions[value];
return fld ? fld.name : '';
}
}; };
fieldTypes.option = { fieldTypes.option = {

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const log = require('npmlog'); const log = require('../lib/log');
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const dtHelpers = require('../lib/dt-helpers'); const dtHelpers = require('../lib/dt-helpers');
const shares = require('./shares'); const shares = require('./shares');

View file

@ -6,7 +6,7 @@ const { enforce } = require('../lib/helpers');
const dtHelpers = require('../lib/dt-helpers'); const dtHelpers = require('../lib/dt-helpers');
const entitySettings = require('../lib/entity-settings'); const entitySettings = require('../lib/entity-settings');
const interoperableErrors = require('../shared/interoperable-errors'); const interoperableErrors = require('../shared/interoperable-errors');
const log = require('npmlog'); const log = require('../lib/log');
const {getGlobalNamespaceId} = require('../shared/namespaces'); const {getGlobalNamespaceId} = require('../shared/namespaces');
const {getAdminId} = require('../shared/users'); const {getAdminId} = require('../shared/users');

View file

@ -7,7 +7,7 @@ const fields = require('../models/fields');
const { SubscriptionStatus, SubscriptionSource } = require('../shared/lists'); const { SubscriptionStatus, SubscriptionSource } = require('../shared/lists');
const subscriptions = require('../models/subscriptions'); const subscriptions = require('../models/subscriptions');
const confirmations = require('../models/confirmations'); const confirmations = require('../models/confirmations');
const log = require('npmlog'); const log = require('../lib/log');
const router = require('../lib/router-async').create(); const router = require('../lib/router-async').create();
const mailHelpers = require('../lib/subscription-mail-helpers'); const mailHelpers = require('../lib/subscription-mail-helpers');
const interoperableErrors = require('../shared/interoperable-errors'); const interoperableErrors = require('../shared/interoperable-errors');

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const log = require('npmlog'); const log = require('../lib/log');
const config = require('config'); const config = require('config');
const router = require('../lib/router-async').create(); const router = require('../lib/router-async').create();
const links = require('../models/links'); const links = require('../models/links');

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const log = require('npmlog'); const log = require('../lib/log');
const config = require('config'); const config = require('config');
const router = require('../lib/router-async').create(); const router = require('../lib/router-async').create();
const confirmations = require('../models/confirmations'); const confirmations = require('../models/confirmations');

View file

@ -7,7 +7,7 @@ const sendConfigurations = require('../models/send-configurations');
const contextHelpers = require('../lib/context-helpers'); const contextHelpers = require('../lib/context-helpers');
const {SubscriptionStatus} = require('../shared/lists'); const {SubscriptionStatus} = require('../shared/lists');
const {MailerType} = require('../shared/send-configurations'); const {MailerType} = require('../shared/send-configurations');
const log = require('npmlog'); const log = require('../lib/log');
const multer = require('multer'); const multer = require('multer');
const uploads = multer(); const uploads = multer();

View file

@ -7,7 +7,7 @@
const reportHelpers = require('../lib/report-helpers'); const reportHelpers = require('../lib/report-helpers');
const fork = require('child_process').fork; const fork = require('child_process').fork;
const path = require('path'); const path = require('path');
const log = require('npmlog'); const log = require('../lib/log');
const fs = require('fs'); const fs = require('fs');
const privilegeHelpers = require('../lib/privilege-helpers'); const privilegeHelpers = require('../lib/privilege-helpers');

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const log = require('npmlog'); const log = require('../lib/log');
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const feedparser = require('feedparser-promised'); const feedparser = require('feedparser-promised');
const { CampaignType, CampaignStatus, CampaignSource } = require('../shared/campaigns'); const { CampaignType, CampaignStatus, CampaignSource } = require('../shared/campaigns');

View file

@ -2,7 +2,7 @@
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const path = require('path'); const path = require('path');
const log = require('npmlog'); const log = require('../lib/log');
const fsExtra = require('fs-extra-promise'); const fsExtra = require('fs-extra-promise');
const {ImportSource, MappingType, ImportStatus, RunStatus} = require('../shared/imports'); const {ImportSource, MappingType, ImportStatus, RunStatus} = require('../shared/imports');
const imports = require('../models/imports'); const imports = require('../models/imports');

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const log = require('npmlog'); const log = require('../lib/log');
const config = require('config'); const config = require('config');
const net = require('net'); const net = require('net');
const campaigns = require('../models/campaigns'); const campaigns = require('../models/campaigns');

View file

@ -2,7 +2,7 @@
const config = require('config'); const config = require('config');
const fork = require('child_process').fork; const fork = require('child_process').fork;
const log = require('npmlog'); const log = require('../lib/log');
const path = require('path'); const path = require('path');
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const {CampaignStatus, CampaignType} = require('../shared/campaigns'); const {CampaignStatus, CampaignType} = require('../shared/campaigns');
@ -24,10 +24,13 @@ const workerBatchSize = 100;
const messageQueue = new Map(); // campaignId -> [{listId, email}] const messageQueue = new Map(); // campaignId -> [{listId, email}]
const messageQueueCont = new Map(); // campaignId -> next batch callback const messageQueueCont = new Map(); // campaignId -> next batch callback
const workAssignment = new Map(); // workerId -> { campaignId, subscribers: [{listId, email}] }
let workerSchedulerCont = null; let workerSchedulerCont = null;
function messagesProcessed(workerId) { function messagesProcessed(workerId) {
workAssignment.delete(workerId);
idleWorkers.push(workerId); idleWorkers.push(workerId);
if (workerSchedulerCont) { if (workerSchedulerCont) {
@ -70,6 +73,7 @@ async function scheduleWorkers() {
if (queue.length > 0) { if (queue.length > 0) {
const subscribers = queue.splice(0, workerBatchSize); const subscribers = queue.splice(0, workerBatchSize);
workAssignment.set(workerId, {campaignId, subscribers});
if (queue.length === 0 && messageQueueCont.has(campaignId)) { if (queue.length === 0 && messageQueueCont.has(campaignId)) {
const scheduleMessages = messageQueueCont.get(campaignId); const scheduleMessages = messageQueueCont.get(campaignId);
@ -113,11 +117,21 @@ async function processCampaign(campaignId) {
let qryGen; let qryGen;
await knex.transaction(async tx => { await knex.transaction(async tx => {
qryGen = await campaigns.getSubscribersQueryGeneratorTx(tx, campaignId, true, retrieveBatchSize); qryGen = await campaigns.getSubscribersQueryGeneratorTx(tx, campaignId, true);
}); });
if (qryGen) { if (qryGen) {
const qry = qryGen(knex).select(['pending_subscriptions.email', 'campaign_lists.list']); let subscribersInProcessing = [...msgQueue];
for (const wa of workAssignment.values()) {
if (wa.campaignId === campaignId) {
subscribersInProcessing = subscribersInProcessing.concat(wa.subscribers);
}
}
const qry = qryGen(knex)
.whereNotIn('pending_subscriptions.email', subscribersInProcessing.map(x => x.email))
.select(['pending_subscriptions.email', 'campaign_lists.list'])
.limit(retrieveBatchSize);
const subs = await qry; const subs = await qry;
if (subs.length === 0) { if (subs.length === 0) {
@ -261,7 +275,7 @@ async function init() {
}); });
process.send({ process.send({
type: 'sender-started' type: 'master-sender-started'
}); });
periodicCampaignsCheck(); periodicCampaignsCheck();

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const config = require('config'); const config = require('config');
const log = require('npmlog'); const log = require('../lib/log');
const mailers = require('../lib/mailers'); const mailers = require('../lib/mailers');
const CampaignSender = require('../lib/campaign-sender'); const CampaignSender = require('../lib/campaign-sender');
@ -25,6 +25,7 @@ async function processMessages(campaignId, subscribers) {
log.verbose('Senders', 'Message sent and status updated for %s:%s', subData.listId, subData.email); log.verbose('Senders', 'Message sent and status updated for %s:%s', subData.listId, subData.email);
} catch (err) { } catch (err) {
log.error('Senders', `Sending message to ${subData.listId}:${subData.email} failed with error: ${err.message}`) log.error('Senders', `Sending message to ${subData.listId}:${subData.email} failed with error: ${err.message}`)
log.verbose(err);
} }
} }

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const log = require('npmlog'); const log = require('../lib/log');
const config = require('config'); const config = require('config');
const crypto = require('crypto'); const crypto = require('crypto');
const humanize = require('humanize'); const humanize = require('humanize');

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const log = require('npmlog'); const log = require('../lib/log');
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const triggers = require('../models/triggers'); const triggers = require('../models/triggers');
const campaigns = require('../models/campaigns'); const campaigns = require('../models/campaigns');

View file

@ -10,7 +10,7 @@
const moment = require('moment-timezone'); const moment = require('moment-timezone');
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const log = require('npmlog'); const log = require('../lib/log');
let lastCheck = false; let lastCheck = false;
const timezone_timeout = 60 * 60 * 1000; const timezone_timeout = 60 * 60 * 1000;

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const { nodeifyFunction, nodeifyPromise } = require('../lib/nodeify'); const { nodeifyFunction, nodeifyPromise } = require('../lib/nodeify');
const log = require('npmlog'); const log = require('../lib/log');
const config = require('config'); const config = require('config');
const {MailerError} = require('../lib/mailers'); const {MailerError} = require('../lib/mailers');
const campaigns = require('../models/campaigns'); const campaigns = require('../models/campaigns');

View file

@ -10,7 +10,7 @@ const handlebarsHelpers = require('../../lib/handlebars-helpers');
const _ = require('../../lib/translate')._; const _ = require('../../lib/translate')._;
const hbs = require('hbs'); const hbs = require('hbs');
const vm = require('vm'); const vm = require('vm');
const log = require('npmlog'); const log = require('../../lib/log');
const fs = require('fs'); const fs = require('fs');
const knex = require('../../lib/knex'); const knex = require('../../lib/knex');
const contextHelpers = require('../../lib/context-helpers'); const contextHelpers = require('../../lib/context-helpers');