feat: Add working hours for natural traffic patterns
Workers check their timezone (from preflight IP geolocation) and current hour's weight probability to determine availability. This creates natural traffic patterns - more workers active during peak hours, fewer during off-peak. Tasks queue up at night and drain during the day. Migrations: - 099: working_hours table with hourly weights by profile - 100: Add timezone column to worker_registry - 101: Store timezone from preflight IP geolocation - 102: check_working_hours() function with probability roll 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -696,6 +696,57 @@ export class TaskWorker {
|
||||
this.isRetryingPreflight = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective max concurrent tasks based on working hours.
|
||||
* Uses the worker's timezone (from preflight IP geolocation) to determine
|
||||
* the current hour's weight, then scales max concurrent tasks accordingly.
|
||||
*
|
||||
* This creates natural traffic patterns - workers run fewer concurrent
|
||||
* tasks during off-peak hours, full capacity during peak hours.
|
||||
*
|
||||
* Example with MAX_CONCURRENT_TASKS = 3:
|
||||
* 3 AM (5% weight): effectiveMax = max(1, round(3 × 0.05)) = 1
|
||||
* 12 PM (75% weight): effectiveMax = max(1, round(3 × 0.75)) = 2
|
||||
* 6 PM (100% weight): effectiveMax = max(1, round(3 × 1.00)) = 3
|
||||
*/
|
||||
private async getEffectiveMaxTasks(): Promise<{ effectiveMax: number; hourWeight: number; reason: string }> {
|
||||
try {
|
||||
const result = await this.pool.query(`
|
||||
SELECT current_hour, hour_weight, worker_timezone
|
||||
FROM check_working_hours($1, 'natural_traffic')
|
||||
`, [this.workerId]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
// Function returned nothing - default to full capacity
|
||||
return {
|
||||
effectiveMax: this.maxConcurrentTasks,
|
||||
hourWeight: 100,
|
||||
reason: 'Working hours check returned no result - using full capacity'
|
||||
};
|
||||
}
|
||||
|
||||
const row = result.rows[0];
|
||||
const hourWeight = row.hour_weight || 100;
|
||||
|
||||
// Scale max concurrent tasks by hour weight, minimum 1
|
||||
const effectiveMax = Math.max(1, Math.round(this.maxConcurrentTasks * hourWeight / 100));
|
||||
|
||||
return {
|
||||
effectiveMax,
|
||||
hourWeight,
|
||||
reason: `Hour ${row.current_hour} (${row.worker_timezone}): ${hourWeight}% → ${effectiveMax}/${this.maxConcurrentTasks} slots`
|
||||
};
|
||||
} catch (err: any) {
|
||||
// On error, default to full capacity (don't block workers due to DB issues)
|
||||
console.warn(`[TaskWorker] Working hours check failed: ${err.message} - using full capacity`);
|
||||
return {
|
||||
effectiveMax: this.maxConcurrentTasks,
|
||||
hourWeight: 100,
|
||||
reason: 'Working hours check error - using full capacity'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy initialization of stealth systems.
|
||||
* Called BEFORE claiming first task (not at worker startup).
|
||||
@@ -1030,6 +1081,25 @@ export class TaskWorker {
|
||||
return; // Return to main loop, will re-check on next iteration
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// WORKING HOURS GATE - Simulate natural traffic patterns
|
||||
// Workers scale their concurrent task limit based on the current
|
||||
// hour's weight in their timezone. This creates natural throughput:
|
||||
// 3 AM (5%): 1 slot → worker runs 1 task at a time
|
||||
// 6 PM (100%): 3 slots → worker runs full capacity
|
||||
// =================================================================
|
||||
const { effectiveMax, hourWeight, reason } = await this.getEffectiveMaxTasks();
|
||||
if (this.activeTasks.size >= effectiveMax) {
|
||||
// Already at working hours capacity - wait before checking again
|
||||
const sleepMs = 10000 + Math.random() * 20000; // 10-30 seconds
|
||||
if (this.activeTasks.size > 0) {
|
||||
// Only log if we're actually throttled (have tasks but can't take more)
|
||||
console.log(`[TaskWorker] ${this.friendlyName} AT CAPACITY - ${reason} (${this.activeTasks.size}/${effectiveMax} active)`);
|
||||
}
|
||||
await this.sleep(sleepMs);
|
||||
return; // Return to main loop, will re-check on next iteration
|
||||
}
|
||||
|
||||
// Pass preflight capabilities to only claim compatible tasks
|
||||
const task = await taskService.claimTask(
|
||||
this.role,
|
||||
|
||||
Reference in New Issue
Block a user