com.jimuwd.xian.registry-proxy 1.1.16 → 1.1.18

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}`);
@@ -213,9 +218,6 @@ async function fetchFromRegistry(registry, targetUrl, reqFromDownstreamClient, l
213
218
  // Return null means skipping current upstream registry.
214
219
  return null;
215
220
  }
216
- finally {
217
- limiter.release();
218
- }
219
221
  }
220
222
  async function writeResponseToDownstreamClient(registryInfo, targetUrl, resToDownstreamClient, upstreamResponse, reqFromDownstreamClient, proxyInfo, _proxyPort, registryInfos) {
221
223
  logger.debug(() => `Writing upstream registry server ${registryInfo.normalizedRegistryUrl}'s ${upstreamResponse.status}${upstreamResponse.statusText ? (' "' + upstreamResponse.statusText + '"') : ''} response to downstream client.`);
@@ -366,14 +368,14 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
366
368
  logger.info('Active registries:', registryInfos.map(r => r.normalizedRegistryUrl));
367
369
  logger.info('Proxy base path:', basePathPrefixedWithSlash);
368
370
  logger.info('HTTPS:', !!proxyInfo.https);
371
+ logger.info(`Proxy server request handler rate limit is ${LIMITER.maxConcurrency}`);
369
372
  const requestHandler = async (reqFromDownstreamClient, resToDownstreamClient) => {
370
373
  const downstreamUserAgent = reqFromDownstreamClient.headers["user-agent"]; // "curl/x.x.x"
371
374
  const downstreamIp = getDownstreamClientIp(reqFromDownstreamClient);
372
375
  const downstreamRequestedHttpMethod = reqFromDownstreamClient.method; // "GET", "POST", etc.
373
376
  const downstreamRequestedHost = reqFromDownstreamClient.headers.host; // "example.com:8080"
374
377
  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}`);
378
+ logger.info(`Received downstream request from '${downstreamUserAgent}' ${downstreamIp} ${downstreamRequestedHttpMethod} ${downstreamRequestedHost} ${downstreamRequestedFullPath}`);
377
379
  if (!downstreamRequestedFullPath || !downstreamRequestedHost) {
378
380
  logger.warn(`400 Invalid Request, downstream ${downstreamUserAgent} req.url is absent or downstream.headers.host is absent.`);
379
381
  resToDownstreamClient.writeHead(400).end('Invalid Request');
@@ -403,7 +405,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
403
405
  logger.warn(`Downstream ${reqFromDownstreamClient.headers["user-agent"]} request is destroyed, no need to proxy request to upstream ${targetUrl} any more.`);
404
406
  return;
405
407
  }
406
- const okResponseOrNull = await fetchFromRegistry(targetRegistry, targetUrl, reqFromDownstreamClient, limiter);
408
+ const okResponseOrNull = await fetchFromRegistry(targetRegistry, targetUrl, reqFromDownstreamClient, LIMITER);
407
409
  if (okResponseOrNull) {
408
410
  successfulResponseFromUpstream = okResponseOrNull;
409
411
  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 executionContext;
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,52 +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
- executionContext = new WeakMap();
6
9
  constructor(maxConcurrency) {
7
- if (maxConcurrency <= 0)
10
+ if (maxConcurrency <= 0) {
8
11
  throw new Error("maxConcurrency must be positive");
12
+ }
9
13
  this.maxConcurrency = maxConcurrency;
10
14
  }
11
- getContextId() {
12
- return {}; // 每次调用生成唯一对象引用
13
- }
14
15
  async acquire() {
15
- const contextId = this.getContextId();
16
- const depth = this.executionContext.get(contextId) || 0;
17
- if (depth > 0) {
18
- this.executionContext.set(contextId, depth + 1);
19
- return;
20
- }
21
16
  if (this.current < this.maxConcurrency) {
22
17
  this.current++;
23
- this.executionContext.set(contextId, 1);
24
18
  return;
25
19
  }
26
20
  return new Promise((resolve) => {
27
- this.queue.push(() => {
28
- this.current++;
29
- this.executionContext.set(contextId, 1);
30
- resolve();
31
- });
21
+ this.queue.push(resolve);
32
22
  });
33
23
  }
34
24
  release() {
35
- const contextId = this.getContextId();
36
- const depth = this.executionContext.get(contextId);
37
- if (depth === undefined) {
25
+ if (this.current <= 0) {
38
26
  throw new Error("release() called without acquire()");
39
27
  }
40
- if (depth > 1) {
41
- this.executionContext.set(contextId, depth - 1);
42
- return;
43
- }
44
- this.executionContext.delete(contextId);
45
28
  this.current--;
46
- if (this.queue.length > 0) {
47
- const next = this.queue.shift();
48
- // 仍异步执行,但需确保顺序
49
- Promise.resolve().then(next);
29
+ const next = this.queue.shift();
30
+ if (next) {
31
+ // 异步执行,避免递归调用栈溢出
32
+ Promise.resolve().then(() => {
33
+ this.current++;
34
+ next();
35
+ });
50
36
  }
51
37
  }
52
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.16",
3
+ "version": "1.1.18",
4
4
  "description": "A lightweight npm registry local proxy with fallback support",
5
5
  "type": "module",
6
6
  "main": "dist/server/index.js",