com.jimuwd.xian.registry-proxy 1.1.17 → 1.1.19

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.
@@ -13,7 +13,8 @@ import { gracefulShutdown, registerProcessShutdownHook } from "./gracefullShutdo
13
13
  import { writePortFile } from "../port.js";
14
14
  import resolveEnvValue from "../utils/resolveEnvValue.js";
15
15
  const { readFile } = fsPromises;
16
- const limiter = new ConcurrencyLimiter(5);
16
+ // 整个registry-proxy server实例 使用的全局限流器
17
+ const LIMITER = new ConcurrencyLimiter(5);
17
18
  function removeEndingSlashAndForceStartingSlash(str) {
18
19
  if (!str)
19
20
  return '/';
@@ -137,8 +138,12 @@ async function loadProxyInfo(proxyConfigPath = './.registry-proxy.yml', localYar
137
138
  const basePath = removeEndingSlashAndForceStartingSlash(proxyConfig.basePath);
138
139
  return { registries, https, basePath };
139
140
  }
141
+ // 有并发限流控制,禁止嵌套调用,否则导致死锁
140
142
  async function fetchFromRegistry(registry, targetUrl, reqFromDownstreamClient, limiter) {
141
- await limiter.acquire();
143
+ // 并发限流控制
144
+ return limiter.run(() => _fetchFromRegistry(registry, targetUrl, reqFromDownstreamClient));
145
+ }
146
+ async function _fetchFromRegistry(registry, targetUrl, reqFromDownstreamClient) {
142
147
  try {
143
148
  logger.info(`Fetching from upstream: ${targetUrl}`);
144
149
  const headersFromDownstreamClient = reqFromDownstreamClient.headers;
@@ -158,10 +163,10 @@ async function fetchFromRegistry(registry, targetUrl, reqFromDownstreamClient, l
158
163
  content-type=${response.headers.get('content-type')} content-encoding=${response.headers.get('content-encoding')} content-length=${response.headers.get('content-length')} transfer-encoding=${response.headers.get('transfer-encoding')}`);
159
164
  return response;
160
165
  }
161
- else if (response.status == 301) {
162
- // HTTP 301 Permanently Moved.
166
+ else if (response.status == 301 || response.status == 308) {
167
+ // HTTP 301 Permanently Moved / HTTP 308 Permanent Redirect
163
168
  logger.info(`${response.status} ${response.statusText} response from upstream ${targetUrl}, moved to location=${response.headers.get('location')}`);
164
- // 对于301永久转义响应,registry-proxy 的行为:透传给下游客户端,让客户端自行跳转(提示:这个跳转后的请求将不再走registry-proxy代理了)
169
+ // 对于301/308永久转义响应,registry-proxy 的行为:透传给下游客户端,让客户端自行跳转(提示:这个跳转后的请求将不再走registry-proxy代理了)
165
170
  return response;
166
171
  }
167
172
  else if (response.status == 302 || response.status == 303 || response.status == 307) {
@@ -174,7 +179,7 @@ async function fetchFromRegistry(registry, targetUrl, reqFromDownstreamClient, l
174
179
  if (redirectedLocation) {
175
180
  logger.info(`${response.status} ${response.statusText} response from upstream ${targetUrl}
176
181
  Fetching from redirected location=${redirectedLocation}`);
177
- return await fetchFromRegistry(registry, redirectedLocation, reqFromDownstreamClient, limiter);
182
+ return await _fetchFromRegistry(registry, redirectedLocation, reqFromDownstreamClient);
178
183
  }
179
184
  else {
180
185
  logger.warn(`${response.status} ${response.statusText} response from upstream ${targetUrl}, but redirect location is empty, skip fetching from ${targetUrl}`);
@@ -194,28 +199,23 @@ async function fetchFromRegistry(registry, targetUrl, reqFromDownstreamClient, l
194
199
  }
195
200
  catch (e) {
196
201
  // Fetch form one of the configured upstream registries failed, this is expected behavior, not error.
197
- if (e instanceof Error) {
198
- const errCode = e.code;
199
- if (errCode === 'ECONNREFUSED') {
200
- logger.info(`Upstream ${targetUrl} refused connection [ECONNREFUSED], skip fetching from registry ${registry.normalizedRegistryUrl}`);
201
- }
202
- else if (errCode === 'ENOTFOUND') {
203
- logger.info(`Unknown upstream domain name in ${targetUrl} [ENOTFOUND], skip fetching from registry ${registry.normalizedRegistryUrl}.`);
204
- }
205
- else {
206
- // Other net error code, print log with stacktrace
207
- logger.warn(`Failed to fetch from ${targetUrl}, ${e.message}`, e);
208
- }
202
+ const errCode = e?.code;
203
+ if (errCode === 'ECONNREFUSED') {
204
+ logger.info(`Upstream ${targetUrl} refused connection [ECONNREFUSED], skip fetching from registry ${registry.normalizedRegistryUrl}`);
205
+ }
206
+ else if (errCode === 'ENOTFOUND') {
207
+ logger.info(`Unknown hostname in upstream url ${targetUrl} [ENOTFOUND], skip fetching from registry ${registry.normalizedRegistryUrl}.`);
208
+ }
209
+ else if (errCode === 'EAI_AGAIN') {
210
+ logger.info(`Could not resolve hostname in upstream url ${targetUrl} [EAI_AGAIN], skip fetching from registry ${registry.normalizedRegistryUrl}.`);
209
211
  }
210
212
  else {
211
- logger.error("Unknown error", e);
213
+ // Other net error code, print log with stacktrace
214
+ logger.warn(`Failed to fetch from ${targetUrl}`, e);
212
215
  }
213
216
  // Return null means skipping current upstream registry.
214
217
  return null;
215
218
  }
216
- finally {
217
- limiter.release();
218
- }
219
219
  }
220
220
  async function writeResponseToDownstreamClient(registryInfo, targetUrl, resToDownstreamClient, upstreamResponse, reqFromDownstreamClient, proxyInfo, _proxyPort, registryInfos) {
221
221
  logger.debug(() => `Writing upstream registry server ${registryInfo.normalizedRegistryUrl}'s ${upstreamResponse.status}${upstreamResponse.statusText ? (' "' + upstreamResponse.statusText + '"') : ''} response to downstream client.`);
@@ -366,14 +366,14 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
366
366
  logger.info('Active registries:', registryInfos.map(r => r.normalizedRegistryUrl));
367
367
  logger.info('Proxy base path:', basePathPrefixedWithSlash);
368
368
  logger.info('HTTPS:', !!proxyInfo.https);
369
+ logger.info(`Proxy server request handler rate limit is ${LIMITER.maxConcurrency}`);
369
370
  const requestHandler = async (reqFromDownstreamClient, resToDownstreamClient) => {
370
371
  const downstreamUserAgent = reqFromDownstreamClient.headers["user-agent"]; // "curl/x.x.x"
371
372
  const downstreamIp = getDownstreamClientIp(reqFromDownstreamClient);
372
373
  const downstreamRequestedHttpMethod = reqFromDownstreamClient.method; // "GET", "POST", etc.
373
374
  const downstreamRequestedHost = reqFromDownstreamClient.headers.host; // "example.com:8080"
374
375
  const downstreamRequestedFullPath = reqFromDownstreamClient.url; // "/some/path?param=1&param=2"
375
- logger.info(`Received downstream request from '${downstreamUserAgent}' ${downstreamIp} ${downstreamRequestedHttpMethod} ${downstreamRequestedHost} ${downstreamRequestedFullPath}
376
- Proxy server request handler rate limit is ${limiter.maxConcurrency}`);
376
+ logger.info(`Received downstream request from '${downstreamUserAgent}' ${downstreamIp} ${downstreamRequestedHttpMethod} ${downstreamRequestedHost} ${downstreamRequestedFullPath}`);
377
377
  if (!downstreamRequestedFullPath || !downstreamRequestedHost) {
378
378
  logger.warn(`400 Invalid Request, downstream ${downstreamUserAgent} req.url is absent or downstream.headers.host is absent.`);
379
379
  resToDownstreamClient.writeHead(400).end('Invalid Request');
@@ -403,7 +403,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
403
403
  logger.warn(`Downstream ${reqFromDownstreamClient.headers["user-agent"]} request is destroyed, no need to proxy request to upstream ${targetUrl} any more.`);
404
404
  return;
405
405
  }
406
- const okResponseOrNull = await fetchFromRegistry(targetRegistry, targetUrl, reqFromDownstreamClient, limiter);
406
+ const okResponseOrNull = await fetchFromRegistry(targetRegistry, targetUrl, reqFromDownstreamClient, LIMITER);
407
407
  if (okResponseOrNull) {
408
408
  successfulResponseFromUpstream = okResponseOrNull;
409
409
  break;
@@ -1,10 +1,12 @@
1
- export default class ReentrantConcurrencyLimiter {
1
+ /**
2
+ * 并发控制器
3
+ * @note 不支持重入,重入可能导致死锁
4
+ */
5
+ export default class ConcurrencyLimiter {
2
6
  readonly maxConcurrency: number;
3
7
  private current;
4
8
  private queue;
5
- private executionStack;
6
9
  constructor(maxConcurrency: number);
7
- private getContextId;
8
10
  acquire(): Promise<void>;
9
11
  release(): void;
10
12
  run<T>(task: () => Promise<T>): Promise<T>;
@@ -1,51 +1,38 @@
1
- export default class ReentrantConcurrencyLimiter {
1
+ /**
2
+ * 并发控制器
3
+ * @note 不支持重入,重入可能导致死锁
4
+ */
5
+ export default class ConcurrencyLimiter {
2
6
  maxConcurrency;
3
7
  current = 0;
4
8
  queue = [];
5
- executionStack = [];
6
9
  constructor(maxConcurrency) {
7
10
  if (maxConcurrency <= 0) {
8
11
  throw new Error("maxConcurrency must be positive");
9
12
  }
10
13
  this.maxConcurrency = maxConcurrency;
11
14
  }
12
- getContextId() {
13
- return Symbol('context');
14
- }
15
15
  async acquire() {
16
- const contextId = this.getContextId();
17
- const existingContext = this.executionStack.find(c => c.contextId === contextId);
18
- if (existingContext) {
19
- existingContext.depth++;
20
- return;
21
- }
22
16
  if (this.current < this.maxConcurrency) {
23
17
  this.current++;
24
- this.executionStack.push({ contextId, depth: 1 });
25
18
  return;
26
19
  }
27
20
  return new Promise((resolve) => {
28
- this.queue.push(() => {
29
- this.current++;
30
- this.executionStack.push({ contextId, depth: 1 });
31
- resolve();
32
- });
21
+ this.queue.push(resolve);
33
22
  });
34
23
  }
35
24
  release() {
36
- if (this.executionStack.length === 0) {
25
+ if (this.current <= 0) {
37
26
  throw new Error("release() called without acquire()");
38
27
  }
39
- const lastContext = this.executionStack[this.executionStack.length - 1];
40
- lastContext.depth--;
41
- if (lastContext.depth === 0) {
42
- this.executionStack.pop();
43
- this.current--;
44
- if (this.queue.length > 0) {
45
- const next = this.queue.shift();
46
- if (next)
47
- next();
48
- }
28
+ this.current--;
29
+ const next = this.queue.shift();
30
+ if (next) {
31
+ // 异步执行,避免递归调用栈溢出
32
+ Promise.resolve().then(() => {
33
+ this.current++;
34
+ next();
35
+ });
49
36
  }
50
37
  }
51
38
  async run(task) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.jimuwd.xian.registry-proxy",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "description": "A lightweight npm registry local proxy with fallback support",
5
5
  "type": "module",
6
6
  "main": "dist/server/index.js",