Repository navigation
Expand file tree
/
Copy pathllm-queue.js
More file actions
162 lines (147 loc) · 5.67 KB
/
Copy pathllm-queue.js
File metadata and controls
162 lines (147 loc) · 5.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
* @module llm-queue
* Priority queue for Ollama inference requests.
* Prevents local or multi-node endpoints from being overwhelmed by batch tasks while prioritizing real-time agents.
*/
import { envOr } from './config.js';
export const PRIORITY = {
CRITICAL: 0, // Real-time UI / active IDE session
HIGH: 1, // Interactive agent
MEDIUM: 2, // Background agent processing
LOW: 3 // Bulk embeddings / offline tasks
};
export class AsyncLock {
constructor() {
this._chains = new Map();
}
async acquire(key, fn) {
const prev = this._chains.get(key) || Promise.resolve();
let release;
const next = new Promise((resolve) => {
release = resolve;
});
const current = prev.then(() => next);
this._chains.set(key, current);
await prev;
try {
return await fn();
} finally {
release();
if (this._chains.get(key) === current) {
this._chains.delete(key);
}
}
}
}
export const DEFAULT_FLEET_OLLAMA_NODES = Object.freeze([
'http://127.0.0.1:11434'
]);
export class PriorityQueue {
constructor(concurrency = 3) {
this.concurrency = Number.isInteger(concurrency) && concurrency > 0 ? concurrency : 3;
this.active = 0;
this.queue = [];
this.endpoints = this.initEndpoints();
this.rrIndex = 0;
this._lock = new AsyncLock();
}
initEndpoints() {
const confUrl = envOr('OLLAMA_URL');
const nodes = envOr('OLLAMA_NODES');
if (nodes) {
const parsed = String(nodes)
.split(',')
.map((n) => n.trim())
.filter(Boolean);
if (parsed.length > 0) return parsed;
}
if (confUrl) {
const trimmedUrl = String(confUrl).trim();
if (trimmedUrl) {
return [trimmedUrl, ...DEFAULT_FLEET_OLLAMA_NODES.filter((n) => n !== trimmedUrl)];
}
}
return [...DEFAULT_FLEET_OLLAMA_NODES];
}
getNextEndpoint() {
const ep = this.endpoints[this.rrIndex];
this.rrIndex = (this.rrIndex + 1) % this.endpoints.length;
return ep;
}
/**
* Enqueue a task to run against an Ollama endpoint.
* @param {function(string): Promise<unknown>} taskFn - The async function to run, receives endpoint URL.
* @param {number} priority - Lower number = higher priority.
*/
enqueue(taskFn, priority = PRIORITY.MEDIUM) {
return new Promise((resolve, reject) => {
this._lock.acquire('pq', () => {
this.queue.push({ taskFn, priority, resolve, reject });
// Re-sort queue so lowest priority number is first
this.queue.sort((a, b) => a.priority - b.priority);
}).then(() => {
void this.process();
}).catch(reject);
});
}
async process() {
const item = await this._lock.acquire('pq', () => {
if (this.active >= this.concurrency || this.queue.length === 0) return null;
this.active++;
return this.queue.shift();
});
if (!item) return;
let outcome = null;
try {
if (typeof item.taskFn !== 'function') {
outcome = { ok: false, error: new TypeError('[OllamaQueue] taskFn must be a function') };
} else if (!Array.isArray(this.endpoints) || this.endpoints.length === 0) {
outcome = { ok: false, error: new Error('[OllamaQueue] No Ollama endpoints configured') };
} else {
let attempts = 0;
const maxAttempts = this.endpoints.length;
let lastErr = null;
while (attempts < maxAttempts && !outcome) {
const endpoint = await this._lock.acquire('pq', () => this.getNextEndpoint());
try {
const result = await item.taskFn(endpoint);
outcome = { ok: true, value: result };
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err);
console.warn(`[OllamaQueue] Endpoint ${endpoint} failed: ${errMsg}. Retrying...`);
lastErr = err;
attempts++;
}
}
if (!outcome) {
const lastMsg = lastErr instanceof Error ? lastErr.message : String(lastErr);
outcome = {
ok: false,
error: new Error(`[OllamaQueue] All fleet nodes failed. Last error: ${lastMsg}`)
};
}
}
} catch (unexpectedErr) {
outcome = {
ok: false,
error: unexpectedErr instanceof Error ? unexpectedErr : new Error(String(unexpectedErr))
};
} finally {
await this._lock.acquire('pq', () => {
this.active = Math.max(0, this.active - 1);
});
// Automatically process the next queued task before resuming caller continuations
void this.process();
if (outcome?.ok) {
item.resolve(outcome.value);
} else {
item.reject(outcome?.error ?? new Error('[OllamaQueue] Queue task failed'));
}
}
}
}
// Default concurrency allows 3 parallel requests across configured endpoints
export const ollamaQueue = new PriorityQueue(
process.env.OLLAMA_MAX_CONCURRENCY ? parseInt(process.env.OLLAMA_MAX_CONCURRENCY, 10) : 3
);
export const globalQueue = ollamaQueue;