@hile/micro 2.0.7 → 2.0.15
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/README.md +104 -19
- package/dist/application.d.ts +66 -3
- package/dist/application.js +676 -114
- package/dist/registry.d.ts +8 -1
- package/dist/registry.js +146 -36
- package/dist/server.d.ts +1 -0
- package/dist/server.js +13 -2
- package/package.json +5 -6
package/README.md
CHANGED
|
@@ -108,20 +108,50 @@ const result = await response();
|
|
|
108
108
|
|
|
109
109
|
### 熔断器 (Circuit Breaker)
|
|
110
110
|
|
|
111
|
-
`call()` 自动跟踪每个 namespace 下各 peer
|
|
111
|
+
`call()` 和 `stream()` 自动跟踪每个 namespace 下各 peer 的调用结果。熔断状态保存在调用方 `Application` 的本地内存中,不依赖 Redis、数据库或 Registry 共享状态。
|
|
112
112
|
|
|
113
113
|
| 场景 | 行为 |
|
|
114
114
|
|------|------|
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
115
|
+
| `closed` 状态调用失败 | 累计连续失败次数 |
|
|
116
|
+
| 连续失败达到阈值 | peer 进入 `open`,选路时通过 Registry `exclude` 临时排除 |
|
|
117
|
+
| 冷却时间到期 | peer 进入 `half-open`,只放行少量探测请求 |
|
|
118
|
+
| `half-open` 探测成功 | 累计成功次数,达到阈值后恢复 `closed` |
|
|
119
|
+
| `half-open` 探测失败 | 重新进入 `open`,冷却时间指数退避 |
|
|
120
|
+
| 所有 `open` peer 都被排除 | 重置该 namespace 的本地熔断状态,重新选择 peer;若 Registry 不可用但已选中的缓存 Client 仍连接,则沿用缓存发起探测,避免完全饿死 |
|
|
119
121
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
如果 peer 正处于 `half-open` 且探测名额已满,本次调用不会占用额外探测名额;有其他 peer 时改选其他 peer,没有可用 peer 时需要在已有探测完成后再尝试。
|
|
123
|
+
|
|
124
|
+
默认策略:
|
|
125
|
+
|
|
126
|
+
| 配置 | 默认值 | 说明 |
|
|
127
|
+
|------|--------|------|
|
|
128
|
+
| `failureThreshold` | `3` | 连续失败 3 次后打开熔断 |
|
|
129
|
+
| `failureWindowMs` | `60000` | 连续失败统计窗口 |
|
|
130
|
+
| `successThreshold` | `2` | half-open 连续成功 2 次后恢复 |
|
|
131
|
+
| `cooldownMs` | `10000` | 首次打开后的冷却时间 |
|
|
132
|
+
| `maxCooldownMs` | `120000` | 指数退避冷却上限 |
|
|
133
|
+
| `halfOpenMaxProbes` | `1` | half-open 同时放行的探测数 |
|
|
134
|
+
|
|
135
|
+
可以在构造 `Application` 时覆盖:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const app = new Application({
|
|
139
|
+
namespace: 'checkout',
|
|
140
|
+
registry: { host, port },
|
|
141
|
+
circuitBreaker: {
|
|
142
|
+
failureThreshold: 3,
|
|
143
|
+
failureWindowMs: 60_000,
|
|
144
|
+
successThreshold: 2,
|
|
145
|
+
cooldownMs: 10_000,
|
|
146
|
+
maxCooldownMs: 120_000,
|
|
147
|
+
halfOpenMaxProbes: 1,
|
|
148
|
+
shouldRecordFailure: (err) => true,
|
|
149
|
+
shouldRetry: (err) => true,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`shouldRecordFailure` 返回 `false` 的错误不会计入熔断;`shouldRetry` 返回 `false` 的错误不会自动重试。可用它们区分业务错误和连接、超时等基础设施错误。
|
|
125
155
|
|
|
126
156
|
### 请求超时
|
|
127
157
|
|
|
@@ -136,8 +166,8 @@ const app = new Application({
|
|
|
136
166
|
});
|
|
137
167
|
|
|
138
168
|
// 单次调用覆盖
|
|
139
|
-
await app.call('svc', '/api', data, 5_000);
|
|
140
|
-
await app.call('svc', '/api', data, 1_000, 0); // 1s 超时, 不重试
|
|
169
|
+
await app.call('svc', '/api', data, { timeout: 5_000 }); // 5s 超时, 默认重试 1 次
|
|
170
|
+
await app.call('svc', '/api', data, { timeout: 1_000, retries: 0 }); // 1s 超时, 不重试
|
|
141
171
|
```
|
|
142
172
|
|
|
143
173
|
超时触发时,底层 MessageModem 会向对端发送 **ABORT** 消息取消远程执行。
|
|
@@ -169,12 +199,51 @@ try {
|
|
|
169
199
|
`call()` 默认 retries=1,失败后自动换 peer 重试:
|
|
170
200
|
|
|
171
201
|
```typescript
|
|
172
|
-
await app.call('svc', '/api', data);
|
|
173
|
-
await app.call('svc', '/api', data, 5000, 3); // 超时 5s, 重试 3 次
|
|
174
|
-
await app.call('svc', '/api', data, 5000, 0); // 超时 5s, 不重试
|
|
202
|
+
await app.call('svc', '/api', data); // 默认重试 1 次
|
|
203
|
+
await app.call('svc', '/api', data, { timeout: 5000, retries: 3 }); // 超时 5s, 重试 3 次
|
|
204
|
+
await app.call('svc', '/api', data, { timeout: 5000, retries: 0 }); // 超时 5s, 不重试
|
|
175
205
|
```
|
|
176
206
|
|
|
177
|
-
重试策略:失败 → `
|
|
207
|
+
重试策略:失败 → 按 `shouldRecordFailure` 更新本地熔断状态 → 按 `shouldRetry` 判断是否继续下一次尝试 → 下次发现服务时用 `getActiveExcludes()` 排除 `open` 或探测名额已满的 `half-open` peer → Registry `/‑/find` 返回其他 peer。
|
|
208
|
+
|
|
209
|
+
### 流式调用 (Stream)
|
|
210
|
+
|
|
211
|
+
`stream()` 用于**需要持续推送数据**的场景:大数据集、实时事件、LLM token 流、进度上报等。不需要流式传输时优先用 `call()`。
|
|
212
|
+
|
|
213
|
+
**Provider 侧**:消息处理器必须返回 async generator(`async function*`)。
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// 通过 register() 注册
|
|
217
|
+
app.register('/events', async function* () {
|
|
218
|
+
for (let i = 0; i < 100; i++) {
|
|
219
|
+
yield { seq: i, time: Date.now() }
|
|
220
|
+
await new Promise(r => setTimeout(r, 100))
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// 或通过 .msg 文件定义(推荐)
|
|
225
|
+
// src/messages/events.msg.ts
|
|
226
|
+
import { defineMessage } from '@hile/message-loader'
|
|
227
|
+
export default defineMessage(async function* ({ data }) {
|
|
228
|
+
for (const item of await fetchItems(data.query)) {
|
|
229
|
+
yield item
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Consumer 侧**:`app.stream()` 返回 `Readable` stream,可用 `for await` 逐 chunk 消费。
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
const stream = await app.stream('data-svc', '/events', { query: 'recent' })
|
|
238
|
+
for await (const chunk of stream) {
|
|
239
|
+
console.log(chunk) // { seq: 0, time: 1718000000000 }
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**注意事项**:
|
|
244
|
+
- 普通 handler(返回非 async iterable)被 `stream()` 调用时会报错 `"Invalid async iterable"`
|
|
245
|
+
- 不需要流式传输时用 `call()`,不要用 `stream()` 取单次返回值
|
|
246
|
+
- `stream()` 享有与 `call()` 相同的选路与熔断;同步建流失败可按 `retries` 重试,流已返回后的异步错误会计入熔断但不会自动重放流
|
|
178
247
|
|
|
179
248
|
### 健康检查
|
|
180
249
|
|
|
@@ -197,7 +266,7 @@ const health = await app.dispatch('/-/health', {});
|
|
|
197
266
|
|
|
198
267
|
### 缓存降级
|
|
199
268
|
|
|
200
|
-
当 Registry 不可用但本地仍有已缓存的 Client 连接时,`get()`
|
|
269
|
+
当 Registry 不可用但本地仍有已缓存的 Client 连接时,`get()` 自动降级使用缓存。熔断器在“所有 `open` peer 都被排除”并触发本地 reset 时,也会保留已经选中的可用缓存 Client,不会因为 Registry 短暂不可用而丢弃仍可通信的连接。
|
|
201
270
|
|
|
202
271
|
| 场景 | 行为 |
|
|
203
272
|
|------|------|
|
|
@@ -363,10 +432,25 @@ class Application extends Server {
|
|
|
363
432
|
namespace: string,
|
|
364
433
|
url: string,
|
|
365
434
|
data: any,
|
|
366
|
-
|
|
367
|
-
|
|
435
|
+
options?: {
|
|
436
|
+
timeout?: number, // 请求超时(ms),默认 requestTimeoutMs
|
|
437
|
+
retries?: number, // 失败重试次数,默认 1
|
|
438
|
+
signal?: AbortSignal, // 手动取消
|
|
439
|
+
},
|
|
368
440
|
): Promise<T>;
|
|
369
441
|
|
|
442
|
+
// 流式调用:get + stream + 熔断;同步建流失败可重试
|
|
443
|
+
// provider handler 必须返回 async generator,consumer 获得 Readable stream
|
|
444
|
+
stream(
|
|
445
|
+
namespace: string,
|
|
446
|
+
url: string,
|
|
447
|
+
data: any,
|
|
448
|
+
options?: {
|
|
449
|
+
signal?: AbortSignal,
|
|
450
|
+
retries?: number, // 失败重试次数,默认 1
|
|
451
|
+
},
|
|
452
|
+
): Promise<import('stream').Readable>;
|
|
453
|
+
|
|
370
454
|
// 注册路由(provider 侧)
|
|
371
455
|
register<T = any>(url: string, handler: (ctx) => Promise<T>): () => void;
|
|
372
456
|
|
|
@@ -410,6 +494,7 @@ class Server extends MessageLoader {
|
|
|
410
494
|
class Client extends MessageWs {
|
|
411
495
|
request(url: string, data: any, timeout?: number): { abort(): void; response<T>(): Promise<T> };
|
|
412
496
|
push(url: string, data: any, timeout?: number): void;
|
|
497
|
+
stream(url: string, data: any, options?: { signal?: AbortSignal }): Readable;
|
|
413
498
|
dispose(): void;
|
|
414
499
|
}
|
|
415
500
|
```
|
package/dist/application.d.ts
CHANGED
|
@@ -15,6 +15,28 @@ type EnvRequestResult<T extends Record<string, Record<string, any>>, R> = R exte
|
|
|
15
15
|
} ? {
|
|
16
16
|
[K in N]: EnvFieldsForRequest<T, N, F>;
|
|
17
17
|
} : never;
|
|
18
|
+
export type CircuitBreakerStatus = 'closed' | 'open' | 'half-open';
|
|
19
|
+
export type CircuitBreakerOptions = {
|
|
20
|
+
/** 连续失败达到阈值后打开熔断器,默认 3 */
|
|
21
|
+
failureThreshold?: number;
|
|
22
|
+
/** 连续失败统计窗口(毫秒),默认 60000 */
|
|
23
|
+
failureWindowMs?: number;
|
|
24
|
+
/** half-open 探测连续成功达到阈值后恢复,默认 2 */
|
|
25
|
+
successThreshold?: number;
|
|
26
|
+
/** 首次打开后的冷却时间(毫秒),默认 10000 */
|
|
27
|
+
cooldownMs?: number;
|
|
28
|
+
/** 指数退避冷却时间上限(毫秒),默认 120000 */
|
|
29
|
+
maxCooldownMs?: number;
|
|
30
|
+
/** half-open 状态下同时放行的探测请求数,默认 1 */
|
|
31
|
+
halfOpenMaxProbes?: number;
|
|
32
|
+
/** 返回 false 的错误不会计入熔断,默认全部计入 */
|
|
33
|
+
shouldRecordFailure?: (err: unknown) => boolean;
|
|
34
|
+
/** 返回 false 的错误不会自动重试,默认全部允许重试 */
|
|
35
|
+
shouldRetry?: (err: unknown) => boolean;
|
|
36
|
+
};
|
|
37
|
+
type RegistryLookupOptions = {
|
|
38
|
+
allowExcludedCachedFallback?: boolean;
|
|
39
|
+
};
|
|
18
40
|
export type GetEnvVariablesResult<T extends Record<string, Record<string, any>>, Requests extends readonly EnvRequest<T>[]> = UnionToIntersection<EnvRequestResult<T, Requests[number]>>;
|
|
19
41
|
export type ApplicationProps = {
|
|
20
42
|
namespace: string;
|
|
@@ -23,30 +45,68 @@ export type ApplicationProps = {
|
|
|
23
45
|
registryLookupTimeoutMs?: number;
|
|
24
46
|
/** 单次 request() 等待响应的上限(毫秒),默认 `30000` */
|
|
25
47
|
requestTimeoutMs?: number;
|
|
48
|
+
/** 本地内存熔断策略配置 */
|
|
49
|
+
circuitBreaker?: CircuitBreakerOptions;
|
|
26
50
|
} & MicroServerProps;
|
|
27
51
|
export declare class Application extends Server {
|
|
28
52
|
private registry?;
|
|
29
53
|
private reconnectTimeout?;
|
|
30
54
|
private registryReconnectPromise;
|
|
55
|
+
private registryReconnectGeneration;
|
|
31
56
|
/** 为 true 时不再向 Registry 重连(listen 返回的 teardown 已触发) */
|
|
32
57
|
private stopped;
|
|
58
|
+
private listenGeneration;
|
|
33
59
|
private readonly _registry_address;
|
|
34
60
|
private readonly _registryLookupTimeoutMs;
|
|
35
61
|
private readonly _requestTimeoutMs;
|
|
62
|
+
private readonly _circuitBreaker;
|
|
36
63
|
private readonly namespaces;
|
|
37
|
-
private static readonly CB_COOLDOWN_MS;
|
|
38
64
|
private readonly circuitBreakers;
|
|
39
|
-
private readonly fallbacks;
|
|
40
65
|
private readonly topics;
|
|
66
|
+
private readonly publishedTopics;
|
|
67
|
+
private readonly publishedTopicRevisions;
|
|
68
|
+
private readonly publishedTopicDirty;
|
|
69
|
+
private readonly publishedTopicVersions;
|
|
70
|
+
private readonly publishedTopicSignatures;
|
|
71
|
+
private readonly topicSyncs;
|
|
72
|
+
private readonly topicUpdateVersions;
|
|
73
|
+
private publishIntentVersion;
|
|
41
74
|
constructor(props: ApplicationProps);
|
|
75
|
+
private dispatchTopicUpdate;
|
|
42
76
|
listen(port?: number): Promise<() => Promise<void>>;
|
|
43
77
|
private scheduleRegistryRetry;
|
|
78
|
+
private canUsePubSub;
|
|
79
|
+
private assertCanUsePubSub;
|
|
80
|
+
private ensureRegistryReconnectScheduled;
|
|
81
|
+
private handleRegistrySyncFailure;
|
|
82
|
+
private registryRequestOptions;
|
|
83
|
+
private recordPublishedTopic;
|
|
84
|
+
private enqueueTopicSync;
|
|
85
|
+
private syncPublishedTopic;
|
|
86
|
+
private syncUnpublishedTopic;
|
|
87
|
+
private syncUnsubscribedTopic;
|
|
88
|
+
private syncRestoredSubscription;
|
|
89
|
+
private restoreSubscription;
|
|
90
|
+
private rollbackLocalSubscription;
|
|
44
91
|
private reconnectToRegistry;
|
|
92
|
+
private peerKey;
|
|
93
|
+
private getPeerStates;
|
|
94
|
+
private getOrCreatePeerState;
|
|
95
|
+
private deletePeerState;
|
|
96
|
+
private openCircuit;
|
|
97
|
+
private acquireCircuitProbe;
|
|
98
|
+
private shouldRecordCircuitFailure;
|
|
99
|
+
private shouldRetryCircuitFailure;
|
|
45
100
|
private recordSuccess;
|
|
46
101
|
private recordFailure;
|
|
102
|
+
private getActiveCircuitExcludes;
|
|
47
103
|
private getActiveExcludes;
|
|
104
|
+
private trackCircuitStream;
|
|
105
|
+
private selectCircuitClient;
|
|
106
|
+
private selectCircuitProbe;
|
|
48
107
|
private findFromRegistry;
|
|
49
108
|
get(namespace: string, exclude?: string[]): Promise<Client>;
|
|
109
|
+
protected resolveClient(namespace: string, exclude?: string[], options?: RegistryLookupOptions): Promise<Client>;
|
|
50
110
|
call<T = any>(namespace: string, url: string, data: any, options?: {
|
|
51
111
|
timeout?: number;
|
|
52
112
|
retries?: number;
|
|
@@ -60,7 +120,10 @@ export declare class Application extends Server {
|
|
|
60
120
|
update: (payload: T) => Promise</*elided*/ any>;
|
|
61
121
|
unpublish: () => Promise</*elided*/ any>;
|
|
62
122
|
}>;
|
|
63
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* 对同一 topic 可多次 subscribe,各自独立回调。
|
|
125
|
+
* 传入同一个 callback 引用第二次调用时幂等返回 unsubscribe,不重复注册。
|
|
126
|
+
*/
|
|
64
127
|
subscribe<T = any>(topic: string, callback: (data: T) => any, isReconnect?: boolean): Promise<() => Promise<void>>;
|
|
65
128
|
}
|
|
66
129
|
export {};
|