threadforge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/bin/forge.js +1050 -0
- package/bin/host-commands.js +344 -0
- package/bin/platform-commands.js +570 -0
- package/package.json +71 -0
- package/shared/auth.js +475 -0
- package/src/core/DirectMessageBus.js +364 -0
- package/src/core/EndpointResolver.js +247 -0
- package/src/core/ForgeContext.js +2227 -0
- package/src/core/ForgeHost.js +122 -0
- package/src/core/ForgePlatform.js +145 -0
- package/src/core/Ingress.js +768 -0
- package/src/core/Interceptors.js +420 -0
- package/src/core/MessageBus.js +310 -0
- package/src/core/Prometheus.js +305 -0
- package/src/core/RequestContext.js +413 -0
- package/src/core/RoutingStrategy.js +316 -0
- package/src/core/Supervisor.js +1306 -0
- package/src/core/ThreadAllocator.js +196 -0
- package/src/core/WorkerChannelManager.js +879 -0
- package/src/core/config.js +624 -0
- package/src/core/host-config.js +311 -0
- package/src/core/network-utils.js +166 -0
- package/src/core/platform-config.js +308 -0
- package/src/decorators/ServiceProxy.js +899 -0
- package/src/decorators/index.js +571 -0
- package/src/deploy/NginxGenerator.js +865 -0
- package/src/deploy/PlatformManifestGenerator.js +96 -0
- package/src/deploy/RouteManifestGenerator.js +112 -0
- package/src/deploy/index.js +984 -0
- package/src/frontend/FrontendDevLifecycle.js +65 -0
- package/src/frontend/FrontendPluginOrchestrator.js +187 -0
- package/src/frontend/SiteResolver.js +63 -0
- package/src/frontend/StaticMountRegistry.js +90 -0
- package/src/frontend/index.js +5 -0
- package/src/frontend/plugins/index.js +2 -0
- package/src/frontend/plugins/viteFrontend.js +79 -0
- package/src/frontend/types.js +35 -0
- package/src/index.js +56 -0
- package/src/internals.js +31 -0
- package/src/plugins/PluginManager.js +537 -0
- package/src/plugins/ScopedPostgres.js +192 -0
- package/src/plugins/ScopedRedis.js +142 -0
- package/src/plugins/index.js +1729 -0
- package/src/registry/ServiceRegistry.js +796 -0
- package/src/scaling/ScaleAdvisor.js +442 -0
- package/src/services/Service.js +195 -0
- package/src/services/worker-bootstrap.js +676 -0
- package/src/templates/auth-service.js +65 -0
- package/src/templates/identity-service.js +75 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ThreadAllocator
|
|
5
|
+
*
|
|
6
|
+
* Distributes available CPU cores across services based on
|
|
7
|
+
* fixed thread counts and weighted auto-allocation.
|
|
8
|
+
*/
|
|
9
|
+
export class ThreadAllocator {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
// Allow override via environment variable or config
|
|
12
|
+
let envCpus = null;
|
|
13
|
+
if (process.env.FORGE_CPUS) {
|
|
14
|
+
const parsed = parseInt(process.env.FORGE_CPUS, 10);
|
|
15
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
16
|
+
envCpus = parsed;
|
|
17
|
+
} else {
|
|
18
|
+
console.warn(
|
|
19
|
+
`[ThreadForge] Warning: Invalid FORGE_CPUS value "${process.env.FORGE_CPUS}". ` +
|
|
20
|
+
`Expected a positive integer. Ignoring.`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const rawCpus = options.cpus ?? envCpus ?? os.cpus().length;
|
|
26
|
+
|
|
27
|
+
// Clamp to minimum of 1 to handle constrained environments where os.cpus() returns 0
|
|
28
|
+
this.totalCpus = Math.max(1, rawCpus);
|
|
29
|
+
|
|
30
|
+
// Log warning if clamping occurred
|
|
31
|
+
if (rawCpus < 1) {
|
|
32
|
+
console.warn(
|
|
33
|
+
`[ThreadForge] Warning: CPU count detected as ${rawCpus}, clamping to minimum of 1. ` +
|
|
34
|
+
`This may occur in constrained sandbox environments. ` +
|
|
35
|
+
`Override with FORGE_CPUS environment variable if needed.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.reservedForSupervisor = options.reserved ?? 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Calculate thread allocation for all services.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} services - Normalized service configs (keyed by name)
|
|
46
|
+
* @returns {Map<string, number>} - Service name → allocated thread count
|
|
47
|
+
*/
|
|
48
|
+
allocate(services) {
|
|
49
|
+
const available = this.totalCpus - this.reservedForSupervisor;
|
|
50
|
+
const entries = Object.values(services);
|
|
51
|
+
|
|
52
|
+
if (entries.length === 0) {
|
|
53
|
+
return new Map();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Phase 1: Honor fixed thread counts
|
|
57
|
+
let remaining = available;
|
|
58
|
+
const fixedServices = entries.filter((s) => typeof s.threads === "number");
|
|
59
|
+
const autoServices = entries.filter((s) => s.threads === "auto");
|
|
60
|
+
|
|
61
|
+
let fixedTotal = 0;
|
|
62
|
+
for (const s of fixedServices) {
|
|
63
|
+
fixedTotal += s.threads;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (fixedTotal > available) {
|
|
67
|
+
console.warn(
|
|
68
|
+
`[ThreadForge] Warning: Fixed thread allocations (${fixedTotal}) exceed ` +
|
|
69
|
+
`available cores (${available}). Services may contend for resources.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
remaining = Math.max(0, remaining - fixedTotal);
|
|
74
|
+
|
|
75
|
+
// Phase 2: Distribute remaining cores to 'auto' services by weight
|
|
76
|
+
const totalWeight = autoServices.reduce((sum, s) => sum + Math.max(1, s.weight), 0);
|
|
77
|
+
|
|
78
|
+
const allocation = new Map();
|
|
79
|
+
|
|
80
|
+
for (const s of fixedServices) {
|
|
81
|
+
allocation.set(s.name, s.threads);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (autoServices.length > 0 && remaining > 0) {
|
|
85
|
+
// First pass: proportional allocation
|
|
86
|
+
let allocated = 0;
|
|
87
|
+
const provisional = [];
|
|
88
|
+
|
|
89
|
+
for (const s of autoServices) {
|
|
90
|
+
const share = Math.max(1, Math.round((s.weight / totalWeight) * remaining));
|
|
91
|
+
provisional.push({ name: s.name, threads: share });
|
|
92
|
+
allocated += share;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Adjust if we over/under allocated due to rounding
|
|
96
|
+
const diff = remaining - allocated;
|
|
97
|
+
if (diff !== 0) {
|
|
98
|
+
// Sort by weight descending, give/take from highest weight services
|
|
99
|
+
provisional.sort((a, b) => {
|
|
100
|
+
const wA = services[a.name]?.weight ?? 1;
|
|
101
|
+
const wB = services[b.name]?.weight ?? 1;
|
|
102
|
+
return wB - wA;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let adjust = Math.abs(diff);
|
|
106
|
+
const direction = diff > 0 ? 1 : -1;
|
|
107
|
+
let i = 0;
|
|
108
|
+
|
|
109
|
+
let safetyCounter = 0;
|
|
110
|
+
const maxIterations = provisional.length * 2;
|
|
111
|
+
while (adjust > 0 && safetyCounter < maxIterations) {
|
|
112
|
+
safetyCounter++;
|
|
113
|
+
const newVal = provisional[i].threads + direction;
|
|
114
|
+
if (newVal >= 1) {
|
|
115
|
+
provisional[i].threads = newVal;
|
|
116
|
+
adjust--;
|
|
117
|
+
}
|
|
118
|
+
i++;
|
|
119
|
+
if (i >= provisional.length) i = 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (adjust > 0) {
|
|
123
|
+
console.warn(`[ThreadAllocator] Could not fully adjust allocation (${adjust} threads remaining)`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Second pass: if total still exceeds remaining, reduce lowest-weight services
|
|
128
|
+
let totalProvisional = provisional.reduce((sum, p) => sum + p.threads, 0);
|
|
129
|
+
if (totalProvisional > remaining) {
|
|
130
|
+
// Sort by weight ascending so we reduce lowest-weight first
|
|
131
|
+
const byWeightAsc = [...provisional].sort((a, b) => {
|
|
132
|
+
const wA = services[a.name]?.weight ?? 1;
|
|
133
|
+
const wB = services[b.name]?.weight ?? 1;
|
|
134
|
+
return wA - wB;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
for (const p of byWeightAsc) {
|
|
138
|
+
if (totalProvisional <= remaining) break;
|
|
139
|
+
const excess = totalProvisional - remaining;
|
|
140
|
+
const canReduce = p.threads - 1; // keep at least 1
|
|
141
|
+
if (canReduce <= 0) continue;
|
|
142
|
+
const reduction = Math.min(canReduce, excess);
|
|
143
|
+
p.threads -= reduction;
|
|
144
|
+
totalProvisional -= reduction;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (totalProvisional > remaining) {
|
|
148
|
+
console.warn(
|
|
149
|
+
`[ThreadAllocator] Auto services allocated ${totalProvisional} threads ` +
|
|
150
|
+
`but only ${remaining} available. Some services may contend for resources.`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const p of provisional) {
|
|
156
|
+
allocation.set(p.name, p.threads);
|
|
157
|
+
}
|
|
158
|
+
} else if (autoServices.length > 0) {
|
|
159
|
+
// No remaining cores, give each auto service 1 thread minimum
|
|
160
|
+
for (const s of autoServices) {
|
|
161
|
+
allocation.set(s.name, 1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return allocation;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get a summary of the allocation for display.
|
|
170
|
+
*/
|
|
171
|
+
summary(services) {
|
|
172
|
+
const allocation = this.allocate(services);
|
|
173
|
+
let totalAllocated = 0;
|
|
174
|
+
const rows = [];
|
|
175
|
+
|
|
176
|
+
for (const [name, threads] of allocation) {
|
|
177
|
+
totalAllocated += threads;
|
|
178
|
+
const svc = services[name];
|
|
179
|
+
rows.push({
|
|
180
|
+
name,
|
|
181
|
+
port: svc.port,
|
|
182
|
+
threads,
|
|
183
|
+
mode: svc.mode,
|
|
184
|
+
weight: svc.weight,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
totalCpus: this.totalCpus,
|
|
190
|
+
reserved: this.reservedForSupervisor,
|
|
191
|
+
allocated: totalAllocated,
|
|
192
|
+
idle: Math.max(0, this.totalCpus - this.reservedForSupervisor - totalAllocated),
|
|
193
|
+
services: rows,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|