rate limit queue, content
This commit is contained in:
parent
6cdac6ec7c
commit
e90da3b2fb
@ -998,10 +998,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset smart prompting when submitting
|
|
||||||
resetSmartPrompting();
|
resetSmartPrompting();
|
||||||
|
|
||||||
const taskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
const taskId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
||||||
|
console.log(`[FRONTEND] Starting AI request with taskId: ${taskId}`);
|
||||||
|
|
||||||
aiResults.style.display = 'none';
|
aiResults.style.display = 'none';
|
||||||
aiError.style.display = 'none';
|
aiError.style.display = 'none';
|
||||||
@ -1011,7 +1011,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const taskIdDisplay = document.getElementById('current-task-id');
|
const taskIdDisplay = document.getElementById('current-task-id');
|
||||||
if (queueStatus && taskIdDisplay) {
|
if (queueStatus && taskIdDisplay) {
|
||||||
queueStatus.style.display = 'block';
|
queueStatus.style.display = 'block';
|
||||||
taskIdDisplay.textContent = taskId;
|
taskIdDisplay.textContent = taskId.slice(-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
aiSubmitBtn.disabled = true;
|
aiSubmitBtn.disabled = true;
|
||||||
@ -1023,64 +1023,90 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const updateQueueStatus = async () => {
|
const updateQueueStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/ai/queue-status?taskId=${taskId}`);
|
const response = await fetch(`/api/ai/queue-status?taskId=${taskId}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`[FRONTEND] Queue status HTTP error: ${response.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
const queueLength = document.getElementById('queue-length');
|
||||||
const queueLength = document.getElementById('queue-length');
|
const estimatedTime = document.getElementById('estimated-time');
|
||||||
const estimatedTime = document.getElementById('estimated-time');
|
const positionBadge = document.getElementById('queue-position-badge');
|
||||||
const positionBadge = document.getElementById('queue-position-badge');
|
const progressBar = document.getElementById('queue-progress');
|
||||||
const progressBar = document.getElementById('queue-progress');
|
|
||||||
|
|
||||||
if (queueLength) queueLength.textContent = data.queueLength;
|
if (queueLength) {
|
||||||
|
queueLength.textContent = data.queueLength || 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (estimatedTime) {
|
if (estimatedTime) {
|
||||||
if (data.estimatedWaitTime > 0) {
|
if (data.estimatedWaitTime > 0) {
|
||||||
estimatedTime.textContent = formatDuration(data.estimatedWaitTime);
|
estimatedTime.textContent = formatDuration(data.estimatedWaitTime);
|
||||||
} else {
|
} else {
|
||||||
estimatedTime.textContent = 'Verarbeitung läuft...';
|
estimatedTime.textContent = 'Verarbeitung läuft...';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (positionBadge && data.currentPosition) {
|
if (positionBadge) {
|
||||||
|
if (data.currentPosition) {
|
||||||
positionBadge.textContent = data.currentPosition;
|
positionBadge.textContent = data.currentPosition;
|
||||||
|
|
||||||
if (progressBar && data.queueLength > 0) {
|
if (progressBar && data.queueLength > 0) {
|
||||||
const progress = Math.max(0, ((data.queueLength - data.currentPosition + 1) / data.queueLength) * 100);
|
const progress = Math.max(0, ((data.queueLength - data.currentPosition + 1) / data.queueLength) * 100);
|
||||||
progressBar.style.width = `${progress}%`;
|
progressBar.style.width = `${progress}%`;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
if (data.taskStatus === 'processing') {
|
||||||
if (data.isProcessing && !data.currentPosition) {
|
positionBadge.textContent = '⚡';
|
||||||
if (positionBadge) positionBadge.textContent = '⚡';
|
if (progressBar) progressBar.style.width = '100%';
|
||||||
if (progressBar) progressBar.style.width = '100%';
|
console.log(`[FRONTEND] Task ${taskId.slice(-6)} is processing but no position returned`);
|
||||||
if (estimatedTime) estimatedTime.textContent = 'Verarbeitung läuft...';
|
} else if (data.taskStatus === 'completed') {
|
||||||
|
positionBadge.textContent = '✅';
|
||||||
|
if (progressBar) progressBar.style.width = '100%';
|
||||||
|
console.log(`[FRONTEND] Task ${taskId.slice(-6)} completed`);
|
||||||
|
} else if (data.taskStatus === 'failed') {
|
||||||
|
positionBadge.textContent = '❌';
|
||||||
|
if (progressBar) progressBar.style.width = '100%';
|
||||||
|
console.log(`[FRONTEND] Task ${taskId.slice(-6)} failed`);
|
||||||
|
} else {
|
||||||
|
positionBadge.textContent = '?';
|
||||||
|
if (progressBar) progressBar.style.width = '0%';
|
||||||
|
console.log(`[FRONTEND] Task ${taskId.slice(-6)} status unknown:`, data.taskStatus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Queue status update failed:', error);
|
console.error('[FRONTEND] Queue status update failed:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateQueueStatus();
|
const aiRequestPromise = fetch('/api/ai/query', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query,
|
||||||
|
mode: currentMode,
|
||||||
|
taskId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
statusInterval = setInterval(updateQueueStatus, 500);
|
setTimeout(() => {
|
||||||
|
updateQueueStatus();
|
||||||
|
statusInterval = setInterval(updateQueueStatus, 1000); // Poll every 1 second for better responsiveness
|
||||||
|
}, 500);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/ai/query', {
|
const response = await aiRequestPromise;
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
query,
|
|
||||||
mode: currentMode,
|
|
||||||
taskId
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (statusInterval) clearInterval(statusInterval);
|
if (statusInterval) {
|
||||||
|
clearInterval(statusInterval);
|
||||||
|
console.log(`[FRONTEND] AI request completed for ${taskId.slice(-6)}, stopping status polling`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(data.error || `HTTP ${response.status}`);
|
throw new Error(data.error || `HTTP ${response.status}`);
|
||||||
@ -1102,7 +1128,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
aiResults.style.display = 'block';
|
aiResults.style.display = 'block';
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('AI query failed:', error);
|
console.error(`[FRONTEND] AI query failed for ${taskId.slice(-6)}:`, error);
|
||||||
|
|
||||||
if (statusInterval) clearInterval(statusInterval);
|
if (statusInterval) clearInterval(statusInterval);
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -287,16 +287,22 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { query, mode = 'workflow', taskId: clientTaskId } = body;
|
const { query, mode = 'workflow', taskId: clientTaskId } = body;
|
||||||
|
|
||||||
|
// ADD THIS DEBUG LOGGING
|
||||||
|
console.log(`[AI API] Received request - TaskId: ${clientTaskId}, Mode: ${mode}, Query length: ${query?.length || 0}`);
|
||||||
|
|
||||||
if (!query || typeof query !== 'string') {
|
if (!query || typeof query !== 'string') {
|
||||||
|
console.log(`[AI API] Invalid query for task ${clientTaskId}`);
|
||||||
return apiError.badRequest('Query required');
|
return apiError.badRequest('Query required');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['workflow', 'tool'].includes(mode)) {
|
if (!['workflow', 'tool'].includes(mode)) {
|
||||||
|
console.log(`[AI API] Invalid mode for task ${clientTaskId}: ${mode}`);
|
||||||
return apiError.badRequest('Invalid mode. Must be "workflow" or "tool"');
|
return apiError.badRequest('Invalid mode. Must be "workflow" or "tool"');
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizedQuery = sanitizeInput(query);
|
const sanitizedQuery = sanitizeInput(query);
|
||||||
if (sanitizedQuery.includes('[FILTERED]')) {
|
if (sanitizedQuery.includes('[FILTERED]')) {
|
||||||
|
console.log(`[AI API] Filtered input detected for task ${clientTaskId}`);
|
||||||
return apiError.badRequest('Invalid input detected');
|
return apiError.badRequest('Invalid input detected');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,6 +314,9 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
const taskId = clientTaskId || `ai_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
const taskId = clientTaskId || `ai_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
||||||
|
|
||||||
|
console.log(`[AI API] About to enqueue task ${taskId}`);
|
||||||
|
|
||||||
|
|
||||||
const aiResponse = await enqueueApiCall(() =>
|
const aiResponse = await enqueueApiCall(() =>
|
||||||
fetch(process.env.AI_API_ENDPOINT + '/v1/chat/completions', {
|
fetch(process.env.AI_API_ENDPOINT + '/v1/chat/completions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
31
src/pages/api/ai/queue-debug.ts
Normal file
31
src/pages/api/ai/queue-debug.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// src/pages/api/ai/queue-debug.ts - Enhanced debug endpoint
|
||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { getQueueDebugState, getAllTaskStatuses } from '../../../utils/rateLimitedQueue.js';
|
||||||
|
import { apiResponse, apiServerError } from '../../../utils/api.js';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
// Only allow in development or with special header
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
const debugHeader = request.headers.get('X-Debug-Queue');
|
||||||
|
|
||||||
|
if (!isDev && debugHeader !== 'true') {
|
||||||
|
return apiServerError.internal('Debug endpoint not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const debugState = getQueueDebugState();
|
||||||
|
const allTaskStatuses = getAllTaskStatuses();
|
||||||
|
|
||||||
|
return apiResponse.success({
|
||||||
|
...debugState,
|
||||||
|
allTaskStatuses,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
message: 'Enhanced queue debug state snapshot'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Queue debug error:', error);
|
||||||
|
return apiServerError.internal('Failed to get queue debug state');
|
||||||
|
}
|
||||||
|
};
|
@ -12,51 +12,67 @@ interface QueuedTask {
|
|||||||
id: string;
|
id: string;
|
||||||
task: Task;
|
task: Task;
|
||||||
addedAt: number;
|
addedAt: number;
|
||||||
|
status: 'queued' | 'processing' | 'completed' | 'failed';
|
||||||
|
startedAt?: number;
|
||||||
|
completedAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueueStatus {
|
export interface QueueStatus {
|
||||||
queueLength: number;
|
queueLength: number;
|
||||||
isProcessing: boolean;
|
isProcessing: boolean;
|
||||||
estimatedWaitTime: number; // in milliseconds
|
estimatedWaitTime: number;
|
||||||
currentPosition?: number;
|
currentPosition?: number;
|
||||||
|
taskStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RateLimitedQueue {
|
class RateLimitedQueue {
|
||||||
private queue: QueuedTask[] = [];
|
private tasks: QueuedTask[] = [];
|
||||||
private processing = false;
|
private isProcessing = false;
|
||||||
private delayMs = RATE_LIMIT_DELAY_MS;
|
private delayMs = RATE_LIMIT_DELAY_MS;
|
||||||
private lastProcessedAt = 0;
|
private lastProcessedAt = 0;
|
||||||
|
private currentlyProcessingTaskId: string | null = null;
|
||||||
|
|
||||||
add<T>(task: Task<T>, taskId?: string): Promise<T> {
|
add<T>(task: Task<T>, taskId?: string): Promise<T> {
|
||||||
const id = taskId || this.generateTaskId();
|
const id = taskId || this.generateTaskId();
|
||||||
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
this.queue.push({
|
const queuedTask: QueuedTask = {
|
||||||
id,
|
id,
|
||||||
task: async () => {
|
task: async () => {
|
||||||
try {
|
try {
|
||||||
const result = await task();
|
const result = await task();
|
||||||
resolve(result);
|
resolve(result);
|
||||||
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addedAt: Date.now()
|
addedAt: Date.now(),
|
||||||
});
|
status: 'queued'
|
||||||
this.process();
|
};
|
||||||
|
|
||||||
|
this.tasks.push(queuedTask);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.processQueue();
|
||||||
|
}, 100);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus(taskId?: string): QueueStatus {
|
getStatus(taskId?: string): QueueStatus {
|
||||||
const queueLength = this.queue.length;
|
const queuedTasks = this.tasks.filter(t => t.status === 'queued');
|
||||||
|
const processingTasks = this.tasks.filter(t => t.status === 'processing');
|
||||||
|
const queueLength = queuedTasks.length + processingTasks.length;
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
let estimatedWaitTime = 0;
|
let estimatedWaitTime = 0;
|
||||||
if (queueLength > 0) {
|
if (queueLength > 0) {
|
||||||
if (this.processing) {
|
if (this.isProcessing && this.lastProcessedAt > 0) {
|
||||||
const timeSinceLastRequest = now - this.lastProcessedAt;
|
const timeSinceLastRequest = now - this.lastProcessedAt;
|
||||||
const remainingDelay = Math.max(0, this.delayMs - timeSinceLastRequest);
|
const remainingDelay = Math.max(0, this.delayMs * 2 - timeSinceLastRequest);
|
||||||
estimatedWaitTime = remainingDelay + (queueLength - 1) * this.delayMs;
|
estimatedWaitTime = remainingDelay + queuedTasks.length * this.delayMs;
|
||||||
} else {
|
} else {
|
||||||
estimatedWaitTime = queueLength * this.delayMs;
|
estimatedWaitTime = queueLength * this.delayMs;
|
||||||
}
|
}
|
||||||
@ -64,14 +80,41 @@ class RateLimitedQueue {
|
|||||||
|
|
||||||
const status: QueueStatus = {
|
const status: QueueStatus = {
|
||||||
queueLength,
|
queueLength,
|
||||||
isProcessing: this.processing,
|
isProcessing: this.isProcessing,
|
||||||
estimatedWaitTime
|
estimatedWaitTime
|
||||||
};
|
};
|
||||||
|
|
||||||
if (taskId) {
|
if (taskId) {
|
||||||
const position = this.queue.findIndex(item => item.id === taskId);
|
const task = this.tasks.find(t => t.id === taskId);
|
||||||
if (position >= 0) {
|
|
||||||
status.currentPosition = position + 1;
|
if (task) {
|
||||||
|
status.taskStatus = task.status;
|
||||||
|
|
||||||
|
if (task.status === 'processing') {
|
||||||
|
status.currentPosition = 1;
|
||||||
|
} else if (task.status === 'queued') {
|
||||||
|
const queuedTasksInOrder = this.tasks
|
||||||
|
.filter(t => t.status === 'queued')
|
||||||
|
.sort((a, b) => a.addedAt - b.addedAt);
|
||||||
|
|
||||||
|
const positionInQueue = queuedTasksInOrder.findIndex(t => t.id === taskId);
|
||||||
|
|
||||||
|
if (positionInQueue >= 0) {
|
||||||
|
const processingOffset = processingTasks.length > 0 ? 1 : 0;
|
||||||
|
status.currentPosition = processingOffset + positionInQueue + 1;
|
||||||
|
}
|
||||||
|
} else if (task.status === 'completed' || task.status === 'failed') {
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const taskTimestamp = taskId.match(/ai_(\d+)_/)?.[1];
|
||||||
|
if (taskTimestamp) {
|
||||||
|
const taskAge = now - parseInt(taskTimestamp);
|
||||||
|
if (taskAge < 30000) {
|
||||||
|
status.taskStatus = 'starting';
|
||||||
|
} else {
|
||||||
|
status.taskStatus = 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,23 +130,60 @@ class RateLimitedQueue {
|
|||||||
return this.delayMs;
|
return this.delayMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async process(): Promise<void> {
|
private async processQueue(): Promise<void> {
|
||||||
if (this.processing) return;
|
if (this.isProcessing) {
|
||||||
this.processing = true;
|
return;
|
||||||
|
|
||||||
while (this.queue.length > 0) {
|
|
||||||
const next = this.queue.shift();
|
|
||||||
if (!next) continue;
|
|
||||||
|
|
||||||
this.lastProcessedAt = Date.now();
|
|
||||||
await next.task();
|
|
||||||
|
|
||||||
if (this.queue.length > 0) {
|
|
||||||
await new Promise((r) => setTimeout(r, this.delayMs));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processing = false;
|
this.isProcessing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const nextTask = this.tasks
|
||||||
|
.filter(t => t.status === 'queued')
|
||||||
|
.sort((a, b) => a.addedAt - b.addedAt)[0];
|
||||||
|
|
||||||
|
if (!nextTask) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTask.status = 'processing';
|
||||||
|
nextTask.startedAt = Date.now();
|
||||||
|
this.currentlyProcessingTaskId = nextTask.id;
|
||||||
|
this.lastProcessedAt = Date.now();
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
await nextTask.task();
|
||||||
|
nextTask.status = 'completed';
|
||||||
|
nextTask.completedAt = Date.now();
|
||||||
|
console.log(`[QUEUE] Task ${nextTask.id} completed`);
|
||||||
|
} catch (error) {
|
||||||
|
nextTask.status = 'failed';
|
||||||
|
nextTask.completedAt = Date.now();
|
||||||
|
console.error(`[QUEUE] Task ${nextTask.id} failed:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentlyProcessingTaskId = null;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const index = this.tasks.findIndex(t => t.id === nextTask.id);
|
||||||
|
if (index >= 0) {
|
||||||
|
console.log(`[QUEUE] Removing completed task ${nextTask.id}`);
|
||||||
|
this.tasks.splice(index, 1);
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
const hasMoreQueued = this.tasks.some(t => t.status === 'queued');
|
||||||
|
if (hasMoreQueued) {
|
||||||
|
console.log(`[QUEUE] Waiting ${this.delayMs}ms before next task`);
|
||||||
|
await new Promise((r) => setTimeout(r, this.delayMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isProcessing = false;
|
||||||
|
console.log(`[QUEUE] Queue processing finished`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateTaskId(): string {
|
private generateTaskId(): string {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user