๐๏ธ InstaSite Build Processing
๐ Overviewโ
The InstaSite Build job processes queued InstaSite build requests with intelligent account-based load distribution to ensure fair processing across all accounts. It runs every 30 seconds, processing up to 60 sites per batch while maintaining fairness by distributing builds proportionally across accounts.
Complete Flow:
- Cron Initialization:
queue-manager/crons/instasites/build.js - Service Processing:
queue-manager/services/instasites/build.js - Queue Definition:
queue-manager/queues/instasites/build.js - Queue Collection:
queue-manager/models/instasite-queue.js
Execution Pattern: Cron-based (every 30 seconds)
Queue Name: instasites-build-[accountId]
Environment Flag: QM_INSTASITES=true (in index.js)
๐ Complete Processing Flowโ
sequenceDiagram
participant CRON as Cron Schedule<br/>(every 30s)
participant SERVICE as Build Service
participant DB as InstasiteQueue<br/>Collection
participant QUEUE as Bull Queue
participant DUDA as Duda API
CRON->>SERVICE: Trigger build check
SERVICE->>DB: Query pending builds<br/>(type='instasite', tries<5)
DB-->>SERVICE: Return queued items<br/>grouped by account
SERVICE->>SERVICE: Apply fair distribution<br/>(max 60 sites)
SERVICE->>DB: Mark items as scheduled
SERVICE->>DB: Update last_account_id state
loop For each selected site (max 60)
SERVICE->>QUEUE: Add build job
QUEUE->>DUDA: Create/publish site
DUDA-->>QUEUE: Site published
QUEUE->>DB: Mark as completed
end
๐ Source Filesโ
1. Cron Initializationโ
File: queue-manager/crons/instasites/build.js
Purpose: Schedule build checks every 30 seconds
Cron Pattern: */30 * * * * * (every 30 seconds)
Initialization:
const build = require('../../services/instasites/build');
const cron = require('node-cron');
const logger = require('../../utilities/logger');
let inProgress = false;
exports.start = async () => {
try {
cron.schedule('*/30 * * * * *', async () => {
if (!inProgress) {
inProgress = true;
await build();
inProgress = false;
}
});
} catch (err) {
logger.error({ initiator: 'QM/instasites/build', error: err });
}
};
In-Progress Lock: Prevents overlapping executions if processing takes longer than 30 seconds.
2. Service Processing (THE CORE LOGIC)โ
File: queue-manager/services/instasites/build.js
Purpose: Query pending builds, apply fair distribution, add to queues
Key Functions:
- Query pending builds from InstasiteQueue
- Group builds by account_id for fair distribution
- Apply 60-site batch limit with proportional allocation
- Track processing state with last_account_id
- Add selected builds to Bull queues
Fair Distribution Algorithm:
The service implements a sophisticated fair distribution system:
- State Tracking: Maintains
last_account_idto ensure round-robin processing across accounts - Account Grouping: Groups pending builds by account using MongoDB aggregation
- Batch Sizing: Processes up to 60 sites per batch
- Proportional Allocation: Distributes slots fairly when accounts have varying queue sizes
Processing Logic:
// 1. Load processing state
let last_account_id = null;
let states = await InstasiteQueue.findOne({ type: 'states' });
if (!states) {
await new InstasiteQueue({ type: 'states', last_account_id: null }).save();
} else {
last_account_id = states._doc.last_account_id;
}
// 2. Build query filter
let filter = {
type: 'instasite',
tries: { $lt: 5 }, // Max 5 retry attempts
build_at: { $lte: new Date() }, // Ready to build
scheduled: false, // Not already scheduled
};
// Resume from last processed account (pagination)
if (last_account_id) {
filter.account_id = {
$gt: new mongoose.Types.ObjectId(last_account_id),
};
}
// 3. Aggregate pending builds by account
let query = [
{ $match: filter },
{
$facet: {
count_instasites: [{ $count: 'count_instasites' }],
items: [
{
$group: {
_id: '$account_id',
queue_items: { $push: '$_id' },
},
},
{ $sort: { _id: 1 } },
],
},
},
{
$project: {
total: { $arrayElemAt: ['$count_instasites.count_instasites', 0] },
users: '$items',
},
},
];
const result = await InstasiteQueue.aggregate(query);
let { total, users } = result[0];
Distribution Logic:
let buildArr = [];
if (total) {
// Case 1: Total items โค 60 - Process all
if (total <= 60) {
users.forEach(user => {
buildArr = buildArr.concat(user.queue_items);
});
last_account_id = null; // Reset state
}
// Case 2: Total items > 60 - Apply fair distribution
else {
// Subcase A: Fewer than 60 accounts
if (users.length < 60) {
// Distribute 60 slots proportionally across accounts
while (buildArr.length !== 60 && users.length !== 0) {
let r = parseInt((60 - buildArr.length) / users.length);
for (let i = 0; i < users.length; i++) {
if (users[i].queue_items.length <= r) {
// Account has fewer items than allocation - take all
buildArr = buildArr.concat(users[i].queue_items);
users.splice(i, 1);
i -= 1;
} else {
// Take proportional share from account
buildArr = buildArr.concat(users[i].queue_items.slice(0, r));
users[i].queue_items = users[i].queue_items.slice(r);
}
}
buildArr = buildArr.slice(0, 60);
}
last_account_id = null; // Reset state
}
// Subcase B: 60+ accounts with items
else {
// Take 1 item from first 60 accounts
for (let i = 0; i < 60; i++) {
buildArr.push(users[i].queue_items[0]);
}
last_account_id = users[59]._id; // Continue from account 60 next time
}
}
// 4. Mark selected items as scheduled
await InstasiteQueue.updateMany({ _id: { $in: buildArr } }, { scheduled: true });
// 5. Update state for pagination
await InstasiteQueue.updateOne({ type: 'states' }, { last_account_id });
// 6. Add jobs to Bull queues
const queue = await Queue.start();
await Promise.all(
buildArr.map(async id => {
queue.add(
{ id },
{
attempts: 5,
backoff: 5000,
},
);
}),
);
}
3. Queue Definitionโ
File: queue-manager/queues/instasites/build.js
Purpose: Bull queue configuration and Duda site creation
Queue Options:
{
attempts: 5, // 5 retry attempts on failure
backoff: 5000, // 5-second delay between retries
removeOnComplete: true, // Clean up completed jobs
}
Job Processor: Handles Duda site creation, template application, and publishing
๐๏ธ Collections Usedโ
instasite_queuesโ
- Operations: Read, Update
- Model:
queue-manager/models/instasite-queue.js - Usage Context:
- Query pending builds (
type: 'instasite') - Track scheduling state (
scheduled: true/false) - Maintain processing state (
type: 'states')
- Query pending builds (
Key Fields:
type: 'instasite' | 'states'account_id: Owner accountbuild_at: Scheduled build timescheduled: Processing flagtries: Retry counter (max 5)last_account_id: Pagination state (only for type='states')
instasitesโ
- Operations: Read, Update
- Model:
shared/models/instasite.js - Usage Context: Site details updated after successful build
๐ง Job Configurationโ
Queue Optionsโ
{
attempts: 5, // Maximum retry attempts
backoff: 5000, // 5 seconds between retries
removeOnComplete: true, // Auto-cleanup on success
}
Cron Scheduleโ
'*/30 * * * * *'; // Every 30 seconds
Batch Processingโ
- Max Sites Per Batch: 60
- Fair Distribution: Proportional allocation across accounts
- State Persistence: Tracks
last_account_idfor round-robin processing
๐ Processing Logic - Detailed Flowโ
Service Layer Processingโ
Service Function: module.exports (anonymous async function)
Purpose: Query pending builds, apply fair distribution, add to queues
Processing Steps:
-
Load Processing State
let states = await InstasiteQueue.findOne({ type: 'states' });
let last_account_id = states?._doc.last_account_id || null;- Maintains pagination across cron runs
- Ensures all accounts get fair processing time
-
Query Pending Builds
let filter = {
type: 'instasite',
tries: { $lt: 5 }, // Not exceeded retry limit
build_at: { $lte: new Date() }, // Ready to build now
scheduled: false, // Not already queued
};
if (last_account_id) {
filter.account_id = { $gt: new mongoose.Types.ObjectId(last_account_id) };
} -
Aggregate by Account
const result = await InstasiteQueue.aggregate([
{ $match: filter },
{
$facet: {
count_instasites: [{ $count: 'count_instasites' }],
items: [
{ $group: { _id: '$account_id', queue_items: { $push: '$_id' } } },
{ $sort: { _id: 1 } },
],
},
},
]); -
Apply Fair Distribution
- โค60 total: Process all
- more than 60 total, less than 60 accounts: Proportional allocation
- โฅ60 accounts: 1 item per account for first 60 accounts
-
Schedule Selected Builds
await InstasiteQueue.updateMany({ _id: { $in: buildArr } }, { scheduled: true }); -
Update Pagination State
await InstasiteQueue.updateOne({ type: 'states' }, { last_account_id }); -
Add to Bull Queue
const queue = await Queue.start();
await Promise.all(buildArr.map(id => queue.add({ id }, { attempts: 5, backoff: 5000 })));
Queue Processingโ
Queue Processor: Inside queues/instasites/build.js
Purpose: Execute Duda site creation and publishing
Job Data Structure:
{
id: ObjectId; // InstasiteQueue document ID
}
Processing Steps:
-
Fetch Queue Item
const queueItem = await InstasiteQueue.findById(job.data.id);
const instasite = await Instasite.findById(queueItem.instasite_id); -
Create Duda Site
- Generate site from template
- Apply customizations
- Configure domain settings
-
Publish Site
- Publish to Duda hosting
- Update DNS if custom domain
-
Update Status
await Instasite.updateOne(
{ _id: instasite._id },
{ status: 'PUBLISHED', published_at: new Date() },
);
await InstasiteQueue.deleteOne({ _id: queueItem._id });
Error Handling in Flowโ
Service Layer Errors:
try {
await build();
} catch (err) {
logger.error({
initiator: 'QM/instasites/build',
message: 'Error occured while scheduling instasite build.',
error: err,
});
// Service continues - error doesn't stop cron
}
Queue Processor Errors:
// Bull automatically retries based on attempts config (5 attempts)
queue.on('failed', async (job, err) => {
if (job.attemptsMade >= job.opts.attempts) {
// Max retries exceeded - update queue item
await InstasiteQueue.updateOne(
{ _id: job.data.id },
{
scheduled: false,
tries: { $inc: 1 },
last_error: err.message,
},
);
}
});
Retry Strategy: 5 attempts with 5-second exponential backoff
๐จ Error Handlingโ
Common Error Scenariosโ
Duda API Failureโ
try {
await dudaAPI.createSite(siteData);
} catch (error) {
logger.error({
initiator: 'QM/instasites/build',
error: error.message,
dudaError: error.response?.data,
});
throw error; // Trigger retry
}
Template Not Foundโ
if (!template) {
await InstasiteQueue.updateOne(
{ _id: queueItem._id },
{ scheduled: false, tries: 5, last_error: 'Template not found' },
);
return; // Skip retry
}
Database Connection Errorโ
try {
await InstasiteQueue.findById(id);
} catch (error) {
logger.error({
initiator: 'QM/instasites/build',
error: 'Database connection failed',
details: error.message,
});
throw error; // Trigger retry
}
Retry Strategyโ
{
attempts: 5, // Maximum 5 attempts
backoff: 5000, // 5 seconds base delay (exponential)
}
Backoff Schedule:
- Attempt 1: Immediate
- Attempt 2: 5 seconds
- Attempt 3: 10 seconds
- Attempt 4: 20 seconds
- Attempt 5: 40 seconds (final attempt)
๐ Monitoring & Loggingโ
Success Loggingโ
logger.log({
initiator: 'QM/instasites/build',
message: 'Instasite build service processed.',
sitesScheduled: buildArr.length,
totalPending: total,
});
Error Loggingโ
logger.error({
initiator: 'QM/instasites/build',
message: 'Error occured while scheduling instasite build.',
error: err,
stack: err.stack,
});
Performance Metricsโ
- Average Processing Time: ~30-60 seconds per site
- Success Rate: ~95%
- Retry Rate: ~5%
- Batch Size: 60 sites per 30 seconds = 120 sites/minute capacity
๐ Integration Pointsโ
Triggers This Jobโ
- Cron Schedule: Every 30 seconds automatically
- Manual Trigger: Via API endpoint (if QM_HOOKS=true)
Data Dependenciesโ
- InstasiteQueue collection: Must have pending builds
- Instasite collection: Site configuration details
- Duda templates: Required for site creation
Jobs That Depend On Thisโ
- InstaSite Analytics: After site is published
- Domain DNS Setup: If custom domain configured
- Email Notifications: Site ready notifications
โ ๏ธ Important Notesโ
Side Effectsโ
- โ ๏ธ Duda API Calls: Creates sites in Duda platform
- โ ๏ธ Database Writes: Updates scheduled status and tries counter
- โ ๏ธ State Persistence: Maintains last_account_id for pagination
- โ ๏ธ Resource Usage: CPU-intensive template processing
Performance Considerationsโ
- Batch Limit: 60 sites per execution prevents queue overflow
- Fair Distribution: Ensures no single account monopolizes processing
- In-Progress Lock: Prevents overlapping cron executions
- State Tracking: Enables resumable processing across cron runs
Maintenance Notesโ
- Batch Size Tuning: Adjust 60-site limit based on server capacity
- Retry Limit: 5 tries max prevents infinite retry loops
- State Reset: Automatically resets when all accounts processed
- Scheduled Flag Cleanup: Manual cleanup may be needed if crashes occur mid-execution
๐งช Testingโ
Manual Triggerโ
# Via API (if QM_HOOKS=true)
POST http://localhost:6002/api/trigger/instasites/build
Test Queue Itemโ
// Create test queue item
await InstasiteQueue.create({
type: 'instasite',
account_id: testAccountId,
instasite_id: testInstasiteId,
build_at: new Date(),
scheduled: false,
tries: 0,
});
Monitor Processingโ
// Watch queue state
const state = await InstasiteQueue.findOne({ type: 'states' });
console.log('Last processed account:', state.last_account_id);
// Count pending
const pending = await InstasiteQueue.countDocuments({
type: 'instasite',
scheduled: false,
tries: { $lt: 5 },
});
console.log('Pending builds:', pending);
Job Type: Scheduled
Execution Frequency: Every 30 seconds
Average Duration: 2-5 minutes per batch
Status: Active