@milaboratories/pl-drivers 1.16.0 → 1.16.1

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.
@@ -26,6 +26,12 @@ var ClientDownload = class {
26
26
  localFileReadLimiter = new _milaboratories_ts_helpers.ConcurrencyLimitingExecutor(32);
27
27
  /** Caches presigned download URLs by resource id until they (almost) expire. */
28
28
  urlCache;
29
+ /** Active remote downloads; in-flight gauges are summed over this set on read. */
30
+ inFlight = /* @__PURE__ */ new Set();
31
+ presignedUrlCacheHits = 0;
32
+ presignedUrlCacheMisses = 0;
33
+ presignedUrlStaleHits = 0;
34
+ presignedUrlRequestSumLatencyMs = 0;
29
35
  constructor(wireClientProviderFactory, httpClient, logger, localProjections) {
30
36
  this.httpClient = httpClient;
31
37
  this.logger = logger;
@@ -54,22 +60,65 @@ var ClientDownload = class {
54
60
  const remoteHeaders = Object.fromEntries(headers.map(({ name, value }) => [name, value]));
55
61
  this.logger.info(`blob ${(0, _milaboratories_pl_client.stringifyWithResourceId)(info)} download started, url: ${downloadUrl}, range: ${JSON.stringify(ops.range ?? null)}`);
56
62
  const timer = _milaboratories_helpers.PerfTimer.start();
57
- const result = isLocal(downloadUrl) ? await this.withLocalFileContent(downloadUrl, ops, handler) : await this.remoteFileDownloader.withContent(downloadUrl, remoteHeaders, ops, handler);
63
+ const result = isLocal(downloadUrl) ? await this.withLocalFileContent(downloadUrl, ops, handler) : await this.withTrackedRemoteContent(downloadUrl, remoteHeaders, ops, handler);
58
64
  this.logger.info(`blob ${(0, _milaboratories_pl_client.stringifyWithResourceId)(info)} download finished, took: ${timer.elapsed()}`);
59
65
  return result;
60
66
  };
61
67
  const cached = this.urlCache.get(info.id);
62
68
  if (cached !== void 0) try {
63
- return await attempt(cached);
69
+ const result = await attempt(cached);
70
+ this.presignedUrlCacheHits++;
71
+ return result;
64
72
  } catch (error) {
65
73
  if (!require_download_errors.isDownloadNetworkError400(error)) throw error;
66
74
  this.urlCache.delete(info.id);
75
+ this.presignedUrlStaleHits++;
67
76
  this.logger.info(`cached download URL for blob ${(0, _milaboratories_pl_client.stringifyWithResourceId)(info)} rejected (status ${error.statusCode}), re-fetching`);
68
77
  }
78
+ else this.presignedUrlCacheMisses++;
79
+ const urlFetchStartMs = performance.now();
69
80
  const fresh = await this.grpcGetDownloadUrl(info, options, ops.signal);
81
+ this.presignedUrlRequestSumLatencyMs += performance.now() - urlFetchStartMs;
70
82
  this.urlCache.set(info.id, fresh);
71
83
  return await attempt(fresh);
72
84
  }
85
+ /** Download-client-owned slice of {@link BlobDriverMetrics}: presigned-URL cache + in-flight downloads. */
86
+ metrics() {
87
+ let inFlightBytesTotal = 0;
88
+ let inFlightBytesReceived = 0;
89
+ for (const rec of this.inFlight) {
90
+ inFlightBytesTotal += rec.size;
91
+ inFlightBytesReceived += rec.received;
92
+ }
93
+ return {
94
+ downloadsInFlight: this.inFlight.size,
95
+ inFlightBytesTotal,
96
+ inFlightBytesReceived,
97
+ presignedUrlCacheHits: this.presignedUrlCacheHits,
98
+ presignedUrlCacheMisses: this.presignedUrlCacheMisses,
99
+ presignedUrlStaleHits: this.presignedUrlStaleHits,
100
+ presignedUrlRequestSumLatencyMs: this.presignedUrlRequestSumLatencyMs
101
+ };
102
+ }
103
+ /** Wraps a remote download so it appears in the in-flight gauges and its received bytes are counted live. */
104
+ async withTrackedRemoteContent(url, headers, ops, handler) {
105
+ const rec = {
106
+ size: 0,
107
+ received: 0
108
+ };
109
+ this.inFlight.add(rec);
110
+ try {
111
+ return await this.remoteFileDownloader.withContent(url, headers, ops, async (content, size) => {
112
+ rec.size = size;
113
+ return await handler(content.pipeThrough(new TransformStream({ transform: (chunk, controller) => {
114
+ rec.received += chunk.byteLength;
115
+ controller.enqueue(chunk);
116
+ } })), size);
117
+ });
118
+ } finally {
119
+ this.inFlight.delete(rec);
120
+ }
121
+ }
73
122
  async withLocalFileContent(url, ops, handler) {
74
123
  const { storageId, relativePath } = parseLocalUrl(url);
75
124
  const fullPath = getFullPath(storageId, this.localStorageIdsToRoot, relativePath);
@@ -1 +1 @@
1
- {"version":3,"file":"download.cjs","names":["ConcurrencyLimitingExecutor","DownloadClient","RestAPI","RemoteFileDownloader","DownloadUrlCache","PerfTimer","isDownloadNetworkError400","fsp","fs","Readable","path"],"sources":["../../src/clients/download.ts"],"sourcesContent":["/* eslint-disable n/no-unsupported-features/node-builtins */\nimport type { WireClientProvider, WireClientProviderFactory } from \"@milaboratories/pl-client\";\nimport {\n addRTypeToMetadata,\n stringifyWithResourceId,\n RestAPI,\n createRTypeRoutingHeader,\n parseSignedResourceId,\n signatureToBase64Url,\n} from \"@milaboratories/pl-client\";\nimport type { ResourceInfo } from \"@milaboratories/pl-tree\";\nimport { PerfTimer } from \"@milaboratories/helpers\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport { ConcurrencyLimitingExecutor } from \"@milaboratories/ts-helpers\";\nimport type { RpcOptions } from \"@protobuf-ts/runtime-rpc\";\nimport * as fs from \"node:fs\";\nimport * as fsp from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport type { Dispatcher } from \"undici\";\nimport type { LocalStorageProjection } from \"../drivers/types\";\nimport { type ContentHandler, RemoteFileDownloader } from \"../helpers/download\";\nimport { isDownloadNetworkError400 } from \"../helpers/download_errors\";\nimport { validateAbsolute } from \"../helpers/validate\";\nimport type { DownloadAPI_GetDownloadURL_Response } from \"../proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol\";\nimport { DownloadClient } from \"../proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client\";\nimport type { DownloadApiPaths, DownloadRestClientType } from \"../proto-rest\";\nimport { type GetContentOptions } from \"@milaboratories/pl-model-common\";\nimport { DownloadUrlCache } from \"./download_url_cache\";\n\n/** Gets URLs for downloading from pl-core, parses them and reads or downloads\n * files locally and from the web. */\nexport class ClientDownload {\n public readonly wire: WireClientProvider<DownloadRestClientType | DownloadClient>;\n private readonly remoteFileDownloader: RemoteFileDownloader;\n\n /** Helps to find a storage root directory by a storage id from URL scheme. */\n private readonly localStorageIdsToRoot: Map<string, string>;\n\n /** Concurrency limiter for local file reads - limit to 32 parallel reads */\n private readonly localFileReadLimiter = new ConcurrencyLimitingExecutor(32);\n\n /** Caches presigned download URLs by resource id until they (almost) expire. */\n private readonly urlCache: DownloadUrlCache;\n\n constructor(\n wireClientProviderFactory: WireClientProviderFactory,\n public readonly httpClient: Dispatcher,\n public readonly logger: MiLogger,\n /** Pl storages available locally */\n localProjections: LocalStorageProjection[],\n ) {\n this.wire = wireClientProviderFactory.createWireClientProvider((wireConn) => {\n if (wireConn.type === \"grpc\") {\n return new DownloadClient(wireConn.Transport);\n } else {\n return RestAPI.createClient<DownloadApiPaths>({\n hostAndPort: wireConn.Config.hostAndPort,\n ssl: wireConn.Config.ssl,\n dispatcher: wireConn.Dispatcher,\n middlewares: wireConn.Middlewares,\n });\n }\n });\n this.remoteFileDownloader = new RemoteFileDownloader(httpClient);\n this.localStorageIdsToRoot = newLocalStorageIdsToRoot(localProjections);\n this.urlCache = new DownloadUrlCache(logger);\n }\n\n close() {}\n\n /**\n * Gets a presign URL and downloads the file.\n * An optional range with 2 numbers from what byte and to what byte to download can be provided.\n * @param fromBytes - from byte including this byte\n * @param toBytes - to byte excluding this byte\n */\n async withBlobContent<T>(\n info: ResourceInfo,\n options: RpcOptions | undefined,\n ops: GetContentOptions,\n handler: ContentHandler<T>,\n ): Promise<T> {\n const attempt = async ({\n downloadUrl,\n headers,\n }: DownloadAPI_GetDownloadURL_Response): Promise<T> => {\n const remoteHeaders = Object.fromEntries(headers.map(({ name, value }) => [name, value]));\n this.logger.info(\n `blob ${stringifyWithResourceId(info)} download started, ` +\n `url: ${downloadUrl}, ` +\n `range: ${JSON.stringify(ops.range ?? null)}`,\n );\n\n const timer = PerfTimer.start();\n const result = isLocal(downloadUrl)\n ? await this.withLocalFileContent(downloadUrl, ops, handler)\n : await this.remoteFileDownloader.withContent(downloadUrl, remoteHeaders, ops, handler);\n\n this.logger.info(\n `blob ${stringifyWithResourceId(info)} download finished, ` + `took: ${timer.elapsed()}`,\n );\n return result;\n };\n\n const cached = this.urlCache.get(info.id);\n if (cached !== undefined) {\n try {\n return await attempt(cached);\n } catch (error) {\n if (!isDownloadNetworkError400(error)) throw error;\n this.urlCache.delete(info.id);\n this.logger.info(\n `cached download URL for blob ${stringifyWithResourceId(info)} rejected ` +\n `(status ${error.statusCode}), re-fetching`,\n );\n }\n }\n\n const fresh = await this.grpcGetDownloadUrl(info, options, ops.signal);\n this.urlCache.set(info.id, fresh);\n return await attempt(fresh);\n }\n\n async withLocalFileContent<T>(\n url: string,\n ops: GetContentOptions,\n handler: ContentHandler<T>,\n ): Promise<T> {\n const { storageId, relativePath } = parseLocalUrl(url);\n const fullPath = getFullPath(storageId, this.localStorageIdsToRoot, relativePath);\n\n return await this.localFileReadLimiter.run(async () => {\n const readOps = {\n start: ops.range?.from,\n end: ops.range?.to !== undefined ? ops.range.to - 1 : undefined,\n signal: ops.signal,\n };\n let stream: fs.ReadStream | undefined;\n let handlerSuccess = false;\n\n try {\n const stat = await fsp.stat(fullPath);\n stream = fs.createReadStream(fullPath, readOps);\n const webStream = Readable.toWeb(stream);\n\n const result = await handler(webStream, stat.size);\n handlerSuccess = true;\n return result;\n } catch (error) {\n // Cleanup on error (including handler errors)\n if (!handlerSuccess && stream && !stream.destroyed) {\n stream.destroy();\n }\n throw error;\n }\n });\n }\n\n private async grpcGetDownloadUrl(\n { id, type }: ResourceInfo,\n options?: RpcOptions,\n signal?: AbortSignal,\n ): Promise<DownloadAPI_GetDownloadURL_Response> {\n const withAbort = options ?? {};\n withAbort.abort = signal;\n\n const { globalId, signature } = parseSignedResourceId(id);\n const client = this.wire.get();\n if (client instanceof DownloadClient) {\n return await client.getDownloadURL(\n { resourceId: globalId, resourceSignature: signature, isInternalUse: false },\n addRTypeToMetadata(type, withAbort),\n ).response;\n } else {\n return (\n await client.POST(\"/v1/get-download-url\", {\n body: {\n resourceId: globalId.toString(),\n resourceSignature: signatureToBase64Url(signature),\n isInternalUse: false,\n },\n headers: { ...createRTypeRoutingHeader(type) },\n })\n ).data!;\n }\n }\n}\n\nexport function parseLocalUrl(url: string) {\n const parsed = new URL(url);\n if (parsed.pathname == \"\")\n throw new WrongLocalFileUrl(`url for local filepath ${url} does not match url scheme`);\n\n return {\n storageId: parsed.host,\n relativePath: decodeURIComponent(parsed.pathname.slice(1)),\n };\n}\n\nexport function getFullPath(\n storageId: string,\n localStorageIdsToRoot: Map<string, string>,\n relativePath: string,\n) {\n const root = localStorageIdsToRoot.get(storageId);\n if (root === undefined) throw new UnknownStorageError(`Unknown storage location: ${storageId}`);\n\n if (root === \"\") return relativePath;\n\n return path.join(root, relativePath);\n}\n\nconst storageProtocol = \"storage://\";\nfunction isLocal(url: string) {\n return url.startsWith(storageProtocol);\n}\n\n/** Throws when a local URL have invalid scheme. */\nexport class WrongLocalFileUrl extends Error {\n name = \"WrongLocalFileUrl\";\n}\n\n/** Happens when a storage for a local file can't be found. */\nexport class UnknownStorageError extends Error {\n name = \"UnknownStorageError\";\n}\n\nexport function newLocalStorageIdsToRoot(projections: LocalStorageProjection[]) {\n const idToRoot: Map<string, string> = new Map();\n for (const lp of projections) {\n // Empty string means no prefix, i.e. any file on this machine can be got from the storage.\n if (lp.localPath !== \"\") {\n validateAbsolute(lp.localPath);\n }\n idToRoot.set(lp.storageId, lp.localPath);\n }\n\n return idToRoot;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"download.cjs","names":["ConcurrencyLimitingExecutor","DownloadClient","RestAPI","RemoteFileDownloader","DownloadUrlCache","PerfTimer","isDownloadNetworkError400","fsp","fs","Readable","path"],"sources":["../../src/clients/download.ts"],"sourcesContent":["/* eslint-disable n/no-unsupported-features/node-builtins */\nimport type { WireClientProvider, WireClientProviderFactory } from \"@milaboratories/pl-client\";\nimport {\n addRTypeToMetadata,\n stringifyWithResourceId,\n RestAPI,\n createRTypeRoutingHeader,\n parseSignedResourceId,\n signatureToBase64Url,\n} from \"@milaboratories/pl-client\";\nimport type { ResourceInfo } from \"@milaboratories/pl-tree\";\nimport { PerfTimer } from \"@milaboratories/helpers\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport { ConcurrencyLimitingExecutor } from \"@milaboratories/ts-helpers\";\nimport type { RpcOptions } from \"@protobuf-ts/runtime-rpc\";\nimport * as fs from \"node:fs\";\nimport * as fsp from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport type { Dispatcher } from \"undici\";\nimport type { LocalStorageProjection } from \"../drivers/types\";\nimport { type ContentHandler, RemoteFileDownloader } from \"../helpers/download\";\nimport { isDownloadNetworkError400 } from \"../helpers/download_errors\";\nimport { validateAbsolute } from \"../helpers/validate\";\nimport type { DownloadAPI_GetDownloadURL_Response } from \"../proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol\";\nimport { DownloadClient } from \"../proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client\";\nimport type { DownloadApiPaths, DownloadRestClientType } from \"../proto-rest\";\nimport { type GetContentOptions, type BlobDriverMetrics } from \"@milaboratories/pl-model-common\";\nimport { DownloadUrlCache } from \"./download_url_cache\";\n\n/** Subset of {@link BlobDriverMetrics} owned by the download client (presigned cache + in-flight downloads). */\ntype ClientDownloadMetrics = Omit<BlobDriverMetrics, \"uncachedRequests\" | \"uncachedRequestBytes\">;\n\n/** Gets URLs for downloading from pl-core, parses them and reads or downloads\n * files locally and from the web. */\nexport class ClientDownload {\n public readonly wire: WireClientProvider<DownloadRestClientType | DownloadClient>;\n private readonly remoteFileDownloader: RemoteFileDownloader;\n\n /** Helps to find a storage root directory by a storage id from URL scheme. */\n private readonly localStorageIdsToRoot: Map<string, string>;\n\n /** Concurrency limiter for local file reads - limit to 32 parallel reads */\n private readonly localFileReadLimiter = new ConcurrencyLimitingExecutor(32);\n\n /** Caches presigned download URLs by resource id until they (almost) expire. */\n private readonly urlCache: DownloadUrlCache;\n\n /** Active remote downloads; in-flight gauges are summed over this set on read. */\n private readonly inFlight = new Set<{ size: number; received: number }>();\n private presignedUrlCacheHits = 0;\n private presignedUrlCacheMisses = 0;\n private presignedUrlStaleHits = 0;\n private presignedUrlRequestSumLatencyMs = 0;\n\n constructor(\n wireClientProviderFactory: WireClientProviderFactory,\n public readonly httpClient: Dispatcher,\n public readonly logger: MiLogger,\n /** Pl storages available locally */\n localProjections: LocalStorageProjection[],\n ) {\n this.wire = wireClientProviderFactory.createWireClientProvider((wireConn) => {\n if (wireConn.type === \"grpc\") {\n return new DownloadClient(wireConn.Transport);\n } else {\n return RestAPI.createClient<DownloadApiPaths>({\n hostAndPort: wireConn.Config.hostAndPort,\n ssl: wireConn.Config.ssl,\n dispatcher: wireConn.Dispatcher,\n middlewares: wireConn.Middlewares,\n });\n }\n });\n this.remoteFileDownloader = new RemoteFileDownloader(httpClient);\n this.localStorageIdsToRoot = newLocalStorageIdsToRoot(localProjections);\n this.urlCache = new DownloadUrlCache(logger);\n }\n\n close() {}\n\n /**\n * Gets a presign URL and downloads the file.\n * An optional range with 2 numbers from what byte and to what byte to download can be provided.\n * @param fromBytes - from byte including this byte\n * @param toBytes - to byte excluding this byte\n */\n async withBlobContent<T>(\n info: ResourceInfo,\n options: RpcOptions | undefined,\n ops: GetContentOptions,\n handler: ContentHandler<T>,\n ): Promise<T> {\n const attempt = async ({\n downloadUrl,\n headers,\n }: DownloadAPI_GetDownloadURL_Response): Promise<T> => {\n const remoteHeaders = Object.fromEntries(headers.map(({ name, value }) => [name, value]));\n this.logger.info(\n `blob ${stringifyWithResourceId(info)} download started, ` +\n `url: ${downloadUrl}, ` +\n `range: ${JSON.stringify(ops.range ?? null)}`,\n );\n\n const timer = PerfTimer.start();\n const result = isLocal(downloadUrl)\n ? await this.withLocalFileContent(downloadUrl, ops, handler)\n : await this.withTrackedRemoteContent(downloadUrl, remoteHeaders, ops, handler);\n\n this.logger.info(\n `blob ${stringifyWithResourceId(info)} download finished, ` + `took: ${timer.elapsed()}`,\n );\n return result;\n };\n\n const cached = this.urlCache.get(info.id);\n if (cached !== undefined) {\n try {\n const result = await attempt(cached);\n this.presignedUrlCacheHits++;\n return result;\n } catch (error) {\n if (!isDownloadNetworkError400(error)) throw error;\n this.urlCache.delete(info.id);\n this.presignedUrlStaleHits++;\n this.logger.info(\n `cached download URL for blob ${stringifyWithResourceId(info)} rejected ` +\n `(status ${error.statusCode}), re-fetching`,\n );\n }\n } else {\n this.presignedUrlCacheMisses++;\n }\n\n const urlFetchStartMs = performance.now();\n const fresh = await this.grpcGetDownloadUrl(info, options, ops.signal);\n this.presignedUrlRequestSumLatencyMs += performance.now() - urlFetchStartMs;\n this.urlCache.set(info.id, fresh);\n return await attempt(fresh);\n }\n\n /** Download-client-owned slice of {@link BlobDriverMetrics}: presigned-URL cache + in-flight downloads. */\n metrics(): ClientDownloadMetrics {\n let inFlightBytesTotal = 0;\n let inFlightBytesReceived = 0;\n for (const rec of this.inFlight) {\n inFlightBytesTotal += rec.size;\n inFlightBytesReceived += rec.received;\n }\n return {\n downloadsInFlight: this.inFlight.size,\n inFlightBytesTotal,\n inFlightBytesReceived,\n presignedUrlCacheHits: this.presignedUrlCacheHits,\n presignedUrlCacheMisses: this.presignedUrlCacheMisses,\n presignedUrlStaleHits: this.presignedUrlStaleHits,\n presignedUrlRequestSumLatencyMs: this.presignedUrlRequestSumLatencyMs,\n };\n }\n\n /** Wraps a remote download so it appears in the in-flight gauges and its received bytes are counted live. */\n private async withTrackedRemoteContent<T>(\n url: string,\n headers: Record<string, string>,\n ops: GetContentOptions,\n handler: ContentHandler<T>,\n ): Promise<T> {\n const rec = { size: 0, received: 0 };\n this.inFlight.add(rec);\n try {\n return await this.remoteFileDownloader.withContent(\n url,\n headers,\n ops,\n async (content, size) => {\n rec.size = size;\n const counted = content.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform: (chunk, controller) => {\n rec.received += chunk.byteLength;\n controller.enqueue(chunk);\n },\n }),\n );\n return await handler(counted, size);\n },\n );\n } finally {\n this.inFlight.delete(rec);\n }\n }\n\n async withLocalFileContent<T>(\n url: string,\n ops: GetContentOptions,\n handler: ContentHandler<T>,\n ): Promise<T> {\n const { storageId, relativePath } = parseLocalUrl(url);\n const fullPath = getFullPath(storageId, this.localStorageIdsToRoot, relativePath);\n\n return await this.localFileReadLimiter.run(async () => {\n const readOps = {\n start: ops.range?.from,\n end: ops.range?.to !== undefined ? ops.range.to - 1 : undefined,\n signal: ops.signal,\n };\n let stream: fs.ReadStream | undefined;\n let handlerSuccess = false;\n\n try {\n const stat = await fsp.stat(fullPath);\n stream = fs.createReadStream(fullPath, readOps);\n const webStream = Readable.toWeb(stream);\n\n const result = await handler(webStream, stat.size);\n handlerSuccess = true;\n return result;\n } catch (error) {\n // Cleanup on error (including handler errors)\n if (!handlerSuccess && stream && !stream.destroyed) {\n stream.destroy();\n }\n throw error;\n }\n });\n }\n\n private async grpcGetDownloadUrl(\n { id, type }: ResourceInfo,\n options?: RpcOptions,\n signal?: AbortSignal,\n ): Promise<DownloadAPI_GetDownloadURL_Response> {\n const withAbort = options ?? {};\n withAbort.abort = signal;\n\n const { globalId, signature } = parseSignedResourceId(id);\n const client = this.wire.get();\n if (client instanceof DownloadClient) {\n return await client.getDownloadURL(\n { resourceId: globalId, resourceSignature: signature, isInternalUse: false },\n addRTypeToMetadata(type, withAbort),\n ).response;\n } else {\n return (\n await client.POST(\"/v1/get-download-url\", {\n body: {\n resourceId: globalId.toString(),\n resourceSignature: signatureToBase64Url(signature),\n isInternalUse: false,\n },\n headers: { ...createRTypeRoutingHeader(type) },\n })\n ).data!;\n }\n }\n}\n\nexport function parseLocalUrl(url: string) {\n const parsed = new URL(url);\n if (parsed.pathname == \"\")\n throw new WrongLocalFileUrl(`url for local filepath ${url} does not match url scheme`);\n\n return {\n storageId: parsed.host,\n relativePath: decodeURIComponent(parsed.pathname.slice(1)),\n };\n}\n\nexport function getFullPath(\n storageId: string,\n localStorageIdsToRoot: Map<string, string>,\n relativePath: string,\n) {\n const root = localStorageIdsToRoot.get(storageId);\n if (root === undefined) throw new UnknownStorageError(`Unknown storage location: ${storageId}`);\n\n if (root === \"\") return relativePath;\n\n return path.join(root, relativePath);\n}\n\nconst storageProtocol = \"storage://\";\nfunction isLocal(url: string) {\n return url.startsWith(storageProtocol);\n}\n\n/** Throws when a local URL have invalid scheme. */\nexport class WrongLocalFileUrl extends Error {\n name = \"WrongLocalFileUrl\";\n}\n\n/** Happens when a storage for a local file can't be found. */\nexport class UnknownStorageError extends Error {\n name = \"UnknownStorageError\";\n}\n\nexport function newLocalStorageIdsToRoot(projections: LocalStorageProjection[]) {\n const idToRoot: Map<string, string> = new Map();\n for (const lp of projections) {\n // Empty string means no prefix, i.e. any file on this machine can be got from the storage.\n if (lp.localPath !== \"\") {\n validateAbsolute(lp.localPath);\n }\n idToRoot.set(lp.storageId, lp.localPath);\n }\n\n return idToRoot;\n}\n"],"mappings":"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"}
@@ -6,10 +6,12 @@ import { WireClientProvider, WireClientProviderFactory } from "@milaboratories/p
6
6
  import { Dispatcher } from "undici";
7
7
  import { RpcOptions } from "@protobuf-ts/runtime-rpc";
8
8
  import { MiLogger } from "@milaboratories/ts-helpers";
9
- import { GetContentOptions } from "@milaboratories/pl-model-common";
9
+ import { BlobDriverMetrics, GetContentOptions } from "@milaboratories/pl-model-common";
10
10
  import { ResourceInfo } from "@milaboratories/pl-tree";
11
11
 
12
12
  //#region src/clients/download.d.ts
13
+ /** Subset of {@link BlobDriverMetrics} owned by the download client (presigned cache + in-flight downloads). */
14
+ type ClientDownloadMetrics = Omit<BlobDriverMetrics, "uncachedRequests" | "uncachedRequestBytes">;
13
15
  /** Gets URLs for downloading from pl-core, parses them and reads or downloads
14
16
  * files locally and from the web. */
15
17
  declare class ClientDownload {
@@ -23,6 +25,12 @@ declare class ClientDownload {
23
25
  private readonly localFileReadLimiter;
24
26
  /** Caches presigned download URLs by resource id until they (almost) expire. */
25
27
  private readonly urlCache;
28
+ /** Active remote downloads; in-flight gauges are summed over this set on read. */
29
+ private readonly inFlight;
30
+ private presignedUrlCacheHits;
31
+ private presignedUrlCacheMisses;
32
+ private presignedUrlStaleHits;
33
+ private presignedUrlRequestSumLatencyMs;
26
34
  constructor(wireClientProviderFactory: WireClientProviderFactory, httpClient: Dispatcher, logger: MiLogger, /** Pl storages available locally */
27
35
 
28
36
  localProjections: LocalStorageProjection[]);
@@ -34,6 +42,10 @@ declare class ClientDownload {
34
42
  * @param toBytes - to byte excluding this byte
35
43
  */
36
44
  withBlobContent<T>(info: ResourceInfo, options: RpcOptions | undefined, ops: GetContentOptions, handler: ContentHandler$1<T>): Promise<T>;
45
+ /** Download-client-owned slice of {@link BlobDriverMetrics}: presigned-URL cache + in-flight downloads. */
46
+ metrics(): ClientDownloadMetrics;
47
+ /** Wraps a remote download so it appears in the in-flight gauges and its received bytes are counted live. */
48
+ private withTrackedRemoteContent;
37
49
  withLocalFileContent<T>(url: string, ops: GetContentOptions, handler: ContentHandler$1<T>): Promise<T>;
38
50
  private grpcGetDownloadUrl;
39
51
  }
@@ -1 +1 @@
1
- {"version":3,"file":"download.d.ts","names":[],"sources":["../../src/clients/download.ts"],"mappings":";;;;;;;;;;;;;;cAgCa,cAAA;EAAA,SAeO,UAAA,EAAY,UAAA;EAAA,SACZ,MAAA,EAAQ,QAAA;EAAA,SAfV,IAAA,EAAM,kBAAA,CAAmB,sBAAA,GAAyB,cAAA;EAAA,iBACjD,oBAAA;EADwB;EAAA,iBAIxB,qBAAA;EAJK;EAAA,iBAOL,oBAAA;EAOa;EAAA,iBAJb,QAAA;cAGf,yBAAA,EAA2B,yBAAA,EACX,UAAA,EAAY,UAAA,EACZ,MAAA,EAAQ,QAAA,EA8BlB;;EA5BN,gBAAA,EAAkB,sBAAA;EAmBpB,KAAA,CAAA;EAY0B;;;;;;EAJpB,eAAA,GAAA,CACJ,IAAA,EAAM,YAAA,EACN,OAAA,EAAS,UAAA,cACT,GAAA,EAAK,iBAAA,EACL,OAAA,EAAS,gBAAA,CAAe,CAAA,IACvB,OAAA,CAAQ,CAAA;EA0CL,oBAAA,GAAA,CACJ,GAAA,UACA,GAAA,EAAK,iBAAA,EACL,OAAA,EAAS,gBAAA,CAAe,CAAA,IACvB,OAAA,CAAQ,CAAA;EAAA,QA+BG,kBAAA;AAAA;AAAA,iBA8BA,aAAA,CAAc,GAAA;;;;iBAWd,WAAA,CACd,SAAA,UACA,qBAAA,EAAuB,GAAA,kBACvB,YAAA;;cAgBW,iBAAA,SAA0B,KAAA;EACrC,IAAA;AAAA;;cAIW,mBAAA,SAA4B,KAAA;EACvC,IAAA;AAAA;AAAA,iBAGc,wBAAA,CAAyB,WAAA,EAAa,sBAAA,KAAwB,GAAA"}
1
+ {"version":3,"file":"download.d.ts","names":[],"sources":["../../src/clients/download.ts"],"mappings":";;;;;;;;;;;;;KA+BK,qBAAA,GAAwB,IAAA,CAAK,iBAAA;AAJ+D;;AAAA,cAQpF,cAAA;EAAA,SAsBO,UAAA,EAAY,UAAA;EAAA,SACZ,MAAA,EAAQ,QAAA;EAAA,SAtBV,IAAA,EAAM,kBAAA,CAAmB,sBAAA,GAAyB,cAAA;EAAA,iBACjD,oBAAA;EAFQ;EAAA,iBAKR,qBAAA;EAkBS;EAAA,iBAfT,oBAAA;EAPiD;EAAA,iBAUjD,QAAA;EAUY;EAAA,iBAPZ,QAAA;EAAA,QACT,qBAAA;EAAA,QACA,uBAAA;EAAA,QACA,qBAAA;EAAA,QACA,+BAAA;cAGN,yBAAA,EAA2B,yBAAA,EACX,UAAA,EAAY,UAAA,EACZ,MAAA,EAAQ,QAAA,EAiCA;;EA/BxB,gBAAA,EAAkB,sBAAA;EAmBpB,KAAA,CAAA;EAaG;;;;;;EALG,eAAA,GAAA,CACJ,IAAA,EAAM,YAAA,EACN,OAAA,EAAS,UAAA,cACT,GAAA,EAAK,iBAAA,EACL,OAAA,EAAS,gBAAA,CAAe,CAAA,IACvB,OAAA,CAAQ,CAAA;EAwGD;EAtDV,OAAA,CAAA,GAAW,qBAAA;EArFO;EAAA,QAwGJ,wBAAA;EA+BR,oBAAA,GAAA,CACJ,GAAA,UACA,GAAA,EAAK,iBAAA,EACL,OAAA,EAAS,gBAAA,CAAe,CAAA,IACvB,OAAA,CAAQ,CAAA;EAAA,QA+BG,kBAAA;AAAA;AAAA,iBA8BA,aAAA,CAAc,GAAA;;;;iBAWd,WAAA,CACd,SAAA,UACA,qBAAA,EAAuB,GAAA,kBACvB,YAAA;;cAgBW,iBAAA,SAA0B,KAAA;EACrC,IAAA;AAAA;;cAIW,mBAAA,SAA4B,KAAA;EACvC,IAAA;AAAA;AAAA,iBAGc,wBAAA,CAAyB,WAAA,EAAa,sBAAA,KAAwB,GAAA"}
@@ -22,6 +22,12 @@ var ClientDownload = class {
22
22
  localFileReadLimiter = new ConcurrencyLimitingExecutor(32);
23
23
  /** Caches presigned download URLs by resource id until they (almost) expire. */
24
24
  urlCache;
25
+ /** Active remote downloads; in-flight gauges are summed over this set on read. */
26
+ inFlight = /* @__PURE__ */ new Set();
27
+ presignedUrlCacheHits = 0;
28
+ presignedUrlCacheMisses = 0;
29
+ presignedUrlStaleHits = 0;
30
+ presignedUrlRequestSumLatencyMs = 0;
25
31
  constructor(wireClientProviderFactory, httpClient, logger, localProjections) {
26
32
  this.httpClient = httpClient;
27
33
  this.logger = logger;
@@ -50,22 +56,65 @@ var ClientDownload = class {
50
56
  const remoteHeaders = Object.fromEntries(headers.map(({ name, value }) => [name, value]));
51
57
  this.logger.info(`blob ${stringifyWithResourceId(info)} download started, url: ${downloadUrl}, range: ${JSON.stringify(ops.range ?? null)}`);
52
58
  const timer = PerfTimer.start();
53
- const result = isLocal(downloadUrl) ? await this.withLocalFileContent(downloadUrl, ops, handler) : await this.remoteFileDownloader.withContent(downloadUrl, remoteHeaders, ops, handler);
59
+ const result = isLocal(downloadUrl) ? await this.withLocalFileContent(downloadUrl, ops, handler) : await this.withTrackedRemoteContent(downloadUrl, remoteHeaders, ops, handler);
54
60
  this.logger.info(`blob ${stringifyWithResourceId(info)} download finished, took: ${timer.elapsed()}`);
55
61
  return result;
56
62
  };
57
63
  const cached = this.urlCache.get(info.id);
58
64
  if (cached !== void 0) try {
59
- return await attempt(cached);
65
+ const result = await attempt(cached);
66
+ this.presignedUrlCacheHits++;
67
+ return result;
60
68
  } catch (error) {
61
69
  if (!isDownloadNetworkError400(error)) throw error;
62
70
  this.urlCache.delete(info.id);
71
+ this.presignedUrlStaleHits++;
63
72
  this.logger.info(`cached download URL for blob ${stringifyWithResourceId(info)} rejected (status ${error.statusCode}), re-fetching`);
64
73
  }
74
+ else this.presignedUrlCacheMisses++;
75
+ const urlFetchStartMs = performance.now();
65
76
  const fresh = await this.grpcGetDownloadUrl(info, options, ops.signal);
77
+ this.presignedUrlRequestSumLatencyMs += performance.now() - urlFetchStartMs;
66
78
  this.urlCache.set(info.id, fresh);
67
79
  return await attempt(fresh);
68
80
  }
81
+ /** Download-client-owned slice of {@link BlobDriverMetrics}: presigned-URL cache + in-flight downloads. */
82
+ metrics() {
83
+ let inFlightBytesTotal = 0;
84
+ let inFlightBytesReceived = 0;
85
+ for (const rec of this.inFlight) {
86
+ inFlightBytesTotal += rec.size;
87
+ inFlightBytesReceived += rec.received;
88
+ }
89
+ return {
90
+ downloadsInFlight: this.inFlight.size,
91
+ inFlightBytesTotal,
92
+ inFlightBytesReceived,
93
+ presignedUrlCacheHits: this.presignedUrlCacheHits,
94
+ presignedUrlCacheMisses: this.presignedUrlCacheMisses,
95
+ presignedUrlStaleHits: this.presignedUrlStaleHits,
96
+ presignedUrlRequestSumLatencyMs: this.presignedUrlRequestSumLatencyMs
97
+ };
98
+ }
99
+ /** Wraps a remote download so it appears in the in-flight gauges and its received bytes are counted live. */
100
+ async withTrackedRemoteContent(url, headers, ops, handler) {
101
+ const rec = {
102
+ size: 0,
103
+ received: 0
104
+ };
105
+ this.inFlight.add(rec);
106
+ try {
107
+ return await this.remoteFileDownloader.withContent(url, headers, ops, async (content, size) => {
108
+ rec.size = size;
109
+ return await handler(content.pipeThrough(new TransformStream({ transform: (chunk, controller) => {
110
+ rec.received += chunk.byteLength;
111
+ controller.enqueue(chunk);
112
+ } })), size);
113
+ });
114
+ } finally {
115
+ this.inFlight.delete(rec);
116
+ }
117
+ }
69
118
  async withLocalFileContent(url, ops, handler) {
70
119
  const { storageId, relativePath } = parseLocalUrl(url);
71
120
  const fullPath = getFullPath(storageId, this.localStorageIdsToRoot, relativePath);
@@ -1 +1 @@
1
- {"version":3,"file":"download.js","names":["fs","path"],"sources":["../../src/clients/download.ts"],"sourcesContent":["/* eslint-disable n/no-unsupported-features/node-builtins */\nimport type { WireClientProvider, WireClientProviderFactory } from \"@milaboratories/pl-client\";\nimport {\n addRTypeToMetadata,\n stringifyWithResourceId,\n RestAPI,\n createRTypeRoutingHeader,\n parseSignedResourceId,\n signatureToBase64Url,\n} from \"@milaboratories/pl-client\";\nimport type { ResourceInfo } from \"@milaboratories/pl-tree\";\nimport { PerfTimer } from \"@milaboratories/helpers\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport { ConcurrencyLimitingExecutor } from \"@milaboratories/ts-helpers\";\nimport type { RpcOptions } from \"@protobuf-ts/runtime-rpc\";\nimport * as fs from \"node:fs\";\nimport * as fsp from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport type { Dispatcher } from \"undici\";\nimport type { LocalStorageProjection } from \"../drivers/types\";\nimport { type ContentHandler, RemoteFileDownloader } from \"../helpers/download\";\nimport { isDownloadNetworkError400 } from \"../helpers/download_errors\";\nimport { validateAbsolute } from \"../helpers/validate\";\nimport type { DownloadAPI_GetDownloadURL_Response } from \"../proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol\";\nimport { DownloadClient } from \"../proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client\";\nimport type { DownloadApiPaths, DownloadRestClientType } from \"../proto-rest\";\nimport { type GetContentOptions } from \"@milaboratories/pl-model-common\";\nimport { DownloadUrlCache } from \"./download_url_cache\";\n\n/** Gets URLs for downloading from pl-core, parses them and reads or downloads\n * files locally and from the web. */\nexport class ClientDownload {\n public readonly wire: WireClientProvider<DownloadRestClientType | DownloadClient>;\n private readonly remoteFileDownloader: RemoteFileDownloader;\n\n /** Helps to find a storage root directory by a storage id from URL scheme. */\n private readonly localStorageIdsToRoot: Map<string, string>;\n\n /** Concurrency limiter for local file reads - limit to 32 parallel reads */\n private readonly localFileReadLimiter = new ConcurrencyLimitingExecutor(32);\n\n /** Caches presigned download URLs by resource id until they (almost) expire. */\n private readonly urlCache: DownloadUrlCache;\n\n constructor(\n wireClientProviderFactory: WireClientProviderFactory,\n public readonly httpClient: Dispatcher,\n public readonly logger: MiLogger,\n /** Pl storages available locally */\n localProjections: LocalStorageProjection[],\n ) {\n this.wire = wireClientProviderFactory.createWireClientProvider((wireConn) => {\n if (wireConn.type === \"grpc\") {\n return new DownloadClient(wireConn.Transport);\n } else {\n return RestAPI.createClient<DownloadApiPaths>({\n hostAndPort: wireConn.Config.hostAndPort,\n ssl: wireConn.Config.ssl,\n dispatcher: wireConn.Dispatcher,\n middlewares: wireConn.Middlewares,\n });\n }\n });\n this.remoteFileDownloader = new RemoteFileDownloader(httpClient);\n this.localStorageIdsToRoot = newLocalStorageIdsToRoot(localProjections);\n this.urlCache = new DownloadUrlCache(logger);\n }\n\n close() {}\n\n /**\n * Gets a presign URL and downloads the file.\n * An optional range with 2 numbers from what byte and to what byte to download can be provided.\n * @param fromBytes - from byte including this byte\n * @param toBytes - to byte excluding this byte\n */\n async withBlobContent<T>(\n info: ResourceInfo,\n options: RpcOptions | undefined,\n ops: GetContentOptions,\n handler: ContentHandler<T>,\n ): Promise<T> {\n const attempt = async ({\n downloadUrl,\n headers,\n }: DownloadAPI_GetDownloadURL_Response): Promise<T> => {\n const remoteHeaders = Object.fromEntries(headers.map(({ name, value }) => [name, value]));\n this.logger.info(\n `blob ${stringifyWithResourceId(info)} download started, ` +\n `url: ${downloadUrl}, ` +\n `range: ${JSON.stringify(ops.range ?? null)}`,\n );\n\n const timer = PerfTimer.start();\n const result = isLocal(downloadUrl)\n ? await this.withLocalFileContent(downloadUrl, ops, handler)\n : await this.remoteFileDownloader.withContent(downloadUrl, remoteHeaders, ops, handler);\n\n this.logger.info(\n `blob ${stringifyWithResourceId(info)} download finished, ` + `took: ${timer.elapsed()}`,\n );\n return result;\n };\n\n const cached = this.urlCache.get(info.id);\n if (cached !== undefined) {\n try {\n return await attempt(cached);\n } catch (error) {\n if (!isDownloadNetworkError400(error)) throw error;\n this.urlCache.delete(info.id);\n this.logger.info(\n `cached download URL for blob ${stringifyWithResourceId(info)} rejected ` +\n `(status ${error.statusCode}), re-fetching`,\n );\n }\n }\n\n const fresh = await this.grpcGetDownloadUrl(info, options, ops.signal);\n this.urlCache.set(info.id, fresh);\n return await attempt(fresh);\n }\n\n async withLocalFileContent<T>(\n url: string,\n ops: GetContentOptions,\n handler: ContentHandler<T>,\n ): Promise<T> {\n const { storageId, relativePath } = parseLocalUrl(url);\n const fullPath = getFullPath(storageId, this.localStorageIdsToRoot, relativePath);\n\n return await this.localFileReadLimiter.run(async () => {\n const readOps = {\n start: ops.range?.from,\n end: ops.range?.to !== undefined ? ops.range.to - 1 : undefined,\n signal: ops.signal,\n };\n let stream: fs.ReadStream | undefined;\n let handlerSuccess = false;\n\n try {\n const stat = await fsp.stat(fullPath);\n stream = fs.createReadStream(fullPath, readOps);\n const webStream = Readable.toWeb(stream);\n\n const result = await handler(webStream, stat.size);\n handlerSuccess = true;\n return result;\n } catch (error) {\n // Cleanup on error (including handler errors)\n if (!handlerSuccess && stream && !stream.destroyed) {\n stream.destroy();\n }\n throw error;\n }\n });\n }\n\n private async grpcGetDownloadUrl(\n { id, type }: ResourceInfo,\n options?: RpcOptions,\n signal?: AbortSignal,\n ): Promise<DownloadAPI_GetDownloadURL_Response> {\n const withAbort = options ?? {};\n withAbort.abort = signal;\n\n const { globalId, signature } = parseSignedResourceId(id);\n const client = this.wire.get();\n if (client instanceof DownloadClient) {\n return await client.getDownloadURL(\n { resourceId: globalId, resourceSignature: signature, isInternalUse: false },\n addRTypeToMetadata(type, withAbort),\n ).response;\n } else {\n return (\n await client.POST(\"/v1/get-download-url\", {\n body: {\n resourceId: globalId.toString(),\n resourceSignature: signatureToBase64Url(signature),\n isInternalUse: false,\n },\n headers: { ...createRTypeRoutingHeader(type) },\n })\n ).data!;\n }\n }\n}\n\nexport function parseLocalUrl(url: string) {\n const parsed = new URL(url);\n if (parsed.pathname == \"\")\n throw new WrongLocalFileUrl(`url for local filepath ${url} does not match url scheme`);\n\n return {\n storageId: parsed.host,\n relativePath: decodeURIComponent(parsed.pathname.slice(1)),\n };\n}\n\nexport function getFullPath(\n storageId: string,\n localStorageIdsToRoot: Map<string, string>,\n relativePath: string,\n) {\n const root = localStorageIdsToRoot.get(storageId);\n if (root === undefined) throw new UnknownStorageError(`Unknown storage location: ${storageId}`);\n\n if (root === \"\") return relativePath;\n\n return path.join(root, relativePath);\n}\n\nconst storageProtocol = \"storage://\";\nfunction isLocal(url: string) {\n return url.startsWith(storageProtocol);\n}\n\n/** Throws when a local URL have invalid scheme. */\nexport class WrongLocalFileUrl extends Error {\n name = \"WrongLocalFileUrl\";\n}\n\n/** Happens when a storage for a local file can't be found. */\nexport class UnknownStorageError extends Error {\n name = \"UnknownStorageError\";\n}\n\nexport function newLocalStorageIdsToRoot(projections: LocalStorageProjection[]) {\n const idToRoot: Map<string, string> = new Map();\n for (const lp of projections) {\n // Empty string means no prefix, i.e. any file on this machine can be got from the storage.\n if (lp.localPath !== \"\") {\n validateAbsolute(lp.localPath);\n }\n idToRoot.set(lp.storageId, lp.localPath);\n }\n\n return idToRoot;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"download.js","names":["fs","path"],"sources":["../../src/clients/download.ts"],"sourcesContent":["/* eslint-disable n/no-unsupported-features/node-builtins */\nimport type { WireClientProvider, WireClientProviderFactory } from \"@milaboratories/pl-client\";\nimport {\n addRTypeToMetadata,\n stringifyWithResourceId,\n RestAPI,\n createRTypeRoutingHeader,\n parseSignedResourceId,\n signatureToBase64Url,\n} from \"@milaboratories/pl-client\";\nimport type { ResourceInfo } from \"@milaboratories/pl-tree\";\nimport { PerfTimer } from \"@milaboratories/helpers\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport { ConcurrencyLimitingExecutor } from \"@milaboratories/ts-helpers\";\nimport type { RpcOptions } from \"@protobuf-ts/runtime-rpc\";\nimport * as fs from \"node:fs\";\nimport * as fsp from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport type { Dispatcher } from \"undici\";\nimport type { LocalStorageProjection } from \"../drivers/types\";\nimport { type ContentHandler, RemoteFileDownloader } from \"../helpers/download\";\nimport { isDownloadNetworkError400 } from \"../helpers/download_errors\";\nimport { validateAbsolute } from \"../helpers/validate\";\nimport type { DownloadAPI_GetDownloadURL_Response } from \"../proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol\";\nimport { DownloadClient } from \"../proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client\";\nimport type { DownloadApiPaths, DownloadRestClientType } from \"../proto-rest\";\nimport { type GetContentOptions, type BlobDriverMetrics } from \"@milaboratories/pl-model-common\";\nimport { DownloadUrlCache } from \"./download_url_cache\";\n\n/** Subset of {@link BlobDriverMetrics} owned by the download client (presigned cache + in-flight downloads). */\ntype ClientDownloadMetrics = Omit<BlobDriverMetrics, \"uncachedRequests\" | \"uncachedRequestBytes\">;\n\n/** Gets URLs for downloading from pl-core, parses them and reads or downloads\n * files locally and from the web. */\nexport class ClientDownload {\n public readonly wire: WireClientProvider<DownloadRestClientType | DownloadClient>;\n private readonly remoteFileDownloader: RemoteFileDownloader;\n\n /** Helps to find a storage root directory by a storage id from URL scheme. */\n private readonly localStorageIdsToRoot: Map<string, string>;\n\n /** Concurrency limiter for local file reads - limit to 32 parallel reads */\n private readonly localFileReadLimiter = new ConcurrencyLimitingExecutor(32);\n\n /** Caches presigned download URLs by resource id until they (almost) expire. */\n private readonly urlCache: DownloadUrlCache;\n\n /** Active remote downloads; in-flight gauges are summed over this set on read. */\n private readonly inFlight = new Set<{ size: number; received: number }>();\n private presignedUrlCacheHits = 0;\n private presignedUrlCacheMisses = 0;\n private presignedUrlStaleHits = 0;\n private presignedUrlRequestSumLatencyMs = 0;\n\n constructor(\n wireClientProviderFactory: WireClientProviderFactory,\n public readonly httpClient: Dispatcher,\n public readonly logger: MiLogger,\n /** Pl storages available locally */\n localProjections: LocalStorageProjection[],\n ) {\n this.wire = wireClientProviderFactory.createWireClientProvider((wireConn) => {\n if (wireConn.type === \"grpc\") {\n return new DownloadClient(wireConn.Transport);\n } else {\n return RestAPI.createClient<DownloadApiPaths>({\n hostAndPort: wireConn.Config.hostAndPort,\n ssl: wireConn.Config.ssl,\n dispatcher: wireConn.Dispatcher,\n middlewares: wireConn.Middlewares,\n });\n }\n });\n this.remoteFileDownloader = new RemoteFileDownloader(httpClient);\n this.localStorageIdsToRoot = newLocalStorageIdsToRoot(localProjections);\n this.urlCache = new DownloadUrlCache(logger);\n }\n\n close() {}\n\n /**\n * Gets a presign URL and downloads the file.\n * An optional range with 2 numbers from what byte and to what byte to download can be provided.\n * @param fromBytes - from byte including this byte\n * @param toBytes - to byte excluding this byte\n */\n async withBlobContent<T>(\n info: ResourceInfo,\n options: RpcOptions | undefined,\n ops: GetContentOptions,\n handler: ContentHandler<T>,\n ): Promise<T> {\n const attempt = async ({\n downloadUrl,\n headers,\n }: DownloadAPI_GetDownloadURL_Response): Promise<T> => {\n const remoteHeaders = Object.fromEntries(headers.map(({ name, value }) => [name, value]));\n this.logger.info(\n `blob ${stringifyWithResourceId(info)} download started, ` +\n `url: ${downloadUrl}, ` +\n `range: ${JSON.stringify(ops.range ?? null)}`,\n );\n\n const timer = PerfTimer.start();\n const result = isLocal(downloadUrl)\n ? await this.withLocalFileContent(downloadUrl, ops, handler)\n : await this.withTrackedRemoteContent(downloadUrl, remoteHeaders, ops, handler);\n\n this.logger.info(\n `blob ${stringifyWithResourceId(info)} download finished, ` + `took: ${timer.elapsed()}`,\n );\n return result;\n };\n\n const cached = this.urlCache.get(info.id);\n if (cached !== undefined) {\n try {\n const result = await attempt(cached);\n this.presignedUrlCacheHits++;\n return result;\n } catch (error) {\n if (!isDownloadNetworkError400(error)) throw error;\n this.urlCache.delete(info.id);\n this.presignedUrlStaleHits++;\n this.logger.info(\n `cached download URL for blob ${stringifyWithResourceId(info)} rejected ` +\n `(status ${error.statusCode}), re-fetching`,\n );\n }\n } else {\n this.presignedUrlCacheMisses++;\n }\n\n const urlFetchStartMs = performance.now();\n const fresh = await this.grpcGetDownloadUrl(info, options, ops.signal);\n this.presignedUrlRequestSumLatencyMs += performance.now() - urlFetchStartMs;\n this.urlCache.set(info.id, fresh);\n return await attempt(fresh);\n }\n\n /** Download-client-owned slice of {@link BlobDriverMetrics}: presigned-URL cache + in-flight downloads. */\n metrics(): ClientDownloadMetrics {\n let inFlightBytesTotal = 0;\n let inFlightBytesReceived = 0;\n for (const rec of this.inFlight) {\n inFlightBytesTotal += rec.size;\n inFlightBytesReceived += rec.received;\n }\n return {\n downloadsInFlight: this.inFlight.size,\n inFlightBytesTotal,\n inFlightBytesReceived,\n presignedUrlCacheHits: this.presignedUrlCacheHits,\n presignedUrlCacheMisses: this.presignedUrlCacheMisses,\n presignedUrlStaleHits: this.presignedUrlStaleHits,\n presignedUrlRequestSumLatencyMs: this.presignedUrlRequestSumLatencyMs,\n };\n }\n\n /** Wraps a remote download so it appears in the in-flight gauges and its received bytes are counted live. */\n private async withTrackedRemoteContent<T>(\n url: string,\n headers: Record<string, string>,\n ops: GetContentOptions,\n handler: ContentHandler<T>,\n ): Promise<T> {\n const rec = { size: 0, received: 0 };\n this.inFlight.add(rec);\n try {\n return await this.remoteFileDownloader.withContent(\n url,\n headers,\n ops,\n async (content, size) => {\n rec.size = size;\n const counted = content.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform: (chunk, controller) => {\n rec.received += chunk.byteLength;\n controller.enqueue(chunk);\n },\n }),\n );\n return await handler(counted, size);\n },\n );\n } finally {\n this.inFlight.delete(rec);\n }\n }\n\n async withLocalFileContent<T>(\n url: string,\n ops: GetContentOptions,\n handler: ContentHandler<T>,\n ): Promise<T> {\n const { storageId, relativePath } = parseLocalUrl(url);\n const fullPath = getFullPath(storageId, this.localStorageIdsToRoot, relativePath);\n\n return await this.localFileReadLimiter.run(async () => {\n const readOps = {\n start: ops.range?.from,\n end: ops.range?.to !== undefined ? ops.range.to - 1 : undefined,\n signal: ops.signal,\n };\n let stream: fs.ReadStream | undefined;\n let handlerSuccess = false;\n\n try {\n const stat = await fsp.stat(fullPath);\n stream = fs.createReadStream(fullPath, readOps);\n const webStream = Readable.toWeb(stream);\n\n const result = await handler(webStream, stat.size);\n handlerSuccess = true;\n return result;\n } catch (error) {\n // Cleanup on error (including handler errors)\n if (!handlerSuccess && stream && !stream.destroyed) {\n stream.destroy();\n }\n throw error;\n }\n });\n }\n\n private async grpcGetDownloadUrl(\n { id, type }: ResourceInfo,\n options?: RpcOptions,\n signal?: AbortSignal,\n ): Promise<DownloadAPI_GetDownloadURL_Response> {\n const withAbort = options ?? {};\n withAbort.abort = signal;\n\n const { globalId, signature } = parseSignedResourceId(id);\n const client = this.wire.get();\n if (client instanceof DownloadClient) {\n return await client.getDownloadURL(\n { resourceId: globalId, resourceSignature: signature, isInternalUse: false },\n addRTypeToMetadata(type, withAbort),\n ).response;\n } else {\n return (\n await client.POST(\"/v1/get-download-url\", {\n body: {\n resourceId: globalId.toString(),\n resourceSignature: signatureToBase64Url(signature),\n isInternalUse: false,\n },\n headers: { ...createRTypeRoutingHeader(type) },\n })\n ).data!;\n }\n }\n}\n\nexport function parseLocalUrl(url: string) {\n const parsed = new URL(url);\n if (parsed.pathname == \"\")\n throw new WrongLocalFileUrl(`url for local filepath ${url} does not match url scheme`);\n\n return {\n storageId: parsed.host,\n relativePath: decodeURIComponent(parsed.pathname.slice(1)),\n };\n}\n\nexport function getFullPath(\n storageId: string,\n localStorageIdsToRoot: Map<string, string>,\n relativePath: string,\n) {\n const root = localStorageIdsToRoot.get(storageId);\n if (root === undefined) throw new UnknownStorageError(`Unknown storage location: ${storageId}`);\n\n if (root === \"\") return relativePath;\n\n return path.join(root, relativePath);\n}\n\nconst storageProtocol = \"storage://\";\nfunction isLocal(url: string) {\n return url.startsWith(storageProtocol);\n}\n\n/** Throws when a local URL have invalid scheme. */\nexport class WrongLocalFileUrl extends Error {\n name = \"WrongLocalFileUrl\";\n}\n\n/** Happens when a storage for a local file can't be found. */\nexport class UnknownStorageError extends Error {\n name = \"UnknownStorageError\";\n}\n\nexport function newLocalStorageIdsToRoot(projections: LocalStorageProjection[]) {\n const idToRoot: Map<string, string> = new Map();\n for (const lp of projections) {\n // Empty string means no prefix, i.e. any file on this machine can be got from the storage.\n if (lp.localPath !== \"\") {\n validateAbsolute(lp.localPath);\n }\n idToRoot.set(lp.storageId, lp.localPath);\n }\n\n return idToRoot;\n}\n"],"mappings":"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"}
@@ -45,6 +45,9 @@ var DownloadDriver = class DownloadDriver {
45
45
  idToLastLines = /* @__PURE__ */ new Map();
46
46
  idToProgressLog = /* @__PURE__ */ new Map();
47
47
  saveDir;
48
+ /** Downloads that bypassed the ranges cache; counted when issued. */
49
+ uncachedRequests = 0;
50
+ uncachedRequestBytes = 0;
48
51
  constructor(logger, clientDownload, clientLogs, saveDir, rangesCacheDir, signer, ops) {
49
52
  this.logger = logger;
50
53
  this.clientDownload = clientDownload;
@@ -147,6 +150,17 @@ var DownloadDriver = class DownloadDriver {
147
150
  bypassRangesCache: true
148
151
  });
149
152
  }
153
+ /**
154
+ * Operational metrics for a monitoring panel. Serv cache metrics are reported separately
155
+ * (different owner) — the panel composes both.
156
+ */
157
+ getMetrics() {
158
+ return {
159
+ uncachedRequests: this.uncachedRequests,
160
+ uncachedRequestBytes: this.uncachedRequestBytes,
161
+ ...this.clientDownload.metrics()
162
+ };
163
+ }
150
164
  async getContentImpl({ handle, options, bypassRangesCache = false }) {
151
165
  const request = () => this.withContent(handle, {
152
166
  ...options,
@@ -190,8 +204,12 @@ var DownloadDriver = class DownloadDriver {
190
204
  signal,
191
205
  handler
192
206
  });
207
+ if (bypassRangesCache) this.uncachedRequests++;
193
208
  return await this.clientDownload.withBlobContent(result.info, { signal }, options, async (content, size) => {
194
- if (bypassRangesCache) return await handler(content, size);
209
+ if (bypassRangesCache) return await handler(content.pipeThrough(new TransformStream({ transform: (chunk, controller) => {
210
+ this.uncachedRequestBytes += chunk.byteLength;
211
+ controller.enqueue(chunk);
212
+ } })), size);
195
213
  const [handlerStream, cacheStream] = content.tee();
196
214
  const handlerPromise = handler(handlerStream, size);
197
215
  (0, node_stream_consumers.buffer)(cacheStream).then((data) => this.rangesCache.set(key, range ?? {
@@ -1 +1 @@
1
- {"version":3,"file":"download_blob.cjs","names":["FilesCache","SparseCacheFsRanges","SparseCacheFsFile","SparseCache","TaskProcessor","path","Computable","blobKey","DownloadBlobTask","newLocalHandle","nonRecoverableError","OnDemandBlobResourceSnapshot","getSize","newRemoteHandle","parseLocalHandle","isOffByOneError","isLocalBlobHandle","withFileContent","isRemoteBlobHandle","parseRemoteHandle","newLogHandle","getResourceInfoFromLogHandle","fsp","pathToKey","CallersCounter","ChangeSource","Updater","readline","Denque","os","WrongResourceTypeError"],"sources":["../../../src/drivers/download_blob/download_blob.ts"],"sourcesContent":["import type { ComputableCtx, ComputableStableDefined, Watcher } from \"@milaboratories/computable\";\nimport { ChangeSource, Computable } from \"@milaboratories/computable\";\nimport type { SignedResourceId, ResourceType } from \"@milaboratories/pl-client\";\nimport {\n isNotFoundError,\n resourceIdToString,\n stringifyWithResourceId,\n} from \"@milaboratories/pl-client\";\nimport type {\n AnyLogHandle,\n BlobDriver,\n ContentHandler,\n GetContentOptions,\n LocalBlobHandle,\n LocalBlobHandleAndSize,\n ReadyLogHandle,\n RemoteBlobHandle,\n RemoteBlobHandleAndSize,\n StreamingApiResponse,\n} from \"@milaboratories/pl-model-common\";\nimport { type RangeBytes, validateRangeBytes } from \"@milaboratories/pl-model-common\";\nimport type { PlTreeEntry, ResourceInfo, ResourceSnapshot } from \"@milaboratories/pl-tree\";\nimport {\n isPlTreeEntry,\n makeResourceSnapshot,\n treeEntryToResourceInfo,\n} from \"@milaboratories/pl-tree\";\nimport type { MiLogger, Signer } from \"@milaboratories/ts-helpers\";\nimport { CallersCounter, mapGet, TaskProcessor } from \"@milaboratories/ts-helpers\";\nimport Denque from \"denque\";\nimport * as fs from \"fs\";\nimport { randomUUID } from \"node:crypto\";\nimport * as fsp from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport * as readline from \"node:readline/promises\";\nimport { buffer } from \"node:stream/consumers\";\nimport type { ClientDownload } from \"../../clients/download\";\nimport type { ClientLogs } from \"../../clients/logs\";\nimport { withFileContent } from \"../helpers/read_file\";\nimport {\n isLocalBlobHandle,\n newLocalHandle,\n parseLocalHandle,\n} from \"../helpers/download_local_handle\";\nimport {\n isRemoteBlobHandle,\n newRemoteHandle,\n parseRemoteHandle,\n} from \"../helpers/download_remote_handle\";\nimport { Updater, WrongResourceTypeError } from \"../helpers/helpers\";\nimport { getResourceInfoFromLogHandle, newLogHandle } from \"../helpers/logs_handle\";\nimport { getSize, OnDemandBlobResourceSnapshot } from \"../types\";\nimport { blobKey, pathToKey } from \"./blob_key\";\nimport { DownloadBlobTask, nonRecoverableError } from \"./download_blob_task\";\nimport { FilesCache } from \"../helpers/files_cache\";\nimport { SparseCache, SparseCacheFsFile, SparseCacheFsRanges } from \"./sparse_cache/cache\";\nimport { isOffByOneError } from \"../../helpers/download_errors\";\n\nexport type DownloadDriverOps = {\n /**\n * A soft limit of the amount of blob storage, in bytes.\n * Once exceeded, the download driver will start deleting blobs one by one\n * when they become unneeded.\n * */\n cacheSoftSizeBytes: number;\n\n /**\n * A hard limit of the amount of sparse cache, in bytes.\n * Once exceeded, the download driver will start deleting blobs one by one.\n *\n * The sparse cache is used to store ranges of blobs.\n * */\n rangesCacheMaxSizeBytes: number;\n\n /**\n * Max number of concurrent downloads while calculating computable states\n * derived from this driver\n * */\n nConcurrentDownloads: number;\n};\n\n/** DownloadDriver holds a queue of downloading tasks,\n * and notifies every watcher when a file were downloaded. */\nexport class DownloadDriver implements BlobDriver, AsyncDisposable {\n /** Represents a unique key to the path of a blob as a map. */\n private keyToDownload: Map<string, DownloadBlobTask> = new Map();\n\n /** Writes and removes files to a hard drive and holds a counter for every\n * file that should be kept. */\n private cache: FilesCache<DownloadBlobTask>;\n private rangesCache: SparseCache;\n\n /** Downloads files and writes them to the local dir. */\n private downloadQueue: TaskProcessor;\n\n private keyToOnDemand: Map<string, OnDemandBlobHolder> = new Map();\n\n private idToLastLines: Map<string, LastLinesGetter> = new Map();\n private idToProgressLog: Map<string, LastLinesGetter> = new Map();\n\n private readonly saveDir: string;\n\n constructor(\n private readonly logger: MiLogger,\n private readonly clientDownload: ClientDownload,\n private readonly clientLogs: ClientLogs,\n saveDir: string,\n private readonly rangesCacheDir: string,\n private readonly signer: Signer,\n private readonly ops: DownloadDriverOps,\n ) {\n this.cache = new FilesCache(this.ops.cacheSoftSizeBytes);\n\n const fsRanges = new SparseCacheFsRanges(this.logger, this.rangesCacheDir);\n const fsStorage = new SparseCacheFsFile(this.logger, this.rangesCacheDir);\n this.rangesCache = new SparseCache(\n this.logger,\n this.ops.rangesCacheMaxSizeBytes,\n fsRanges,\n fsStorage,\n );\n\n this.downloadQueue = new TaskProcessor(this.logger, ops.nConcurrentDownloads);\n\n this.saveDir = path.resolve(saveDir);\n }\n\n static async init(\n logger: MiLogger,\n clientDownload: ClientDownload,\n clientLogs: ClientLogs,\n saveDir: string,\n rangesCacheDir: string,\n signer: Signer,\n ops: DownloadDriverOps,\n ): Promise<DownloadDriver> {\n const driver = new DownloadDriver(\n logger,\n clientDownload,\n clientLogs,\n saveDir,\n rangesCacheDir,\n signer,\n ops,\n );\n await driver.rangesCache.reset();\n\n return driver;\n }\n\n /** Gets a blob or part of the blob by its resource id or downloads a blob and sets it in a cache. */\n public getDownloadedBlob(\n res: ResourceInfo | PlTreeEntry,\n ctx: ComputableCtx,\n ): LocalBlobHandleAndSize | undefined;\n public getDownloadedBlob(\n res: ResourceInfo | PlTreeEntry,\n ): ComputableStableDefined<LocalBlobHandleAndSize>;\n public getDownloadedBlob(\n res: ResourceInfo | PlTreeEntry,\n ctx?: ComputableCtx,\n ): Computable<LocalBlobHandleAndSize | undefined> | LocalBlobHandleAndSize | undefined {\n if (ctx === undefined) {\n return Computable.make((ctx) => this.getDownloadedBlob(res, ctx));\n }\n\n const rInfo = treeEntryToResourceInfo(res, ctx);\n\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseBlob(rInfo, callerId));\n\n const result = this.getDownloadedBlobNoCtx(ctx.watcher, rInfo as ResourceSnapshot, callerId);\n if (result == undefined) {\n ctx.markUnstable(\"download blob is still undefined\");\n }\n\n return result;\n }\n\n private getDownloadedBlobNoCtx(\n w: Watcher,\n rInfo: ResourceSnapshot,\n callerId: string,\n ): LocalBlobHandleAndSize | undefined {\n validateDownloadableResourceType(\"getDownloadedBlob\", rInfo.type);\n\n // We don't need to request files with wider limits,\n // PFrame's engine does it disk-optimally by itself.\n\n const task = this.getOrSetNewTask(rInfo, callerId);\n task.attach(w, callerId);\n\n const result = task.getBlob();\n if (!result.done) {\n return undefined;\n }\n if (result.result.ok) {\n return result.result.value;\n }\n throw result.result.error;\n }\n\n private getOrSetNewTask(rInfo: ResourceSnapshot, callerId: string): DownloadBlobTask {\n const key = blobKey(rInfo.id);\n\n const inMemoryTask = this.keyToDownload.get(key);\n if (inMemoryTask) {\n return inMemoryTask;\n }\n\n // schedule the blob downloading, then it'll be added to the cache.\n const fPath = path.resolve(this.saveDir, key);\n\n const newTask = new DownloadBlobTask(\n this.logger,\n this.clientDownload,\n rInfo,\n newLocalHandle(fPath, this.signer),\n fPath,\n );\n this.keyToDownload.set(key, newTask);\n\n this.downloadQueue.push({\n fn: () => this.downloadBlob(newTask, callerId),\n recoverableErrorPredicate: (e) => !nonRecoverableError(e),\n });\n\n return newTask;\n }\n\n private async downloadBlob(task: DownloadBlobTask, callerId: string) {\n await task.download();\n const blob = task.getBlob();\n if (blob.done && blob.result.ok) {\n this.cache.addCache(task, callerId);\n }\n }\n\n /** Gets on demand blob. */\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ): Computable<RemoteBlobHandleAndSize>;\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ctx?: undefined,\n fromBytes?: number,\n toBytes?: number,\n ): Computable<RemoteBlobHandleAndSize>;\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ctx: ComputableCtx,\n fromBytes?: number,\n toBytes?: number,\n ): RemoteBlobHandleAndSize;\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ctx?: ComputableCtx,\n ): ComputableStableDefined<RemoteBlobHandleAndSize> | RemoteBlobHandleAndSize | undefined {\n if (ctx === undefined) return Computable.make((ctx) => this.getOnDemandBlob(res, ctx));\n\n const rInfo: OnDemandBlobResourceSnapshot = isPlTreeEntry(res)\n ? makeResourceSnapshot(res, OnDemandBlobResourceSnapshot, ctx)\n : res;\n\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseOnDemandBlob(rInfo.id, callerId));\n\n // note that the watcher is not needed,\n // the handler never changes.\n const result = this.getOnDemandBlobNoCtx(rInfo, callerId);\n\n return result;\n }\n\n private getOnDemandBlobNoCtx(\n info: OnDemandBlobResourceSnapshot,\n callerId: string,\n ): RemoteBlobHandleAndSize {\n validateDownloadableResourceType(\"getOnDemandBlob\", info.type);\n\n let blob = this.keyToOnDemand.get(blobKey(info.id));\n\n if (blob === undefined) {\n blob = new OnDemandBlobHolder(getSize(info), newRemoteHandle(info, this.signer));\n this.keyToOnDemand.set(blobKey(info.id), blob);\n }\n\n blob.attach(callerId);\n\n return blob.getHandle();\n }\n\n /** Gets a path from a handle. */\n public getLocalPath(handle: LocalBlobHandle): string {\n const { path } = parseLocalHandle(handle, this.signer);\n return path;\n }\n\n /** Gets a content of a blob by a handle. */\n public async getContent(\n handle: LocalBlobHandle | RemoteBlobHandle,\n options?: GetContentOptions,\n ): Promise<Uint8Array>;\n /** @deprecated Use {@link getContent} with {@link GetContentOptions} instead */\n public async getContent(\n handle: LocalBlobHandle | RemoteBlobHandle,\n range?: RangeBytes,\n ): Promise<Uint8Array>;\n public async getContent(\n handle: LocalBlobHandle | RemoteBlobHandle,\n optionsOrRange?: GetContentOptions | RangeBytes,\n ): Promise<Uint8Array> {\n let options: GetContentOptions = {};\n if (typeof optionsOrRange === \"object\" && optionsOrRange !== null) {\n if (\"range\" in optionsOrRange) {\n options = optionsOrRange;\n } else {\n const range = optionsOrRange as RangeBytes;\n validateRangeBytes(range, `getContent`);\n options = { range };\n }\n }\n\n return await this.getContentImpl({ handle, options });\n }\n\n /**\n * Same as {@link getContent}, but bypasses the ranges cache entirely (no read, no write).\n * For local handles this is identical to {@link getContent}, since local content never\n * uses the ranges cache.\n */\n public async getContentDirect(\n handle: LocalBlobHandle | RemoteBlobHandle,\n options?: GetContentOptions,\n ): Promise<Uint8Array> {\n return await this.getContentImpl({\n handle,\n options: options ?? {},\n bypassRangesCache: true,\n });\n }\n\n private async getContentImpl({\n handle,\n options,\n bypassRangesCache = false,\n }: {\n handle: LocalBlobHandle | RemoteBlobHandle;\n options: GetContentOptions;\n bypassRangesCache?: boolean;\n }): Promise<Uint8Array> {\n const request = () =>\n this.withContent(handle, {\n ...options,\n bypassRangesCache,\n handler: async (content) => {\n const chunks: Uint8Array[] = [];\n for await (const chunk of content) {\n options.signal?.throwIfAborted();\n chunks.push(chunk);\n }\n return Buffer.concat(chunks);\n },\n });\n\n try {\n return await request();\n } catch (error) {\n if (isOffByOneError(error)) {\n return await request();\n }\n throw error;\n }\n }\n\n /** Gets a content stream of a blob by a handle and calls handler with it. */\n public async withContent<T>(\n handle: LocalBlobHandle | RemoteBlobHandle,\n options: GetContentOptions & {\n handler: ContentHandler<T>;\n bypassRangesCache?: boolean;\n },\n ): Promise<T> {\n const { range, signal, handler, bypassRangesCache } = options;\n\n if (isLocalBlobHandle(handle)) {\n return await withFileContent({ path: this.getLocalPath(handle), range, signal, handler });\n }\n\n if (isRemoteBlobHandle(handle)) {\n const result = parseRemoteHandle(handle, this.signer);\n\n const key = blobKey(result.info.id);\n const filePath = bypassRangesCache\n ? undefined\n : await this.rangesCache.get(key, range ?? { from: 0, to: result.size });\n signal?.throwIfAborted();\n\n if (filePath) return await withFileContent({ path: filePath, range, signal, handler });\n\n return await this.clientDownload.withBlobContent(\n result.info,\n { signal },\n options,\n async (content, size) => {\n if (bypassRangesCache) return await handler(content, size);\n\n const [handlerStream, cacheStream] = content.tee();\n\n const handlerPromise = handler(handlerStream, size);\n const _cachePromise = buffer(cacheStream)\n .then((data) => this.rangesCache.set(key, range ?? { from: 0, to: result.size }, data))\n .catch(() => {\n // Ignore cache errors - they shouldn't affect the main handler result\n // This prevents unhandled promise rejections when the stream fails\n });\n\n return await handlerPromise;\n },\n );\n }\n\n throw new Error(\"Malformed remote handle\");\n }\n\n /**\n * Creates computable that will return blob content once it is downloaded.\n * Uses downloaded blob handle under the hood, so stores corresponding blob in file system.\n */\n public getComputableContent(\n res: ResourceInfo | PlTreeEntry,\n range?: RangeBytes,\n ): ComputableStableDefined<Uint8Array> {\n if (range) {\n validateRangeBytes(range, `getComputableContent`);\n }\n\n return Computable.make((ctx) => this.getDownloadedBlob(res, ctx), {\n postprocessValue: (v) => (v ? this.getContent(v.handle, { range }) : undefined),\n }).withStableType();\n }\n\n /** Returns all logs and schedules a job that reads remain logs.\n * Notifies when a new portion of the log appeared. */\n public getLastLogs(\n res: ResourceInfo | PlTreeEntry,\n lines: number,\n ): Computable<string | undefined>;\n public getLastLogs(\n res: ResourceInfo | PlTreeEntry,\n lines: number,\n ctx: ComputableCtx,\n ): Computable<string | undefined>;\n public getLastLogs(\n res: ResourceInfo | PlTreeEntry,\n lines: number,\n ctx?: ComputableCtx,\n ): Computable<string | undefined> | string | undefined {\n if (ctx == undefined) return Computable.make((ctx) => this.getLastLogs(res, lines, ctx));\n\n const r = treeEntryToResourceInfo(res, ctx);\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseBlob(r, callerId));\n\n const result = this.getLastLogsNoCtx(ctx.watcher, r as ResourceSnapshot, lines, callerId);\n if (result == undefined)\n ctx.markUnstable(\"either a file was not downloaded or logs was not read\");\n\n return result;\n }\n\n private getLastLogsNoCtx(\n w: Watcher,\n rInfo: ResourceSnapshot,\n lines: number,\n callerId: string,\n ): string | undefined {\n validateDownloadableResourceType(\"getLastLogs\", rInfo.type);\n const blob = this.getDownloadedBlobNoCtx(w, rInfo, callerId);\n if (blob == undefined) return undefined;\n\n const { path } = parseLocalHandle(blob.handle, this.signer);\n\n let logGetter = this.idToLastLines.get(blobKey(rInfo.id));\n\n if (logGetter == undefined) {\n const newLogGetter = new LastLinesGetter(path, lines);\n this.idToLastLines.set(blobKey(rInfo.id), newLogGetter);\n logGetter = newLogGetter;\n }\n\n const result = logGetter.getOrSchedule(w);\n if (result.error) throw result.error;\n\n return result.log;\n }\n\n /** Returns a last line that has patternToSearch.\n * Notifies when a new line appeared or EOF reached. */\n public getProgressLog(\n res: ResourceInfo | PlTreeEntry,\n patternToSearch: string,\n ): Computable<string | undefined>;\n public getProgressLog(\n res: ResourceInfo | PlTreeEntry,\n patternToSearch: string,\n ctx: ComputableCtx,\n ): string | undefined;\n public getProgressLog(\n res: ResourceInfo | PlTreeEntry,\n patternToSearch: string,\n ctx?: ComputableCtx,\n ): Computable<string | undefined> | string | undefined {\n if (ctx == undefined)\n return Computable.make((ctx) => this.getProgressLog(res, patternToSearch, ctx));\n\n const r = treeEntryToResourceInfo(res, ctx);\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseBlob(r, callerId));\n\n const result = this.getProgressLogNoCtx(\n ctx.watcher,\n r as ResourceSnapshot,\n patternToSearch,\n callerId,\n );\n if (result === undefined)\n ctx.markUnstable(\"either a file was not downloaded or a progress log was not read\");\n\n return result;\n }\n\n private getProgressLogNoCtx(\n w: Watcher,\n rInfo: ResourceSnapshot,\n patternToSearch: string,\n callerId: string,\n ): string | undefined {\n validateDownloadableResourceType(\"getProgressLog\", rInfo.type);\n\n const blob = this.getDownloadedBlobNoCtx(w, rInfo, callerId);\n if (blob == undefined) return undefined;\n const { path } = parseLocalHandle(blob.handle, this.signer);\n\n let logGetter = this.idToProgressLog.get(blobKey(rInfo.id));\n\n if (logGetter == undefined) {\n const newLogGetter = new LastLinesGetter(path, 1, patternToSearch);\n this.idToProgressLog.set(blobKey(rInfo.id), newLogGetter);\n\n logGetter = newLogGetter;\n }\n\n const result = logGetter.getOrSchedule(w);\n if (result.error) throw result.error;\n\n return result.log;\n }\n\n /** Returns an Id of a smart object, that can read logs directly from\n * the platform. */\n public getLogHandle(res: ResourceInfo | PlTreeEntry): Computable<AnyLogHandle>;\n public getLogHandle(res: ResourceInfo | PlTreeEntry, ctx: ComputableCtx): AnyLogHandle;\n public getLogHandle(\n res: ResourceInfo | PlTreeEntry,\n ctx?: ComputableCtx,\n ): Computable<AnyLogHandle> | AnyLogHandle {\n if (ctx == undefined) return Computable.make((ctx) => this.getLogHandle(res, ctx));\n\n const r = treeEntryToResourceInfo(res, ctx);\n\n return this.getLogHandleNoCtx(r as ResourceSnapshot);\n }\n\n private getLogHandleNoCtx(rInfo: ResourceSnapshot): AnyLogHandle {\n validateDownloadableResourceType(\"getLogHandle\", rInfo.type);\n return newLogHandle(false, rInfo);\n }\n\n public async lastLines(\n handle: ReadyLogHandle,\n lineCount: number,\n offsetBytes?: number, // if 0n, then start from the end.\n searchStr?: string,\n ): Promise<StreamingApiResponse> {\n const resp = await this.clientLogs.lastLines(\n getResourceInfoFromLogHandle(handle),\n lineCount,\n BigInt(offsetBytes ?? 0),\n searchStr,\n );\n\n return {\n live: false,\n shouldUpdateHandle: false,\n data: resp.data,\n size: Number(resp.size),\n newOffset: Number(resp.newOffset),\n };\n }\n\n public async readText(\n handle: ReadyLogHandle,\n lineCount: number,\n offsetBytes?: number,\n searchStr?: string,\n ): Promise<StreamingApiResponse> {\n const resp = await this.clientLogs.readText(\n getResourceInfoFromLogHandle(handle),\n lineCount,\n BigInt(offsetBytes ?? 0),\n searchStr,\n );\n\n return {\n live: false,\n shouldUpdateHandle: false,\n data: resp.data,\n size: Number(resp.size),\n newOffset: Number(resp.newOffset),\n };\n }\n\n private async releaseBlob(rInfo: ResourceInfo, callerId: string) {\n const task = this.keyToDownload.get(blobKey(rInfo.id));\n if (task == undefined) {\n return;\n }\n\n if (this.cache.existsFile(blobKey(rInfo.id))) {\n const toDelete = this.cache.removeFile(blobKey(rInfo.id), callerId);\n\n await Promise.all(\n toDelete.map(async (cachedFile) => {\n await fsp.rm(cachedFile.path);\n\n this.cache.removeCache(cachedFile);\n\n this.removeTask(\n mapGet(this.keyToDownload, pathToKey(cachedFile.path)),\n `the task ${stringifyWithResourceId(cachedFile)} was removed` +\n `from cache along with ${stringifyWithResourceId(toDelete.map((d) => d.path))}`,\n );\n }),\n );\n } else {\n // The task is still in a downloading queue.\n const deleted = task.counter.dec(callerId);\n if (deleted) {\n this.removeTask(\n task,\n `the task ${stringifyWithResourceId(task.info())} was removed from cache`,\n );\n }\n }\n }\n\n private removeTask(task: DownloadBlobTask, reason: string) {\n task.abort(reason);\n task.change.markChanged(`download task for ${task.path} removed: ${reason}`);\n this.keyToDownload.delete(pathToKey(task.path));\n this.idToLastLines.delete(blobKey(task.rInfo.id));\n this.idToProgressLog.delete(blobKey(task.rInfo.id));\n }\n\n private async releaseOnDemandBlob(blobId: SignedResourceId, callerId: string) {\n const deleted = this.keyToOnDemand.get(blobKey(blobId))?.release(callerId) ?? false;\n if (deleted) this.keyToOnDemand.delete(blobKey(blobId));\n }\n\n /** Removes all files from a hard drive. */\n async releaseAll() {\n this.downloadQueue.stop();\n\n this.keyToDownload.forEach((task, key) => {\n this.keyToDownload.delete(key);\n task.change.markChanged(`task ${resourceIdToString(task.rInfo.id)} released`);\n });\n }\n\n async dispose(): Promise<void> {\n await this.rangesCache.dispose();\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n await this.dispose();\n }\n}\n\n/** Keeps a counter to the on demand handle. */\nclass OnDemandBlobHolder {\n private readonly counter = new CallersCounter();\n\n constructor(\n private readonly size: number,\n private readonly handle: RemoteBlobHandle,\n ) {}\n\n public getHandle(): RemoteBlobHandleAndSize {\n return { handle: this.handle, size: this.size };\n }\n\n public attach(callerId: string) {\n this.counter.inc(callerId);\n }\n\n public release(callerId: string): boolean {\n return this.counter.dec(callerId);\n }\n}\n\nclass LastLinesGetter {\n private updater: Updater;\n private log: string | undefined;\n private readonly change: ChangeSource = new ChangeSource();\n private error: any | undefined = undefined;\n\n constructor(\n private readonly path: string,\n private readonly lines: number,\n private readonly patternToSearch?: string,\n ) {\n this.updater = new Updater(async () => this.update());\n }\n\n getOrSchedule(w: Watcher): {\n log: string | undefined;\n error?: any | undefined;\n } {\n this.change.attachWatcher(w);\n\n this.updater.schedule();\n\n return {\n log: this.log,\n error: this.error,\n };\n }\n\n async update(): Promise<void> {\n try {\n const newLogs = await getLastLines(this.path, this.lines, this.patternToSearch);\n\n if (this.log != newLogs) this.change.markChanged(`logs for ${this.path} updated`);\n this.log = newLogs;\n } catch (e: any) {\n if (isNotFoundError(e)) {\n // No resource\n this.log = \"\";\n this.error = e;\n this.change.markChanged(`log update for ${this.path} failed, resource not found`);\n return;\n }\n\n throw e;\n }\n }\n}\n\n/** Gets last lines from a file by reading the file from the top and keeping\n * last N lines in a window queue. */\nasync function getLastLines(\n fPath: string,\n nLines: number,\n patternToSearch?: string,\n): Promise<string> {\n let inStream: fs.ReadStream | undefined;\n let rl: readline.Interface | undefined;\n\n try {\n inStream = fs.createReadStream(fPath);\n rl = readline.createInterface({ input: inStream, crlfDelay: Infinity });\n\n const lines = new Denque();\n\n for await (const line of rl) {\n if (patternToSearch != undefined && !line.includes(patternToSearch)) continue;\n\n lines.push(line);\n if (lines.length > nLines) {\n lines.shift();\n }\n }\n\n // last EOL is for keeping backward compat with platforma implementation.\n return lines.toArray().join(os.EOL) + os.EOL;\n } finally {\n // Cleanup resources in finally block to ensure they're always cleaned up\n try {\n if (rl) {\n rl.close();\n }\n } catch (cleanupError) {\n console.error(\"Error closing readline interface:\", cleanupError);\n }\n\n try {\n if (inStream && !inStream.destroyed) {\n inStream.destroy();\n }\n } catch (cleanupError) {\n console.error(\"Error destroying read stream:\", cleanupError);\n }\n }\n}\n\nfunction validateDownloadableResourceType(methodName: string, rType: ResourceType) {\n if (!rType.name.startsWith(\"Blob/\")) {\n let message = `${methodName}: wrong resource type: ${rType.name}, expected: a resource of type that starts with 'Blob/'.`;\n if (rType.name == \"Blob\")\n message += ` If it's called from workflow, should a file be exported with 'file.exportFile' function?`;\n\n throw new WrongResourceTypeError(message);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"download_blob.cjs","names":["FilesCache","SparseCacheFsRanges","SparseCacheFsFile","SparseCache","TaskProcessor","path","Computable","blobKey","DownloadBlobTask","newLocalHandle","nonRecoverableError","OnDemandBlobResourceSnapshot","getSize","newRemoteHandle","parseLocalHandle","isOffByOneError","isLocalBlobHandle","withFileContent","isRemoteBlobHandle","parseRemoteHandle","newLogHandle","getResourceInfoFromLogHandle","fsp","pathToKey","CallersCounter","ChangeSource","Updater","readline","Denque","os","WrongResourceTypeError"],"sources":["../../../src/drivers/download_blob/download_blob.ts"],"sourcesContent":["import type { ComputableCtx, ComputableStableDefined, Watcher } from \"@milaboratories/computable\";\nimport { ChangeSource, Computable } from \"@milaboratories/computable\";\nimport type { SignedResourceId, ResourceType } from \"@milaboratories/pl-client\";\nimport {\n isNotFoundError,\n resourceIdToString,\n stringifyWithResourceId,\n} from \"@milaboratories/pl-client\";\nimport type {\n AnyLogHandle,\n BlobDriver,\n BlobDriverMetrics,\n ContentHandler,\n GetContentOptions,\n LocalBlobHandle,\n LocalBlobHandleAndSize,\n ReadyLogHandle,\n RemoteBlobHandle,\n RemoteBlobHandleAndSize,\n StreamingApiResponse,\n} from \"@milaboratories/pl-model-common\";\nimport { type RangeBytes, validateRangeBytes } from \"@milaboratories/pl-model-common\";\nimport type { PlTreeEntry, ResourceInfo, ResourceSnapshot } from \"@milaboratories/pl-tree\";\nimport {\n isPlTreeEntry,\n makeResourceSnapshot,\n treeEntryToResourceInfo,\n} from \"@milaboratories/pl-tree\";\nimport type { MiLogger, Signer } from \"@milaboratories/ts-helpers\";\nimport { CallersCounter, mapGet, TaskProcessor } from \"@milaboratories/ts-helpers\";\nimport Denque from \"denque\";\nimport * as fs from \"fs\";\nimport { randomUUID } from \"node:crypto\";\nimport * as fsp from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport * as readline from \"node:readline/promises\";\nimport { buffer } from \"node:stream/consumers\";\nimport type { ClientDownload } from \"../../clients/download\";\nimport type { ClientLogs } from \"../../clients/logs\";\nimport { withFileContent } from \"../helpers/read_file\";\nimport {\n isLocalBlobHandle,\n newLocalHandle,\n parseLocalHandle,\n} from \"../helpers/download_local_handle\";\nimport {\n isRemoteBlobHandle,\n newRemoteHandle,\n parseRemoteHandle,\n} from \"../helpers/download_remote_handle\";\nimport { Updater, WrongResourceTypeError } from \"../helpers/helpers\";\nimport { getResourceInfoFromLogHandle, newLogHandle } from \"../helpers/logs_handle\";\nimport { getSize, OnDemandBlobResourceSnapshot } from \"../types\";\nimport { blobKey, pathToKey } from \"./blob_key\";\nimport { DownloadBlobTask, nonRecoverableError } from \"./download_blob_task\";\nimport { FilesCache } from \"../helpers/files_cache\";\nimport { SparseCache, SparseCacheFsFile, SparseCacheFsRanges } from \"./sparse_cache/cache\";\nimport { isOffByOneError } from \"../../helpers/download_errors\";\n\nexport type DownloadDriverOps = {\n /**\n * A soft limit of the amount of blob storage, in bytes.\n * Once exceeded, the download driver will start deleting blobs one by one\n * when they become unneeded.\n * */\n cacheSoftSizeBytes: number;\n\n /**\n * A hard limit of the amount of sparse cache, in bytes.\n * Once exceeded, the download driver will start deleting blobs one by one.\n *\n * The sparse cache is used to store ranges of blobs.\n * */\n rangesCacheMaxSizeBytes: number;\n\n /**\n * Max number of concurrent downloads while calculating computable states\n * derived from this driver\n * */\n nConcurrentDownloads: number;\n};\n\n/** DownloadDriver holds a queue of downloading tasks,\n * and notifies every watcher when a file were downloaded. */\nexport class DownloadDriver implements BlobDriver, AsyncDisposable {\n /** Represents a unique key to the path of a blob as a map. */\n private keyToDownload: Map<string, DownloadBlobTask> = new Map();\n\n /** Writes and removes files to a hard drive and holds a counter for every\n * file that should be kept. */\n private cache: FilesCache<DownloadBlobTask>;\n private rangesCache: SparseCache;\n\n /** Downloads files and writes them to the local dir. */\n private downloadQueue: TaskProcessor;\n\n private keyToOnDemand: Map<string, OnDemandBlobHolder> = new Map();\n\n private idToLastLines: Map<string, LastLinesGetter> = new Map();\n private idToProgressLog: Map<string, LastLinesGetter> = new Map();\n\n private readonly saveDir: string;\n\n /** Downloads that bypassed the ranges cache; counted when issued. */\n private uncachedRequests = 0;\n private uncachedRequestBytes = 0;\n\n constructor(\n private readonly logger: MiLogger,\n private readonly clientDownload: ClientDownload,\n private readonly clientLogs: ClientLogs,\n saveDir: string,\n private readonly rangesCacheDir: string,\n private readonly signer: Signer,\n private readonly ops: DownloadDriverOps,\n ) {\n this.cache = new FilesCache(this.ops.cacheSoftSizeBytes);\n\n const fsRanges = new SparseCacheFsRanges(this.logger, this.rangesCacheDir);\n const fsStorage = new SparseCacheFsFile(this.logger, this.rangesCacheDir);\n this.rangesCache = new SparseCache(\n this.logger,\n this.ops.rangesCacheMaxSizeBytes,\n fsRanges,\n fsStorage,\n );\n\n this.downloadQueue = new TaskProcessor(this.logger, ops.nConcurrentDownloads);\n\n this.saveDir = path.resolve(saveDir);\n }\n\n static async init(\n logger: MiLogger,\n clientDownload: ClientDownload,\n clientLogs: ClientLogs,\n saveDir: string,\n rangesCacheDir: string,\n signer: Signer,\n ops: DownloadDriverOps,\n ): Promise<DownloadDriver> {\n const driver = new DownloadDriver(\n logger,\n clientDownload,\n clientLogs,\n saveDir,\n rangesCacheDir,\n signer,\n ops,\n );\n await driver.rangesCache.reset();\n\n return driver;\n }\n\n /** Gets a blob or part of the blob by its resource id or downloads a blob and sets it in a cache. */\n public getDownloadedBlob(\n res: ResourceInfo | PlTreeEntry,\n ctx: ComputableCtx,\n ): LocalBlobHandleAndSize | undefined;\n public getDownloadedBlob(\n res: ResourceInfo | PlTreeEntry,\n ): ComputableStableDefined<LocalBlobHandleAndSize>;\n public getDownloadedBlob(\n res: ResourceInfo | PlTreeEntry,\n ctx?: ComputableCtx,\n ): Computable<LocalBlobHandleAndSize | undefined> | LocalBlobHandleAndSize | undefined {\n if (ctx === undefined) {\n return Computable.make((ctx) => this.getDownloadedBlob(res, ctx));\n }\n\n const rInfo = treeEntryToResourceInfo(res, ctx);\n\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseBlob(rInfo, callerId));\n\n const result = this.getDownloadedBlobNoCtx(ctx.watcher, rInfo as ResourceSnapshot, callerId);\n if (result == undefined) {\n ctx.markUnstable(\"download blob is still undefined\");\n }\n\n return result;\n }\n\n private getDownloadedBlobNoCtx(\n w: Watcher,\n rInfo: ResourceSnapshot,\n callerId: string,\n ): LocalBlobHandleAndSize | undefined {\n validateDownloadableResourceType(\"getDownloadedBlob\", rInfo.type);\n\n // We don't need to request files with wider limits,\n // PFrame's engine does it disk-optimally by itself.\n\n const task = this.getOrSetNewTask(rInfo, callerId);\n task.attach(w, callerId);\n\n const result = task.getBlob();\n if (!result.done) {\n return undefined;\n }\n if (result.result.ok) {\n return result.result.value;\n }\n throw result.result.error;\n }\n\n private getOrSetNewTask(rInfo: ResourceSnapshot, callerId: string): DownloadBlobTask {\n const key = blobKey(rInfo.id);\n\n const inMemoryTask = this.keyToDownload.get(key);\n if (inMemoryTask) {\n return inMemoryTask;\n }\n\n // schedule the blob downloading, then it'll be added to the cache.\n const fPath = path.resolve(this.saveDir, key);\n\n const newTask = new DownloadBlobTask(\n this.logger,\n this.clientDownload,\n rInfo,\n newLocalHandle(fPath, this.signer),\n fPath,\n );\n this.keyToDownload.set(key, newTask);\n\n this.downloadQueue.push({\n fn: () => this.downloadBlob(newTask, callerId),\n recoverableErrorPredicate: (e) => !nonRecoverableError(e),\n });\n\n return newTask;\n }\n\n private async downloadBlob(task: DownloadBlobTask, callerId: string) {\n await task.download();\n const blob = task.getBlob();\n if (blob.done && blob.result.ok) {\n this.cache.addCache(task, callerId);\n }\n }\n\n /** Gets on demand blob. */\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ): Computable<RemoteBlobHandleAndSize>;\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ctx?: undefined,\n fromBytes?: number,\n toBytes?: number,\n ): Computable<RemoteBlobHandleAndSize>;\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ctx: ComputableCtx,\n fromBytes?: number,\n toBytes?: number,\n ): RemoteBlobHandleAndSize;\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ctx?: ComputableCtx,\n ): ComputableStableDefined<RemoteBlobHandleAndSize> | RemoteBlobHandleAndSize | undefined {\n if (ctx === undefined) return Computable.make((ctx) => this.getOnDemandBlob(res, ctx));\n\n const rInfo: OnDemandBlobResourceSnapshot = isPlTreeEntry(res)\n ? makeResourceSnapshot(res, OnDemandBlobResourceSnapshot, ctx)\n : res;\n\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseOnDemandBlob(rInfo.id, callerId));\n\n // note that the watcher is not needed,\n // the handler never changes.\n const result = this.getOnDemandBlobNoCtx(rInfo, callerId);\n\n return result;\n }\n\n private getOnDemandBlobNoCtx(\n info: OnDemandBlobResourceSnapshot,\n callerId: string,\n ): RemoteBlobHandleAndSize {\n validateDownloadableResourceType(\"getOnDemandBlob\", info.type);\n\n let blob = this.keyToOnDemand.get(blobKey(info.id));\n\n if (blob === undefined) {\n blob = new OnDemandBlobHolder(getSize(info), newRemoteHandle(info, this.signer));\n this.keyToOnDemand.set(blobKey(info.id), blob);\n }\n\n blob.attach(callerId);\n\n return blob.getHandle();\n }\n\n /** Gets a path from a handle. */\n public getLocalPath(handle: LocalBlobHandle): string {\n const { path } = parseLocalHandle(handle, this.signer);\n return path;\n }\n\n /** Gets a content of a blob by a handle. */\n public async getContent(\n handle: LocalBlobHandle | RemoteBlobHandle,\n options?: GetContentOptions,\n ): Promise<Uint8Array>;\n /** @deprecated Use {@link getContent} with {@link GetContentOptions} instead */\n public async getContent(\n handle: LocalBlobHandle | RemoteBlobHandle,\n range?: RangeBytes,\n ): Promise<Uint8Array>;\n public async getContent(\n handle: LocalBlobHandle | RemoteBlobHandle,\n optionsOrRange?: GetContentOptions | RangeBytes,\n ): Promise<Uint8Array> {\n let options: GetContentOptions = {};\n if (typeof optionsOrRange === \"object\" && optionsOrRange !== null) {\n if (\"range\" in optionsOrRange) {\n options = optionsOrRange;\n } else {\n const range = optionsOrRange as RangeBytes;\n validateRangeBytes(range, `getContent`);\n options = { range };\n }\n }\n\n return await this.getContentImpl({ handle, options });\n }\n\n /**\n * Same as {@link getContent}, but bypasses the ranges cache entirely (no read, no write).\n * For local handles this is identical to {@link getContent}, since local content never\n * uses the ranges cache.\n */\n public async getContentDirect(\n handle: LocalBlobHandle | RemoteBlobHandle,\n options?: GetContentOptions,\n ): Promise<Uint8Array> {\n return await this.getContentImpl({\n handle,\n options: options ?? {},\n bypassRangesCache: true,\n });\n }\n\n /**\n * Operational metrics for a monitoring panel. Serv cache metrics are reported separately\n * (different owner) — the panel composes both.\n */\n public getMetrics(): BlobDriverMetrics {\n return {\n uncachedRequests: this.uncachedRequests,\n uncachedRequestBytes: this.uncachedRequestBytes,\n ...this.clientDownload.metrics(),\n };\n }\n\n private async getContentImpl({\n handle,\n options,\n bypassRangesCache = false,\n }: {\n handle: LocalBlobHandle | RemoteBlobHandle;\n options: GetContentOptions;\n bypassRangesCache?: boolean;\n }): Promise<Uint8Array> {\n const request = () =>\n this.withContent(handle, {\n ...options,\n bypassRangesCache,\n handler: async (content) => {\n const chunks: Uint8Array[] = [];\n for await (const chunk of content) {\n options.signal?.throwIfAborted();\n chunks.push(chunk);\n }\n return Buffer.concat(chunks);\n },\n });\n\n try {\n return await request();\n } catch (error) {\n if (isOffByOneError(error)) {\n return await request();\n }\n throw error;\n }\n }\n\n /** Gets a content stream of a blob by a handle and calls handler with it. */\n public async withContent<T>(\n handle: LocalBlobHandle | RemoteBlobHandle,\n options: GetContentOptions & {\n handler: ContentHandler<T>;\n bypassRangesCache?: boolean;\n },\n ): Promise<T> {\n const { range, signal, handler, bypassRangesCache } = options;\n\n if (isLocalBlobHandle(handle)) {\n return await withFileContent({ path: this.getLocalPath(handle), range, signal, handler });\n }\n\n if (isRemoteBlobHandle(handle)) {\n const result = parseRemoteHandle(handle, this.signer);\n\n const key = blobKey(result.info.id);\n const filePath = bypassRangesCache\n ? undefined\n : await this.rangesCache.get(key, range ?? { from: 0, to: result.size });\n signal?.throwIfAborted();\n\n if (filePath) return await withFileContent({ path: filePath, range, signal, handler });\n\n if (bypassRangesCache) this.uncachedRequests++;\n\n return await this.clientDownload.withBlobContent(\n result.info,\n { signal },\n options,\n async (content, size) => {\n if (bypassRangesCache) {\n const counted = content.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform: (chunk, controller) => {\n this.uncachedRequestBytes += chunk.byteLength;\n controller.enqueue(chunk);\n },\n }),\n );\n return await handler(counted, size);\n }\n\n const [handlerStream, cacheStream] = content.tee();\n\n const handlerPromise = handler(handlerStream, size);\n const _cachePromise = buffer(cacheStream)\n .then((data) => this.rangesCache.set(key, range ?? { from: 0, to: result.size }, data))\n .catch(() => {\n // Ignore cache errors - they shouldn't affect the main handler result\n // This prevents unhandled promise rejections when the stream fails\n });\n\n return await handlerPromise;\n },\n );\n }\n\n throw new Error(\"Malformed remote handle\");\n }\n\n /**\n * Creates computable that will return blob content once it is downloaded.\n * Uses downloaded blob handle under the hood, so stores corresponding blob in file system.\n */\n public getComputableContent(\n res: ResourceInfo | PlTreeEntry,\n range?: RangeBytes,\n ): ComputableStableDefined<Uint8Array> {\n if (range) {\n validateRangeBytes(range, `getComputableContent`);\n }\n\n return Computable.make((ctx) => this.getDownloadedBlob(res, ctx), {\n postprocessValue: (v) => (v ? this.getContent(v.handle, { range }) : undefined),\n }).withStableType();\n }\n\n /** Returns all logs and schedules a job that reads remain logs.\n * Notifies when a new portion of the log appeared. */\n public getLastLogs(\n res: ResourceInfo | PlTreeEntry,\n lines: number,\n ): Computable<string | undefined>;\n public getLastLogs(\n res: ResourceInfo | PlTreeEntry,\n lines: number,\n ctx: ComputableCtx,\n ): Computable<string | undefined>;\n public getLastLogs(\n res: ResourceInfo | PlTreeEntry,\n lines: number,\n ctx?: ComputableCtx,\n ): Computable<string | undefined> | string | undefined {\n if (ctx == undefined) return Computable.make((ctx) => this.getLastLogs(res, lines, ctx));\n\n const r = treeEntryToResourceInfo(res, ctx);\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseBlob(r, callerId));\n\n const result = this.getLastLogsNoCtx(ctx.watcher, r as ResourceSnapshot, lines, callerId);\n if (result == undefined)\n ctx.markUnstable(\"either a file was not downloaded or logs was not read\");\n\n return result;\n }\n\n private getLastLogsNoCtx(\n w: Watcher,\n rInfo: ResourceSnapshot,\n lines: number,\n callerId: string,\n ): string | undefined {\n validateDownloadableResourceType(\"getLastLogs\", rInfo.type);\n const blob = this.getDownloadedBlobNoCtx(w, rInfo, callerId);\n if (blob == undefined) return undefined;\n\n const { path } = parseLocalHandle(blob.handle, this.signer);\n\n let logGetter = this.idToLastLines.get(blobKey(rInfo.id));\n\n if (logGetter == undefined) {\n const newLogGetter = new LastLinesGetter(path, lines);\n this.idToLastLines.set(blobKey(rInfo.id), newLogGetter);\n logGetter = newLogGetter;\n }\n\n const result = logGetter.getOrSchedule(w);\n if (result.error) throw result.error;\n\n return result.log;\n }\n\n /** Returns a last line that has patternToSearch.\n * Notifies when a new line appeared or EOF reached. */\n public getProgressLog(\n res: ResourceInfo | PlTreeEntry,\n patternToSearch: string,\n ): Computable<string | undefined>;\n public getProgressLog(\n res: ResourceInfo | PlTreeEntry,\n patternToSearch: string,\n ctx: ComputableCtx,\n ): string | undefined;\n public getProgressLog(\n res: ResourceInfo | PlTreeEntry,\n patternToSearch: string,\n ctx?: ComputableCtx,\n ): Computable<string | undefined> | string | undefined {\n if (ctx == undefined)\n return Computable.make((ctx) => this.getProgressLog(res, patternToSearch, ctx));\n\n const r = treeEntryToResourceInfo(res, ctx);\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseBlob(r, callerId));\n\n const result = this.getProgressLogNoCtx(\n ctx.watcher,\n r as ResourceSnapshot,\n patternToSearch,\n callerId,\n );\n if (result === undefined)\n ctx.markUnstable(\"either a file was not downloaded or a progress log was not read\");\n\n return result;\n }\n\n private getProgressLogNoCtx(\n w: Watcher,\n rInfo: ResourceSnapshot,\n patternToSearch: string,\n callerId: string,\n ): string | undefined {\n validateDownloadableResourceType(\"getProgressLog\", rInfo.type);\n\n const blob = this.getDownloadedBlobNoCtx(w, rInfo, callerId);\n if (blob == undefined) return undefined;\n const { path } = parseLocalHandle(blob.handle, this.signer);\n\n let logGetter = this.idToProgressLog.get(blobKey(rInfo.id));\n\n if (logGetter == undefined) {\n const newLogGetter = new LastLinesGetter(path, 1, patternToSearch);\n this.idToProgressLog.set(blobKey(rInfo.id), newLogGetter);\n\n logGetter = newLogGetter;\n }\n\n const result = logGetter.getOrSchedule(w);\n if (result.error) throw result.error;\n\n return result.log;\n }\n\n /** Returns an Id of a smart object, that can read logs directly from\n * the platform. */\n public getLogHandle(res: ResourceInfo | PlTreeEntry): Computable<AnyLogHandle>;\n public getLogHandle(res: ResourceInfo | PlTreeEntry, ctx: ComputableCtx): AnyLogHandle;\n public getLogHandle(\n res: ResourceInfo | PlTreeEntry,\n ctx?: ComputableCtx,\n ): Computable<AnyLogHandle> | AnyLogHandle {\n if (ctx == undefined) return Computable.make((ctx) => this.getLogHandle(res, ctx));\n\n const r = treeEntryToResourceInfo(res, ctx);\n\n return this.getLogHandleNoCtx(r as ResourceSnapshot);\n }\n\n private getLogHandleNoCtx(rInfo: ResourceSnapshot): AnyLogHandle {\n validateDownloadableResourceType(\"getLogHandle\", rInfo.type);\n return newLogHandle(false, rInfo);\n }\n\n public async lastLines(\n handle: ReadyLogHandle,\n lineCount: number,\n offsetBytes?: number, // if 0n, then start from the end.\n searchStr?: string,\n ): Promise<StreamingApiResponse> {\n const resp = await this.clientLogs.lastLines(\n getResourceInfoFromLogHandle(handle),\n lineCount,\n BigInt(offsetBytes ?? 0),\n searchStr,\n );\n\n return {\n live: false,\n shouldUpdateHandle: false,\n data: resp.data,\n size: Number(resp.size),\n newOffset: Number(resp.newOffset),\n };\n }\n\n public async readText(\n handle: ReadyLogHandle,\n lineCount: number,\n offsetBytes?: number,\n searchStr?: string,\n ): Promise<StreamingApiResponse> {\n const resp = await this.clientLogs.readText(\n getResourceInfoFromLogHandle(handle),\n lineCount,\n BigInt(offsetBytes ?? 0),\n searchStr,\n );\n\n return {\n live: false,\n shouldUpdateHandle: false,\n data: resp.data,\n size: Number(resp.size),\n newOffset: Number(resp.newOffset),\n };\n }\n\n private async releaseBlob(rInfo: ResourceInfo, callerId: string) {\n const task = this.keyToDownload.get(blobKey(rInfo.id));\n if (task == undefined) {\n return;\n }\n\n if (this.cache.existsFile(blobKey(rInfo.id))) {\n const toDelete = this.cache.removeFile(blobKey(rInfo.id), callerId);\n\n await Promise.all(\n toDelete.map(async (cachedFile) => {\n await fsp.rm(cachedFile.path);\n\n this.cache.removeCache(cachedFile);\n\n this.removeTask(\n mapGet(this.keyToDownload, pathToKey(cachedFile.path)),\n `the task ${stringifyWithResourceId(cachedFile)} was removed` +\n `from cache along with ${stringifyWithResourceId(toDelete.map((d) => d.path))}`,\n );\n }),\n );\n } else {\n // The task is still in a downloading queue.\n const deleted = task.counter.dec(callerId);\n if (deleted) {\n this.removeTask(\n task,\n `the task ${stringifyWithResourceId(task.info())} was removed from cache`,\n );\n }\n }\n }\n\n private removeTask(task: DownloadBlobTask, reason: string) {\n task.abort(reason);\n task.change.markChanged(`download task for ${task.path} removed: ${reason}`);\n this.keyToDownload.delete(pathToKey(task.path));\n this.idToLastLines.delete(blobKey(task.rInfo.id));\n this.idToProgressLog.delete(blobKey(task.rInfo.id));\n }\n\n private async releaseOnDemandBlob(blobId: SignedResourceId, callerId: string) {\n const deleted = this.keyToOnDemand.get(blobKey(blobId))?.release(callerId) ?? false;\n if (deleted) this.keyToOnDemand.delete(blobKey(blobId));\n }\n\n /** Removes all files from a hard drive. */\n async releaseAll() {\n this.downloadQueue.stop();\n\n this.keyToDownload.forEach((task, key) => {\n this.keyToDownload.delete(key);\n task.change.markChanged(`task ${resourceIdToString(task.rInfo.id)} released`);\n });\n }\n\n async dispose(): Promise<void> {\n await this.rangesCache.dispose();\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n await this.dispose();\n }\n}\n\n/** Keeps a counter to the on demand handle. */\nclass OnDemandBlobHolder {\n private readonly counter = new CallersCounter();\n\n constructor(\n private readonly size: number,\n private readonly handle: RemoteBlobHandle,\n ) {}\n\n public getHandle(): RemoteBlobHandleAndSize {\n return { handle: this.handle, size: this.size };\n }\n\n public attach(callerId: string) {\n this.counter.inc(callerId);\n }\n\n public release(callerId: string): boolean {\n return this.counter.dec(callerId);\n }\n}\n\nclass LastLinesGetter {\n private updater: Updater;\n private log: string | undefined;\n private readonly change: ChangeSource = new ChangeSource();\n private error: any | undefined = undefined;\n\n constructor(\n private readonly path: string,\n private readonly lines: number,\n private readonly patternToSearch?: string,\n ) {\n this.updater = new Updater(async () => this.update());\n }\n\n getOrSchedule(w: Watcher): {\n log: string | undefined;\n error?: any | undefined;\n } {\n this.change.attachWatcher(w);\n\n this.updater.schedule();\n\n return {\n log: this.log,\n error: this.error,\n };\n }\n\n async update(): Promise<void> {\n try {\n const newLogs = await getLastLines(this.path, this.lines, this.patternToSearch);\n\n if (this.log != newLogs) this.change.markChanged(`logs for ${this.path} updated`);\n this.log = newLogs;\n } catch (e: any) {\n if (isNotFoundError(e)) {\n // No resource\n this.log = \"\";\n this.error = e;\n this.change.markChanged(`log update for ${this.path} failed, resource not found`);\n return;\n }\n\n throw e;\n }\n }\n}\n\n/** Gets last lines from a file by reading the file from the top and keeping\n * last N lines in a window queue. */\nasync function getLastLines(\n fPath: string,\n nLines: number,\n patternToSearch?: string,\n): Promise<string> {\n let inStream: fs.ReadStream | undefined;\n let rl: readline.Interface | undefined;\n\n try {\n inStream = fs.createReadStream(fPath);\n rl = readline.createInterface({ input: inStream, crlfDelay: Infinity });\n\n const lines = new Denque();\n\n for await (const line of rl) {\n if (patternToSearch != undefined && !line.includes(patternToSearch)) continue;\n\n lines.push(line);\n if (lines.length > nLines) {\n lines.shift();\n }\n }\n\n // last EOL is for keeping backward compat with platforma implementation.\n return lines.toArray().join(os.EOL) + os.EOL;\n } finally {\n // Cleanup resources in finally block to ensure they're always cleaned up\n try {\n if (rl) {\n rl.close();\n }\n } catch (cleanupError) {\n console.error(\"Error closing readline interface:\", cleanupError);\n }\n\n try {\n if (inStream && !inStream.destroyed) {\n inStream.destroy();\n }\n } catch (cleanupError) {\n console.error(\"Error destroying read stream:\", cleanupError);\n }\n }\n}\n\nfunction validateDownloadableResourceType(methodName: string, rType: ResourceType) {\n if (!rType.name.startsWith(\"Blob/\")) {\n let message = `${methodName}: wrong resource type: ${rType.name}, expected: a resource of type that starts with 'Blob/'.`;\n if (rType.name == \"Blob\")\n message += ` If it's called from workflow, should a file be exported with 'file.exportFile' function?`;\n\n throw new WrongResourceTypeError(message);\n }\n}\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import { ClientDownload } from "../../clients/download.js";
3
3
  import { ClientLogs } from "../../clients/logs.js";
4
4
  import { MiLogger, Signer } from "@milaboratories/ts-helpers";
5
5
  import { Computable, ComputableCtx, ComputableStableDefined } from "@milaboratories/computable";
6
- import { AnyLogHandle, BlobDriver, ContentHandler, GetContentOptions, LocalBlobHandle, LocalBlobHandleAndSize, RangeBytes, ReadyLogHandle, RemoteBlobHandle, RemoteBlobHandleAndSize, StreamingApiResponse } from "@milaboratories/pl-model-common";
6
+ import { AnyLogHandle, BlobDriver, BlobDriverMetrics, ContentHandler, GetContentOptions, LocalBlobHandle, LocalBlobHandleAndSize, RangeBytes, ReadyLogHandle, RemoteBlobHandle, RemoteBlobHandleAndSize, StreamingApiResponse } from "@milaboratories/pl-model-common";
7
7
  import { PlTreeEntry, ResourceInfo } from "@milaboratories/pl-tree";
8
8
 
9
9
  //#region src/drivers/download_blob/download_blob.d.ts
@@ -48,6 +48,9 @@ declare class DownloadDriver implements BlobDriver, AsyncDisposable {
48
48
  private idToLastLines;
49
49
  private idToProgressLog;
50
50
  private readonly saveDir;
51
+ /** Downloads that bypassed the ranges cache; counted when issued. */
52
+ private uncachedRequests;
53
+ private uncachedRequestBytes;
51
54
  constructor(logger: MiLogger, clientDownload: ClientDownload, clientLogs: ClientLogs, saveDir: string, rangesCacheDir: string, signer: Signer, ops: DownloadDriverOps);
52
55
  static init(logger: MiLogger, clientDownload: ClientDownload, clientLogs: ClientLogs, saveDir: string, rangesCacheDir: string, signer: Signer, ops: DownloadDriverOps): Promise<DownloadDriver>;
53
56
  /** Gets a blob or part of the blob by its resource id or downloads a blob and sets it in a cache. */
@@ -73,6 +76,11 @@ declare class DownloadDriver implements BlobDriver, AsyncDisposable {
73
76
  * uses the ranges cache.
74
77
  */
75
78
  getContentDirect(handle: LocalBlobHandle | RemoteBlobHandle, options?: GetContentOptions): Promise<Uint8Array>;
79
+ /**
80
+ * Operational metrics for a monitoring panel. Serv cache metrics are reported separately
81
+ * (different owner) — the panel composes both.
82
+ */
83
+ getMetrics(): BlobDriverMetrics;
76
84
  private getContentImpl;
77
85
  /** Gets a content stream of a blob by a handle and calls handler with it. */
78
86
  withContent<T>(handle: LocalBlobHandle | RemoteBlobHandle, options: GetContentOptions & {
@@ -1 +1 @@
1
- {"version":3,"file":"download_blob.d.ts","names":[],"sources":["../../../src/drivers/download_blob/download_blob.ts"],"mappings":"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"}
1
+ {"version":3,"file":"download_blob.d.ts","names":[],"sources":["../../../src/drivers/download_blob/download_blob.ts"],"mappings":"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"}
@@ -38,6 +38,9 @@ var DownloadDriver = class DownloadDriver {
38
38
  idToLastLines = /* @__PURE__ */ new Map();
39
39
  idToProgressLog = /* @__PURE__ */ new Map();
40
40
  saveDir;
41
+ /** Downloads that bypassed the ranges cache; counted when issued. */
42
+ uncachedRequests = 0;
43
+ uncachedRequestBytes = 0;
41
44
  constructor(logger, clientDownload, clientLogs, saveDir, rangesCacheDir, signer, ops) {
42
45
  this.logger = logger;
43
46
  this.clientDownload = clientDownload;
@@ -140,6 +143,17 @@ var DownloadDriver = class DownloadDriver {
140
143
  bypassRangesCache: true
141
144
  });
142
145
  }
146
+ /**
147
+ * Operational metrics for a monitoring panel. Serv cache metrics are reported separately
148
+ * (different owner) — the panel composes both.
149
+ */
150
+ getMetrics() {
151
+ return {
152
+ uncachedRequests: this.uncachedRequests,
153
+ uncachedRequestBytes: this.uncachedRequestBytes,
154
+ ...this.clientDownload.metrics()
155
+ };
156
+ }
143
157
  async getContentImpl({ handle, options, bypassRangesCache = false }) {
144
158
  const request = () => this.withContent(handle, {
145
159
  ...options,
@@ -183,8 +197,12 @@ var DownloadDriver = class DownloadDriver {
183
197
  signal,
184
198
  handler
185
199
  });
200
+ if (bypassRangesCache) this.uncachedRequests++;
186
201
  return await this.clientDownload.withBlobContent(result.info, { signal }, options, async (content, size) => {
187
- if (bypassRangesCache) return await handler(content, size);
202
+ if (bypassRangesCache) return await handler(content.pipeThrough(new TransformStream({ transform: (chunk, controller) => {
203
+ this.uncachedRequestBytes += chunk.byteLength;
204
+ controller.enqueue(chunk);
205
+ } })), size);
188
206
  const [handlerStream, cacheStream] = content.tee();
189
207
  const handlerPromise = handler(handlerStream, size);
190
208
  buffer(cacheStream).then((data) => this.rangesCache.set(key, range ?? {
@@ -1 +1 @@
1
- {"version":3,"file":"download_blob.js","names":["path","fs"],"sources":["../../../src/drivers/download_blob/download_blob.ts"],"sourcesContent":["import type { ComputableCtx, ComputableStableDefined, Watcher } from \"@milaboratories/computable\";\nimport { ChangeSource, Computable } from \"@milaboratories/computable\";\nimport type { SignedResourceId, ResourceType } from \"@milaboratories/pl-client\";\nimport {\n isNotFoundError,\n resourceIdToString,\n stringifyWithResourceId,\n} from \"@milaboratories/pl-client\";\nimport type {\n AnyLogHandle,\n BlobDriver,\n ContentHandler,\n GetContentOptions,\n LocalBlobHandle,\n LocalBlobHandleAndSize,\n ReadyLogHandle,\n RemoteBlobHandle,\n RemoteBlobHandleAndSize,\n StreamingApiResponse,\n} from \"@milaboratories/pl-model-common\";\nimport { type RangeBytes, validateRangeBytes } from \"@milaboratories/pl-model-common\";\nimport type { PlTreeEntry, ResourceInfo, ResourceSnapshot } from \"@milaboratories/pl-tree\";\nimport {\n isPlTreeEntry,\n makeResourceSnapshot,\n treeEntryToResourceInfo,\n} from \"@milaboratories/pl-tree\";\nimport type { MiLogger, Signer } from \"@milaboratories/ts-helpers\";\nimport { CallersCounter, mapGet, TaskProcessor } from \"@milaboratories/ts-helpers\";\nimport Denque from \"denque\";\nimport * as fs from \"fs\";\nimport { randomUUID } from \"node:crypto\";\nimport * as fsp from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport * as readline from \"node:readline/promises\";\nimport { buffer } from \"node:stream/consumers\";\nimport type { ClientDownload } from \"../../clients/download\";\nimport type { ClientLogs } from \"../../clients/logs\";\nimport { withFileContent } from \"../helpers/read_file\";\nimport {\n isLocalBlobHandle,\n newLocalHandle,\n parseLocalHandle,\n} from \"../helpers/download_local_handle\";\nimport {\n isRemoteBlobHandle,\n newRemoteHandle,\n parseRemoteHandle,\n} from \"../helpers/download_remote_handle\";\nimport { Updater, WrongResourceTypeError } from \"../helpers/helpers\";\nimport { getResourceInfoFromLogHandle, newLogHandle } from \"../helpers/logs_handle\";\nimport { getSize, OnDemandBlobResourceSnapshot } from \"../types\";\nimport { blobKey, pathToKey } from \"./blob_key\";\nimport { DownloadBlobTask, nonRecoverableError } from \"./download_blob_task\";\nimport { FilesCache } from \"../helpers/files_cache\";\nimport { SparseCache, SparseCacheFsFile, SparseCacheFsRanges } from \"./sparse_cache/cache\";\nimport { isOffByOneError } from \"../../helpers/download_errors\";\n\nexport type DownloadDriverOps = {\n /**\n * A soft limit of the amount of blob storage, in bytes.\n * Once exceeded, the download driver will start deleting blobs one by one\n * when they become unneeded.\n * */\n cacheSoftSizeBytes: number;\n\n /**\n * A hard limit of the amount of sparse cache, in bytes.\n * Once exceeded, the download driver will start deleting blobs one by one.\n *\n * The sparse cache is used to store ranges of blobs.\n * */\n rangesCacheMaxSizeBytes: number;\n\n /**\n * Max number of concurrent downloads while calculating computable states\n * derived from this driver\n * */\n nConcurrentDownloads: number;\n};\n\n/** DownloadDriver holds a queue of downloading tasks,\n * and notifies every watcher when a file were downloaded. */\nexport class DownloadDriver implements BlobDriver, AsyncDisposable {\n /** Represents a unique key to the path of a blob as a map. */\n private keyToDownload: Map<string, DownloadBlobTask> = new Map();\n\n /** Writes and removes files to a hard drive and holds a counter for every\n * file that should be kept. */\n private cache: FilesCache<DownloadBlobTask>;\n private rangesCache: SparseCache;\n\n /** Downloads files and writes them to the local dir. */\n private downloadQueue: TaskProcessor;\n\n private keyToOnDemand: Map<string, OnDemandBlobHolder> = new Map();\n\n private idToLastLines: Map<string, LastLinesGetter> = new Map();\n private idToProgressLog: Map<string, LastLinesGetter> = new Map();\n\n private readonly saveDir: string;\n\n constructor(\n private readonly logger: MiLogger,\n private readonly clientDownload: ClientDownload,\n private readonly clientLogs: ClientLogs,\n saveDir: string,\n private readonly rangesCacheDir: string,\n private readonly signer: Signer,\n private readonly ops: DownloadDriverOps,\n ) {\n this.cache = new FilesCache(this.ops.cacheSoftSizeBytes);\n\n const fsRanges = new SparseCacheFsRanges(this.logger, this.rangesCacheDir);\n const fsStorage = new SparseCacheFsFile(this.logger, this.rangesCacheDir);\n this.rangesCache = new SparseCache(\n this.logger,\n this.ops.rangesCacheMaxSizeBytes,\n fsRanges,\n fsStorage,\n );\n\n this.downloadQueue = new TaskProcessor(this.logger, ops.nConcurrentDownloads);\n\n this.saveDir = path.resolve(saveDir);\n }\n\n static async init(\n logger: MiLogger,\n clientDownload: ClientDownload,\n clientLogs: ClientLogs,\n saveDir: string,\n rangesCacheDir: string,\n signer: Signer,\n ops: DownloadDriverOps,\n ): Promise<DownloadDriver> {\n const driver = new DownloadDriver(\n logger,\n clientDownload,\n clientLogs,\n saveDir,\n rangesCacheDir,\n signer,\n ops,\n );\n await driver.rangesCache.reset();\n\n return driver;\n }\n\n /** Gets a blob or part of the blob by its resource id or downloads a blob and sets it in a cache. */\n public getDownloadedBlob(\n res: ResourceInfo | PlTreeEntry,\n ctx: ComputableCtx,\n ): LocalBlobHandleAndSize | undefined;\n public getDownloadedBlob(\n res: ResourceInfo | PlTreeEntry,\n ): ComputableStableDefined<LocalBlobHandleAndSize>;\n public getDownloadedBlob(\n res: ResourceInfo | PlTreeEntry,\n ctx?: ComputableCtx,\n ): Computable<LocalBlobHandleAndSize | undefined> | LocalBlobHandleAndSize | undefined {\n if (ctx === undefined) {\n return Computable.make((ctx) => this.getDownloadedBlob(res, ctx));\n }\n\n const rInfo = treeEntryToResourceInfo(res, ctx);\n\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseBlob(rInfo, callerId));\n\n const result = this.getDownloadedBlobNoCtx(ctx.watcher, rInfo as ResourceSnapshot, callerId);\n if (result == undefined) {\n ctx.markUnstable(\"download blob is still undefined\");\n }\n\n return result;\n }\n\n private getDownloadedBlobNoCtx(\n w: Watcher,\n rInfo: ResourceSnapshot,\n callerId: string,\n ): LocalBlobHandleAndSize | undefined {\n validateDownloadableResourceType(\"getDownloadedBlob\", rInfo.type);\n\n // We don't need to request files with wider limits,\n // PFrame's engine does it disk-optimally by itself.\n\n const task = this.getOrSetNewTask(rInfo, callerId);\n task.attach(w, callerId);\n\n const result = task.getBlob();\n if (!result.done) {\n return undefined;\n }\n if (result.result.ok) {\n return result.result.value;\n }\n throw result.result.error;\n }\n\n private getOrSetNewTask(rInfo: ResourceSnapshot, callerId: string): DownloadBlobTask {\n const key = blobKey(rInfo.id);\n\n const inMemoryTask = this.keyToDownload.get(key);\n if (inMemoryTask) {\n return inMemoryTask;\n }\n\n // schedule the blob downloading, then it'll be added to the cache.\n const fPath = path.resolve(this.saveDir, key);\n\n const newTask = new DownloadBlobTask(\n this.logger,\n this.clientDownload,\n rInfo,\n newLocalHandle(fPath, this.signer),\n fPath,\n );\n this.keyToDownload.set(key, newTask);\n\n this.downloadQueue.push({\n fn: () => this.downloadBlob(newTask, callerId),\n recoverableErrorPredicate: (e) => !nonRecoverableError(e),\n });\n\n return newTask;\n }\n\n private async downloadBlob(task: DownloadBlobTask, callerId: string) {\n await task.download();\n const blob = task.getBlob();\n if (blob.done && blob.result.ok) {\n this.cache.addCache(task, callerId);\n }\n }\n\n /** Gets on demand blob. */\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ): Computable<RemoteBlobHandleAndSize>;\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ctx?: undefined,\n fromBytes?: number,\n toBytes?: number,\n ): Computable<RemoteBlobHandleAndSize>;\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ctx: ComputableCtx,\n fromBytes?: number,\n toBytes?: number,\n ): RemoteBlobHandleAndSize;\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ctx?: ComputableCtx,\n ): ComputableStableDefined<RemoteBlobHandleAndSize> | RemoteBlobHandleAndSize | undefined {\n if (ctx === undefined) return Computable.make((ctx) => this.getOnDemandBlob(res, ctx));\n\n const rInfo: OnDemandBlobResourceSnapshot = isPlTreeEntry(res)\n ? makeResourceSnapshot(res, OnDemandBlobResourceSnapshot, ctx)\n : res;\n\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseOnDemandBlob(rInfo.id, callerId));\n\n // note that the watcher is not needed,\n // the handler never changes.\n const result = this.getOnDemandBlobNoCtx(rInfo, callerId);\n\n return result;\n }\n\n private getOnDemandBlobNoCtx(\n info: OnDemandBlobResourceSnapshot,\n callerId: string,\n ): RemoteBlobHandleAndSize {\n validateDownloadableResourceType(\"getOnDemandBlob\", info.type);\n\n let blob = this.keyToOnDemand.get(blobKey(info.id));\n\n if (blob === undefined) {\n blob = new OnDemandBlobHolder(getSize(info), newRemoteHandle(info, this.signer));\n this.keyToOnDemand.set(blobKey(info.id), blob);\n }\n\n blob.attach(callerId);\n\n return blob.getHandle();\n }\n\n /** Gets a path from a handle. */\n public getLocalPath(handle: LocalBlobHandle): string {\n const { path } = parseLocalHandle(handle, this.signer);\n return path;\n }\n\n /** Gets a content of a blob by a handle. */\n public async getContent(\n handle: LocalBlobHandle | RemoteBlobHandle,\n options?: GetContentOptions,\n ): Promise<Uint8Array>;\n /** @deprecated Use {@link getContent} with {@link GetContentOptions} instead */\n public async getContent(\n handle: LocalBlobHandle | RemoteBlobHandle,\n range?: RangeBytes,\n ): Promise<Uint8Array>;\n public async getContent(\n handle: LocalBlobHandle | RemoteBlobHandle,\n optionsOrRange?: GetContentOptions | RangeBytes,\n ): Promise<Uint8Array> {\n let options: GetContentOptions = {};\n if (typeof optionsOrRange === \"object\" && optionsOrRange !== null) {\n if (\"range\" in optionsOrRange) {\n options = optionsOrRange;\n } else {\n const range = optionsOrRange as RangeBytes;\n validateRangeBytes(range, `getContent`);\n options = { range };\n }\n }\n\n return await this.getContentImpl({ handle, options });\n }\n\n /**\n * Same as {@link getContent}, but bypasses the ranges cache entirely (no read, no write).\n * For local handles this is identical to {@link getContent}, since local content never\n * uses the ranges cache.\n */\n public async getContentDirect(\n handle: LocalBlobHandle | RemoteBlobHandle,\n options?: GetContentOptions,\n ): Promise<Uint8Array> {\n return await this.getContentImpl({\n handle,\n options: options ?? {},\n bypassRangesCache: true,\n });\n }\n\n private async getContentImpl({\n handle,\n options,\n bypassRangesCache = false,\n }: {\n handle: LocalBlobHandle | RemoteBlobHandle;\n options: GetContentOptions;\n bypassRangesCache?: boolean;\n }): Promise<Uint8Array> {\n const request = () =>\n this.withContent(handle, {\n ...options,\n bypassRangesCache,\n handler: async (content) => {\n const chunks: Uint8Array[] = [];\n for await (const chunk of content) {\n options.signal?.throwIfAborted();\n chunks.push(chunk);\n }\n return Buffer.concat(chunks);\n },\n });\n\n try {\n return await request();\n } catch (error) {\n if (isOffByOneError(error)) {\n return await request();\n }\n throw error;\n }\n }\n\n /** Gets a content stream of a blob by a handle and calls handler with it. */\n public async withContent<T>(\n handle: LocalBlobHandle | RemoteBlobHandle,\n options: GetContentOptions & {\n handler: ContentHandler<T>;\n bypassRangesCache?: boolean;\n },\n ): Promise<T> {\n const { range, signal, handler, bypassRangesCache } = options;\n\n if (isLocalBlobHandle(handle)) {\n return await withFileContent({ path: this.getLocalPath(handle), range, signal, handler });\n }\n\n if (isRemoteBlobHandle(handle)) {\n const result = parseRemoteHandle(handle, this.signer);\n\n const key = blobKey(result.info.id);\n const filePath = bypassRangesCache\n ? undefined\n : await this.rangesCache.get(key, range ?? { from: 0, to: result.size });\n signal?.throwIfAborted();\n\n if (filePath) return await withFileContent({ path: filePath, range, signal, handler });\n\n return await this.clientDownload.withBlobContent(\n result.info,\n { signal },\n options,\n async (content, size) => {\n if (bypassRangesCache) return await handler(content, size);\n\n const [handlerStream, cacheStream] = content.tee();\n\n const handlerPromise = handler(handlerStream, size);\n const _cachePromise = buffer(cacheStream)\n .then((data) => this.rangesCache.set(key, range ?? { from: 0, to: result.size }, data))\n .catch(() => {\n // Ignore cache errors - they shouldn't affect the main handler result\n // This prevents unhandled promise rejections when the stream fails\n });\n\n return await handlerPromise;\n },\n );\n }\n\n throw new Error(\"Malformed remote handle\");\n }\n\n /**\n * Creates computable that will return blob content once it is downloaded.\n * Uses downloaded blob handle under the hood, so stores corresponding blob in file system.\n */\n public getComputableContent(\n res: ResourceInfo | PlTreeEntry,\n range?: RangeBytes,\n ): ComputableStableDefined<Uint8Array> {\n if (range) {\n validateRangeBytes(range, `getComputableContent`);\n }\n\n return Computable.make((ctx) => this.getDownloadedBlob(res, ctx), {\n postprocessValue: (v) => (v ? this.getContent(v.handle, { range }) : undefined),\n }).withStableType();\n }\n\n /** Returns all logs and schedules a job that reads remain logs.\n * Notifies when a new portion of the log appeared. */\n public getLastLogs(\n res: ResourceInfo | PlTreeEntry,\n lines: number,\n ): Computable<string | undefined>;\n public getLastLogs(\n res: ResourceInfo | PlTreeEntry,\n lines: number,\n ctx: ComputableCtx,\n ): Computable<string | undefined>;\n public getLastLogs(\n res: ResourceInfo | PlTreeEntry,\n lines: number,\n ctx?: ComputableCtx,\n ): Computable<string | undefined> | string | undefined {\n if (ctx == undefined) return Computable.make((ctx) => this.getLastLogs(res, lines, ctx));\n\n const r = treeEntryToResourceInfo(res, ctx);\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseBlob(r, callerId));\n\n const result = this.getLastLogsNoCtx(ctx.watcher, r as ResourceSnapshot, lines, callerId);\n if (result == undefined)\n ctx.markUnstable(\"either a file was not downloaded or logs was not read\");\n\n return result;\n }\n\n private getLastLogsNoCtx(\n w: Watcher,\n rInfo: ResourceSnapshot,\n lines: number,\n callerId: string,\n ): string | undefined {\n validateDownloadableResourceType(\"getLastLogs\", rInfo.type);\n const blob = this.getDownloadedBlobNoCtx(w, rInfo, callerId);\n if (blob == undefined) return undefined;\n\n const { path } = parseLocalHandle(blob.handle, this.signer);\n\n let logGetter = this.idToLastLines.get(blobKey(rInfo.id));\n\n if (logGetter == undefined) {\n const newLogGetter = new LastLinesGetter(path, lines);\n this.idToLastLines.set(blobKey(rInfo.id), newLogGetter);\n logGetter = newLogGetter;\n }\n\n const result = logGetter.getOrSchedule(w);\n if (result.error) throw result.error;\n\n return result.log;\n }\n\n /** Returns a last line that has patternToSearch.\n * Notifies when a new line appeared or EOF reached. */\n public getProgressLog(\n res: ResourceInfo | PlTreeEntry,\n patternToSearch: string,\n ): Computable<string | undefined>;\n public getProgressLog(\n res: ResourceInfo | PlTreeEntry,\n patternToSearch: string,\n ctx: ComputableCtx,\n ): string | undefined;\n public getProgressLog(\n res: ResourceInfo | PlTreeEntry,\n patternToSearch: string,\n ctx?: ComputableCtx,\n ): Computable<string | undefined> | string | undefined {\n if (ctx == undefined)\n return Computable.make((ctx) => this.getProgressLog(res, patternToSearch, ctx));\n\n const r = treeEntryToResourceInfo(res, ctx);\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseBlob(r, callerId));\n\n const result = this.getProgressLogNoCtx(\n ctx.watcher,\n r as ResourceSnapshot,\n patternToSearch,\n callerId,\n );\n if (result === undefined)\n ctx.markUnstable(\"either a file was not downloaded or a progress log was not read\");\n\n return result;\n }\n\n private getProgressLogNoCtx(\n w: Watcher,\n rInfo: ResourceSnapshot,\n patternToSearch: string,\n callerId: string,\n ): string | undefined {\n validateDownloadableResourceType(\"getProgressLog\", rInfo.type);\n\n const blob = this.getDownloadedBlobNoCtx(w, rInfo, callerId);\n if (blob == undefined) return undefined;\n const { path } = parseLocalHandle(blob.handle, this.signer);\n\n let logGetter = this.idToProgressLog.get(blobKey(rInfo.id));\n\n if (logGetter == undefined) {\n const newLogGetter = new LastLinesGetter(path, 1, patternToSearch);\n this.idToProgressLog.set(blobKey(rInfo.id), newLogGetter);\n\n logGetter = newLogGetter;\n }\n\n const result = logGetter.getOrSchedule(w);\n if (result.error) throw result.error;\n\n return result.log;\n }\n\n /** Returns an Id of a smart object, that can read logs directly from\n * the platform. */\n public getLogHandle(res: ResourceInfo | PlTreeEntry): Computable<AnyLogHandle>;\n public getLogHandle(res: ResourceInfo | PlTreeEntry, ctx: ComputableCtx): AnyLogHandle;\n public getLogHandle(\n res: ResourceInfo | PlTreeEntry,\n ctx?: ComputableCtx,\n ): Computable<AnyLogHandle> | AnyLogHandle {\n if (ctx == undefined) return Computable.make((ctx) => this.getLogHandle(res, ctx));\n\n const r = treeEntryToResourceInfo(res, ctx);\n\n return this.getLogHandleNoCtx(r as ResourceSnapshot);\n }\n\n private getLogHandleNoCtx(rInfo: ResourceSnapshot): AnyLogHandle {\n validateDownloadableResourceType(\"getLogHandle\", rInfo.type);\n return newLogHandle(false, rInfo);\n }\n\n public async lastLines(\n handle: ReadyLogHandle,\n lineCount: number,\n offsetBytes?: number, // if 0n, then start from the end.\n searchStr?: string,\n ): Promise<StreamingApiResponse> {\n const resp = await this.clientLogs.lastLines(\n getResourceInfoFromLogHandle(handle),\n lineCount,\n BigInt(offsetBytes ?? 0),\n searchStr,\n );\n\n return {\n live: false,\n shouldUpdateHandle: false,\n data: resp.data,\n size: Number(resp.size),\n newOffset: Number(resp.newOffset),\n };\n }\n\n public async readText(\n handle: ReadyLogHandle,\n lineCount: number,\n offsetBytes?: number,\n searchStr?: string,\n ): Promise<StreamingApiResponse> {\n const resp = await this.clientLogs.readText(\n getResourceInfoFromLogHandle(handle),\n lineCount,\n BigInt(offsetBytes ?? 0),\n searchStr,\n );\n\n return {\n live: false,\n shouldUpdateHandle: false,\n data: resp.data,\n size: Number(resp.size),\n newOffset: Number(resp.newOffset),\n };\n }\n\n private async releaseBlob(rInfo: ResourceInfo, callerId: string) {\n const task = this.keyToDownload.get(blobKey(rInfo.id));\n if (task == undefined) {\n return;\n }\n\n if (this.cache.existsFile(blobKey(rInfo.id))) {\n const toDelete = this.cache.removeFile(blobKey(rInfo.id), callerId);\n\n await Promise.all(\n toDelete.map(async (cachedFile) => {\n await fsp.rm(cachedFile.path);\n\n this.cache.removeCache(cachedFile);\n\n this.removeTask(\n mapGet(this.keyToDownload, pathToKey(cachedFile.path)),\n `the task ${stringifyWithResourceId(cachedFile)} was removed` +\n `from cache along with ${stringifyWithResourceId(toDelete.map((d) => d.path))}`,\n );\n }),\n );\n } else {\n // The task is still in a downloading queue.\n const deleted = task.counter.dec(callerId);\n if (deleted) {\n this.removeTask(\n task,\n `the task ${stringifyWithResourceId(task.info())} was removed from cache`,\n );\n }\n }\n }\n\n private removeTask(task: DownloadBlobTask, reason: string) {\n task.abort(reason);\n task.change.markChanged(`download task for ${task.path} removed: ${reason}`);\n this.keyToDownload.delete(pathToKey(task.path));\n this.idToLastLines.delete(blobKey(task.rInfo.id));\n this.idToProgressLog.delete(blobKey(task.rInfo.id));\n }\n\n private async releaseOnDemandBlob(blobId: SignedResourceId, callerId: string) {\n const deleted = this.keyToOnDemand.get(blobKey(blobId))?.release(callerId) ?? false;\n if (deleted) this.keyToOnDemand.delete(blobKey(blobId));\n }\n\n /** Removes all files from a hard drive. */\n async releaseAll() {\n this.downloadQueue.stop();\n\n this.keyToDownload.forEach((task, key) => {\n this.keyToDownload.delete(key);\n task.change.markChanged(`task ${resourceIdToString(task.rInfo.id)} released`);\n });\n }\n\n async dispose(): Promise<void> {\n await this.rangesCache.dispose();\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n await this.dispose();\n }\n}\n\n/** Keeps a counter to the on demand handle. */\nclass OnDemandBlobHolder {\n private readonly counter = new CallersCounter();\n\n constructor(\n private readonly size: number,\n private readonly handle: RemoteBlobHandle,\n ) {}\n\n public getHandle(): RemoteBlobHandleAndSize {\n return { handle: this.handle, size: this.size };\n }\n\n public attach(callerId: string) {\n this.counter.inc(callerId);\n }\n\n public release(callerId: string): boolean {\n return this.counter.dec(callerId);\n }\n}\n\nclass LastLinesGetter {\n private updater: Updater;\n private log: string | undefined;\n private readonly change: ChangeSource = new ChangeSource();\n private error: any | undefined = undefined;\n\n constructor(\n private readonly path: string,\n private readonly lines: number,\n private readonly patternToSearch?: string,\n ) {\n this.updater = new Updater(async () => this.update());\n }\n\n getOrSchedule(w: Watcher): {\n log: string | undefined;\n error?: any | undefined;\n } {\n this.change.attachWatcher(w);\n\n this.updater.schedule();\n\n return {\n log: this.log,\n error: this.error,\n };\n }\n\n async update(): Promise<void> {\n try {\n const newLogs = await getLastLines(this.path, this.lines, this.patternToSearch);\n\n if (this.log != newLogs) this.change.markChanged(`logs for ${this.path} updated`);\n this.log = newLogs;\n } catch (e: any) {\n if (isNotFoundError(e)) {\n // No resource\n this.log = \"\";\n this.error = e;\n this.change.markChanged(`log update for ${this.path} failed, resource not found`);\n return;\n }\n\n throw e;\n }\n }\n}\n\n/** Gets last lines from a file by reading the file from the top and keeping\n * last N lines in a window queue. */\nasync function getLastLines(\n fPath: string,\n nLines: number,\n patternToSearch?: string,\n): Promise<string> {\n let inStream: fs.ReadStream | undefined;\n let rl: readline.Interface | undefined;\n\n try {\n inStream = fs.createReadStream(fPath);\n rl = readline.createInterface({ input: inStream, crlfDelay: Infinity });\n\n const lines = new Denque();\n\n for await (const line of rl) {\n if (patternToSearch != undefined && !line.includes(patternToSearch)) continue;\n\n lines.push(line);\n if (lines.length > nLines) {\n lines.shift();\n }\n }\n\n // last EOL is for keeping backward compat with platforma implementation.\n return lines.toArray().join(os.EOL) + os.EOL;\n } finally {\n // Cleanup resources in finally block to ensure they're always cleaned up\n try {\n if (rl) {\n rl.close();\n }\n } catch (cleanupError) {\n console.error(\"Error closing readline interface:\", cleanupError);\n }\n\n try {\n if (inStream && !inStream.destroyed) {\n inStream.destroy();\n }\n } catch (cleanupError) {\n console.error(\"Error destroying read stream:\", cleanupError);\n }\n }\n}\n\nfunction validateDownloadableResourceType(methodName: string, rType: ResourceType) {\n if (!rType.name.startsWith(\"Blob/\")) {\n let message = `${methodName}: wrong resource type: ${rType.name}, expected: a resource of type that starts with 'Blob/'.`;\n if (rType.name == \"Blob\")\n message += ` If it's called from workflow, should a file be exported with 'file.exportFile' function?`;\n\n throw new WrongResourceTypeError(message);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"download_blob.js","names":["path","fs"],"sources":["../../../src/drivers/download_blob/download_blob.ts"],"sourcesContent":["import type { ComputableCtx, ComputableStableDefined, Watcher } from \"@milaboratories/computable\";\nimport { ChangeSource, Computable } from \"@milaboratories/computable\";\nimport type { SignedResourceId, ResourceType } from \"@milaboratories/pl-client\";\nimport {\n isNotFoundError,\n resourceIdToString,\n stringifyWithResourceId,\n} from \"@milaboratories/pl-client\";\nimport type {\n AnyLogHandle,\n BlobDriver,\n BlobDriverMetrics,\n ContentHandler,\n GetContentOptions,\n LocalBlobHandle,\n LocalBlobHandleAndSize,\n ReadyLogHandle,\n RemoteBlobHandle,\n RemoteBlobHandleAndSize,\n StreamingApiResponse,\n} from \"@milaboratories/pl-model-common\";\nimport { type RangeBytes, validateRangeBytes } from \"@milaboratories/pl-model-common\";\nimport type { PlTreeEntry, ResourceInfo, ResourceSnapshot } from \"@milaboratories/pl-tree\";\nimport {\n isPlTreeEntry,\n makeResourceSnapshot,\n treeEntryToResourceInfo,\n} from \"@milaboratories/pl-tree\";\nimport type { MiLogger, Signer } from \"@milaboratories/ts-helpers\";\nimport { CallersCounter, mapGet, TaskProcessor } from \"@milaboratories/ts-helpers\";\nimport Denque from \"denque\";\nimport * as fs from \"fs\";\nimport { randomUUID } from \"node:crypto\";\nimport * as fsp from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport * as readline from \"node:readline/promises\";\nimport { buffer } from \"node:stream/consumers\";\nimport type { ClientDownload } from \"../../clients/download\";\nimport type { ClientLogs } from \"../../clients/logs\";\nimport { withFileContent } from \"../helpers/read_file\";\nimport {\n isLocalBlobHandle,\n newLocalHandle,\n parseLocalHandle,\n} from \"../helpers/download_local_handle\";\nimport {\n isRemoteBlobHandle,\n newRemoteHandle,\n parseRemoteHandle,\n} from \"../helpers/download_remote_handle\";\nimport { Updater, WrongResourceTypeError } from \"../helpers/helpers\";\nimport { getResourceInfoFromLogHandle, newLogHandle } from \"../helpers/logs_handle\";\nimport { getSize, OnDemandBlobResourceSnapshot } from \"../types\";\nimport { blobKey, pathToKey } from \"./blob_key\";\nimport { DownloadBlobTask, nonRecoverableError } from \"./download_blob_task\";\nimport { FilesCache } from \"../helpers/files_cache\";\nimport { SparseCache, SparseCacheFsFile, SparseCacheFsRanges } from \"./sparse_cache/cache\";\nimport { isOffByOneError } from \"../../helpers/download_errors\";\n\nexport type DownloadDriverOps = {\n /**\n * A soft limit of the amount of blob storage, in bytes.\n * Once exceeded, the download driver will start deleting blobs one by one\n * when they become unneeded.\n * */\n cacheSoftSizeBytes: number;\n\n /**\n * A hard limit of the amount of sparse cache, in bytes.\n * Once exceeded, the download driver will start deleting blobs one by one.\n *\n * The sparse cache is used to store ranges of blobs.\n * */\n rangesCacheMaxSizeBytes: number;\n\n /**\n * Max number of concurrent downloads while calculating computable states\n * derived from this driver\n * */\n nConcurrentDownloads: number;\n};\n\n/** DownloadDriver holds a queue of downloading tasks,\n * and notifies every watcher when a file were downloaded. */\nexport class DownloadDriver implements BlobDriver, AsyncDisposable {\n /** Represents a unique key to the path of a blob as a map. */\n private keyToDownload: Map<string, DownloadBlobTask> = new Map();\n\n /** Writes and removes files to a hard drive and holds a counter for every\n * file that should be kept. */\n private cache: FilesCache<DownloadBlobTask>;\n private rangesCache: SparseCache;\n\n /** Downloads files and writes them to the local dir. */\n private downloadQueue: TaskProcessor;\n\n private keyToOnDemand: Map<string, OnDemandBlobHolder> = new Map();\n\n private idToLastLines: Map<string, LastLinesGetter> = new Map();\n private idToProgressLog: Map<string, LastLinesGetter> = new Map();\n\n private readonly saveDir: string;\n\n /** Downloads that bypassed the ranges cache; counted when issued. */\n private uncachedRequests = 0;\n private uncachedRequestBytes = 0;\n\n constructor(\n private readonly logger: MiLogger,\n private readonly clientDownload: ClientDownload,\n private readonly clientLogs: ClientLogs,\n saveDir: string,\n private readonly rangesCacheDir: string,\n private readonly signer: Signer,\n private readonly ops: DownloadDriverOps,\n ) {\n this.cache = new FilesCache(this.ops.cacheSoftSizeBytes);\n\n const fsRanges = new SparseCacheFsRanges(this.logger, this.rangesCacheDir);\n const fsStorage = new SparseCacheFsFile(this.logger, this.rangesCacheDir);\n this.rangesCache = new SparseCache(\n this.logger,\n this.ops.rangesCacheMaxSizeBytes,\n fsRanges,\n fsStorage,\n );\n\n this.downloadQueue = new TaskProcessor(this.logger, ops.nConcurrentDownloads);\n\n this.saveDir = path.resolve(saveDir);\n }\n\n static async init(\n logger: MiLogger,\n clientDownload: ClientDownload,\n clientLogs: ClientLogs,\n saveDir: string,\n rangesCacheDir: string,\n signer: Signer,\n ops: DownloadDriverOps,\n ): Promise<DownloadDriver> {\n const driver = new DownloadDriver(\n logger,\n clientDownload,\n clientLogs,\n saveDir,\n rangesCacheDir,\n signer,\n ops,\n );\n await driver.rangesCache.reset();\n\n return driver;\n }\n\n /** Gets a blob or part of the blob by its resource id or downloads a blob and sets it in a cache. */\n public getDownloadedBlob(\n res: ResourceInfo | PlTreeEntry,\n ctx: ComputableCtx,\n ): LocalBlobHandleAndSize | undefined;\n public getDownloadedBlob(\n res: ResourceInfo | PlTreeEntry,\n ): ComputableStableDefined<LocalBlobHandleAndSize>;\n public getDownloadedBlob(\n res: ResourceInfo | PlTreeEntry,\n ctx?: ComputableCtx,\n ): Computable<LocalBlobHandleAndSize | undefined> | LocalBlobHandleAndSize | undefined {\n if (ctx === undefined) {\n return Computable.make((ctx) => this.getDownloadedBlob(res, ctx));\n }\n\n const rInfo = treeEntryToResourceInfo(res, ctx);\n\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseBlob(rInfo, callerId));\n\n const result = this.getDownloadedBlobNoCtx(ctx.watcher, rInfo as ResourceSnapshot, callerId);\n if (result == undefined) {\n ctx.markUnstable(\"download blob is still undefined\");\n }\n\n return result;\n }\n\n private getDownloadedBlobNoCtx(\n w: Watcher,\n rInfo: ResourceSnapshot,\n callerId: string,\n ): LocalBlobHandleAndSize | undefined {\n validateDownloadableResourceType(\"getDownloadedBlob\", rInfo.type);\n\n // We don't need to request files with wider limits,\n // PFrame's engine does it disk-optimally by itself.\n\n const task = this.getOrSetNewTask(rInfo, callerId);\n task.attach(w, callerId);\n\n const result = task.getBlob();\n if (!result.done) {\n return undefined;\n }\n if (result.result.ok) {\n return result.result.value;\n }\n throw result.result.error;\n }\n\n private getOrSetNewTask(rInfo: ResourceSnapshot, callerId: string): DownloadBlobTask {\n const key = blobKey(rInfo.id);\n\n const inMemoryTask = this.keyToDownload.get(key);\n if (inMemoryTask) {\n return inMemoryTask;\n }\n\n // schedule the blob downloading, then it'll be added to the cache.\n const fPath = path.resolve(this.saveDir, key);\n\n const newTask = new DownloadBlobTask(\n this.logger,\n this.clientDownload,\n rInfo,\n newLocalHandle(fPath, this.signer),\n fPath,\n );\n this.keyToDownload.set(key, newTask);\n\n this.downloadQueue.push({\n fn: () => this.downloadBlob(newTask, callerId),\n recoverableErrorPredicate: (e) => !nonRecoverableError(e),\n });\n\n return newTask;\n }\n\n private async downloadBlob(task: DownloadBlobTask, callerId: string) {\n await task.download();\n const blob = task.getBlob();\n if (blob.done && blob.result.ok) {\n this.cache.addCache(task, callerId);\n }\n }\n\n /** Gets on demand blob. */\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ): Computable<RemoteBlobHandleAndSize>;\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ctx?: undefined,\n fromBytes?: number,\n toBytes?: number,\n ): Computable<RemoteBlobHandleAndSize>;\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ctx: ComputableCtx,\n fromBytes?: number,\n toBytes?: number,\n ): RemoteBlobHandleAndSize;\n public getOnDemandBlob(\n res: OnDemandBlobResourceSnapshot | PlTreeEntry,\n ctx?: ComputableCtx,\n ): ComputableStableDefined<RemoteBlobHandleAndSize> | RemoteBlobHandleAndSize | undefined {\n if (ctx === undefined) return Computable.make((ctx) => this.getOnDemandBlob(res, ctx));\n\n const rInfo: OnDemandBlobResourceSnapshot = isPlTreeEntry(res)\n ? makeResourceSnapshot(res, OnDemandBlobResourceSnapshot, ctx)\n : res;\n\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseOnDemandBlob(rInfo.id, callerId));\n\n // note that the watcher is not needed,\n // the handler never changes.\n const result = this.getOnDemandBlobNoCtx(rInfo, callerId);\n\n return result;\n }\n\n private getOnDemandBlobNoCtx(\n info: OnDemandBlobResourceSnapshot,\n callerId: string,\n ): RemoteBlobHandleAndSize {\n validateDownloadableResourceType(\"getOnDemandBlob\", info.type);\n\n let blob = this.keyToOnDemand.get(blobKey(info.id));\n\n if (blob === undefined) {\n blob = new OnDemandBlobHolder(getSize(info), newRemoteHandle(info, this.signer));\n this.keyToOnDemand.set(blobKey(info.id), blob);\n }\n\n blob.attach(callerId);\n\n return blob.getHandle();\n }\n\n /** Gets a path from a handle. */\n public getLocalPath(handle: LocalBlobHandle): string {\n const { path } = parseLocalHandle(handle, this.signer);\n return path;\n }\n\n /** Gets a content of a blob by a handle. */\n public async getContent(\n handle: LocalBlobHandle | RemoteBlobHandle,\n options?: GetContentOptions,\n ): Promise<Uint8Array>;\n /** @deprecated Use {@link getContent} with {@link GetContentOptions} instead */\n public async getContent(\n handle: LocalBlobHandle | RemoteBlobHandle,\n range?: RangeBytes,\n ): Promise<Uint8Array>;\n public async getContent(\n handle: LocalBlobHandle | RemoteBlobHandle,\n optionsOrRange?: GetContentOptions | RangeBytes,\n ): Promise<Uint8Array> {\n let options: GetContentOptions = {};\n if (typeof optionsOrRange === \"object\" && optionsOrRange !== null) {\n if (\"range\" in optionsOrRange) {\n options = optionsOrRange;\n } else {\n const range = optionsOrRange as RangeBytes;\n validateRangeBytes(range, `getContent`);\n options = { range };\n }\n }\n\n return await this.getContentImpl({ handle, options });\n }\n\n /**\n * Same as {@link getContent}, but bypasses the ranges cache entirely (no read, no write).\n * For local handles this is identical to {@link getContent}, since local content never\n * uses the ranges cache.\n */\n public async getContentDirect(\n handle: LocalBlobHandle | RemoteBlobHandle,\n options?: GetContentOptions,\n ): Promise<Uint8Array> {\n return await this.getContentImpl({\n handle,\n options: options ?? {},\n bypassRangesCache: true,\n });\n }\n\n /**\n * Operational metrics for a monitoring panel. Serv cache metrics are reported separately\n * (different owner) — the panel composes both.\n */\n public getMetrics(): BlobDriverMetrics {\n return {\n uncachedRequests: this.uncachedRequests,\n uncachedRequestBytes: this.uncachedRequestBytes,\n ...this.clientDownload.metrics(),\n };\n }\n\n private async getContentImpl({\n handle,\n options,\n bypassRangesCache = false,\n }: {\n handle: LocalBlobHandle | RemoteBlobHandle;\n options: GetContentOptions;\n bypassRangesCache?: boolean;\n }): Promise<Uint8Array> {\n const request = () =>\n this.withContent(handle, {\n ...options,\n bypassRangesCache,\n handler: async (content) => {\n const chunks: Uint8Array[] = [];\n for await (const chunk of content) {\n options.signal?.throwIfAborted();\n chunks.push(chunk);\n }\n return Buffer.concat(chunks);\n },\n });\n\n try {\n return await request();\n } catch (error) {\n if (isOffByOneError(error)) {\n return await request();\n }\n throw error;\n }\n }\n\n /** Gets a content stream of a blob by a handle and calls handler with it. */\n public async withContent<T>(\n handle: LocalBlobHandle | RemoteBlobHandle,\n options: GetContentOptions & {\n handler: ContentHandler<T>;\n bypassRangesCache?: boolean;\n },\n ): Promise<T> {\n const { range, signal, handler, bypassRangesCache } = options;\n\n if (isLocalBlobHandle(handle)) {\n return await withFileContent({ path: this.getLocalPath(handle), range, signal, handler });\n }\n\n if (isRemoteBlobHandle(handle)) {\n const result = parseRemoteHandle(handle, this.signer);\n\n const key = blobKey(result.info.id);\n const filePath = bypassRangesCache\n ? undefined\n : await this.rangesCache.get(key, range ?? { from: 0, to: result.size });\n signal?.throwIfAborted();\n\n if (filePath) return await withFileContent({ path: filePath, range, signal, handler });\n\n if (bypassRangesCache) this.uncachedRequests++;\n\n return await this.clientDownload.withBlobContent(\n result.info,\n { signal },\n options,\n async (content, size) => {\n if (bypassRangesCache) {\n const counted = content.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform: (chunk, controller) => {\n this.uncachedRequestBytes += chunk.byteLength;\n controller.enqueue(chunk);\n },\n }),\n );\n return await handler(counted, size);\n }\n\n const [handlerStream, cacheStream] = content.tee();\n\n const handlerPromise = handler(handlerStream, size);\n const _cachePromise = buffer(cacheStream)\n .then((data) => this.rangesCache.set(key, range ?? { from: 0, to: result.size }, data))\n .catch(() => {\n // Ignore cache errors - they shouldn't affect the main handler result\n // This prevents unhandled promise rejections when the stream fails\n });\n\n return await handlerPromise;\n },\n );\n }\n\n throw new Error(\"Malformed remote handle\");\n }\n\n /**\n * Creates computable that will return blob content once it is downloaded.\n * Uses downloaded blob handle under the hood, so stores corresponding blob in file system.\n */\n public getComputableContent(\n res: ResourceInfo | PlTreeEntry,\n range?: RangeBytes,\n ): ComputableStableDefined<Uint8Array> {\n if (range) {\n validateRangeBytes(range, `getComputableContent`);\n }\n\n return Computable.make((ctx) => this.getDownloadedBlob(res, ctx), {\n postprocessValue: (v) => (v ? this.getContent(v.handle, { range }) : undefined),\n }).withStableType();\n }\n\n /** Returns all logs and schedules a job that reads remain logs.\n * Notifies when a new portion of the log appeared. */\n public getLastLogs(\n res: ResourceInfo | PlTreeEntry,\n lines: number,\n ): Computable<string | undefined>;\n public getLastLogs(\n res: ResourceInfo | PlTreeEntry,\n lines: number,\n ctx: ComputableCtx,\n ): Computable<string | undefined>;\n public getLastLogs(\n res: ResourceInfo | PlTreeEntry,\n lines: number,\n ctx?: ComputableCtx,\n ): Computable<string | undefined> | string | undefined {\n if (ctx == undefined) return Computable.make((ctx) => this.getLastLogs(res, lines, ctx));\n\n const r = treeEntryToResourceInfo(res, ctx);\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseBlob(r, callerId));\n\n const result = this.getLastLogsNoCtx(ctx.watcher, r as ResourceSnapshot, lines, callerId);\n if (result == undefined)\n ctx.markUnstable(\"either a file was not downloaded or logs was not read\");\n\n return result;\n }\n\n private getLastLogsNoCtx(\n w: Watcher,\n rInfo: ResourceSnapshot,\n lines: number,\n callerId: string,\n ): string | undefined {\n validateDownloadableResourceType(\"getLastLogs\", rInfo.type);\n const blob = this.getDownloadedBlobNoCtx(w, rInfo, callerId);\n if (blob == undefined) return undefined;\n\n const { path } = parseLocalHandle(blob.handle, this.signer);\n\n let logGetter = this.idToLastLines.get(blobKey(rInfo.id));\n\n if (logGetter == undefined) {\n const newLogGetter = new LastLinesGetter(path, lines);\n this.idToLastLines.set(blobKey(rInfo.id), newLogGetter);\n logGetter = newLogGetter;\n }\n\n const result = logGetter.getOrSchedule(w);\n if (result.error) throw result.error;\n\n return result.log;\n }\n\n /** Returns a last line that has patternToSearch.\n * Notifies when a new line appeared or EOF reached. */\n public getProgressLog(\n res: ResourceInfo | PlTreeEntry,\n patternToSearch: string,\n ): Computable<string | undefined>;\n public getProgressLog(\n res: ResourceInfo | PlTreeEntry,\n patternToSearch: string,\n ctx: ComputableCtx,\n ): string | undefined;\n public getProgressLog(\n res: ResourceInfo | PlTreeEntry,\n patternToSearch: string,\n ctx?: ComputableCtx,\n ): Computable<string | undefined> | string | undefined {\n if (ctx == undefined)\n return Computable.make((ctx) => this.getProgressLog(res, patternToSearch, ctx));\n\n const r = treeEntryToResourceInfo(res, ctx);\n const callerId = randomUUID();\n ctx.addOnDestroy(() => this.releaseBlob(r, callerId));\n\n const result = this.getProgressLogNoCtx(\n ctx.watcher,\n r as ResourceSnapshot,\n patternToSearch,\n callerId,\n );\n if (result === undefined)\n ctx.markUnstable(\"either a file was not downloaded or a progress log was not read\");\n\n return result;\n }\n\n private getProgressLogNoCtx(\n w: Watcher,\n rInfo: ResourceSnapshot,\n patternToSearch: string,\n callerId: string,\n ): string | undefined {\n validateDownloadableResourceType(\"getProgressLog\", rInfo.type);\n\n const blob = this.getDownloadedBlobNoCtx(w, rInfo, callerId);\n if (blob == undefined) return undefined;\n const { path } = parseLocalHandle(blob.handle, this.signer);\n\n let logGetter = this.idToProgressLog.get(blobKey(rInfo.id));\n\n if (logGetter == undefined) {\n const newLogGetter = new LastLinesGetter(path, 1, patternToSearch);\n this.idToProgressLog.set(blobKey(rInfo.id), newLogGetter);\n\n logGetter = newLogGetter;\n }\n\n const result = logGetter.getOrSchedule(w);\n if (result.error) throw result.error;\n\n return result.log;\n }\n\n /** Returns an Id of a smart object, that can read logs directly from\n * the platform. */\n public getLogHandle(res: ResourceInfo | PlTreeEntry): Computable<AnyLogHandle>;\n public getLogHandle(res: ResourceInfo | PlTreeEntry, ctx: ComputableCtx): AnyLogHandle;\n public getLogHandle(\n res: ResourceInfo | PlTreeEntry,\n ctx?: ComputableCtx,\n ): Computable<AnyLogHandle> | AnyLogHandle {\n if (ctx == undefined) return Computable.make((ctx) => this.getLogHandle(res, ctx));\n\n const r = treeEntryToResourceInfo(res, ctx);\n\n return this.getLogHandleNoCtx(r as ResourceSnapshot);\n }\n\n private getLogHandleNoCtx(rInfo: ResourceSnapshot): AnyLogHandle {\n validateDownloadableResourceType(\"getLogHandle\", rInfo.type);\n return newLogHandle(false, rInfo);\n }\n\n public async lastLines(\n handle: ReadyLogHandle,\n lineCount: number,\n offsetBytes?: number, // if 0n, then start from the end.\n searchStr?: string,\n ): Promise<StreamingApiResponse> {\n const resp = await this.clientLogs.lastLines(\n getResourceInfoFromLogHandle(handle),\n lineCount,\n BigInt(offsetBytes ?? 0),\n searchStr,\n );\n\n return {\n live: false,\n shouldUpdateHandle: false,\n data: resp.data,\n size: Number(resp.size),\n newOffset: Number(resp.newOffset),\n };\n }\n\n public async readText(\n handle: ReadyLogHandle,\n lineCount: number,\n offsetBytes?: number,\n searchStr?: string,\n ): Promise<StreamingApiResponse> {\n const resp = await this.clientLogs.readText(\n getResourceInfoFromLogHandle(handle),\n lineCount,\n BigInt(offsetBytes ?? 0),\n searchStr,\n );\n\n return {\n live: false,\n shouldUpdateHandle: false,\n data: resp.data,\n size: Number(resp.size),\n newOffset: Number(resp.newOffset),\n };\n }\n\n private async releaseBlob(rInfo: ResourceInfo, callerId: string) {\n const task = this.keyToDownload.get(blobKey(rInfo.id));\n if (task == undefined) {\n return;\n }\n\n if (this.cache.existsFile(blobKey(rInfo.id))) {\n const toDelete = this.cache.removeFile(blobKey(rInfo.id), callerId);\n\n await Promise.all(\n toDelete.map(async (cachedFile) => {\n await fsp.rm(cachedFile.path);\n\n this.cache.removeCache(cachedFile);\n\n this.removeTask(\n mapGet(this.keyToDownload, pathToKey(cachedFile.path)),\n `the task ${stringifyWithResourceId(cachedFile)} was removed` +\n `from cache along with ${stringifyWithResourceId(toDelete.map((d) => d.path))}`,\n );\n }),\n );\n } else {\n // The task is still in a downloading queue.\n const deleted = task.counter.dec(callerId);\n if (deleted) {\n this.removeTask(\n task,\n `the task ${stringifyWithResourceId(task.info())} was removed from cache`,\n );\n }\n }\n }\n\n private removeTask(task: DownloadBlobTask, reason: string) {\n task.abort(reason);\n task.change.markChanged(`download task for ${task.path} removed: ${reason}`);\n this.keyToDownload.delete(pathToKey(task.path));\n this.idToLastLines.delete(blobKey(task.rInfo.id));\n this.idToProgressLog.delete(blobKey(task.rInfo.id));\n }\n\n private async releaseOnDemandBlob(blobId: SignedResourceId, callerId: string) {\n const deleted = this.keyToOnDemand.get(blobKey(blobId))?.release(callerId) ?? false;\n if (deleted) this.keyToOnDemand.delete(blobKey(blobId));\n }\n\n /** Removes all files from a hard drive. */\n async releaseAll() {\n this.downloadQueue.stop();\n\n this.keyToDownload.forEach((task, key) => {\n this.keyToDownload.delete(key);\n task.change.markChanged(`task ${resourceIdToString(task.rInfo.id)} released`);\n });\n }\n\n async dispose(): Promise<void> {\n await this.rangesCache.dispose();\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n await this.dispose();\n }\n}\n\n/** Keeps a counter to the on demand handle. */\nclass OnDemandBlobHolder {\n private readonly counter = new CallersCounter();\n\n constructor(\n private readonly size: number,\n private readonly handle: RemoteBlobHandle,\n ) {}\n\n public getHandle(): RemoteBlobHandleAndSize {\n return { handle: this.handle, size: this.size };\n }\n\n public attach(callerId: string) {\n this.counter.inc(callerId);\n }\n\n public release(callerId: string): boolean {\n return this.counter.dec(callerId);\n }\n}\n\nclass LastLinesGetter {\n private updater: Updater;\n private log: string | undefined;\n private readonly change: ChangeSource = new ChangeSource();\n private error: any | undefined = undefined;\n\n constructor(\n private readonly path: string,\n private readonly lines: number,\n private readonly patternToSearch?: string,\n ) {\n this.updater = new Updater(async () => this.update());\n }\n\n getOrSchedule(w: Watcher): {\n log: string | undefined;\n error?: any | undefined;\n } {\n this.change.attachWatcher(w);\n\n this.updater.schedule();\n\n return {\n log: this.log,\n error: this.error,\n };\n }\n\n async update(): Promise<void> {\n try {\n const newLogs = await getLastLines(this.path, this.lines, this.patternToSearch);\n\n if (this.log != newLogs) this.change.markChanged(`logs for ${this.path} updated`);\n this.log = newLogs;\n } catch (e: any) {\n if (isNotFoundError(e)) {\n // No resource\n this.log = \"\";\n this.error = e;\n this.change.markChanged(`log update for ${this.path} failed, resource not found`);\n return;\n }\n\n throw e;\n }\n }\n}\n\n/** Gets last lines from a file by reading the file from the top and keeping\n * last N lines in a window queue. */\nasync function getLastLines(\n fPath: string,\n nLines: number,\n patternToSearch?: string,\n): Promise<string> {\n let inStream: fs.ReadStream | undefined;\n let rl: readline.Interface | undefined;\n\n try {\n inStream = fs.createReadStream(fPath);\n rl = readline.createInterface({ input: inStream, crlfDelay: Infinity });\n\n const lines = new Denque();\n\n for await (const line of rl) {\n if (patternToSearch != undefined && !line.includes(patternToSearch)) continue;\n\n lines.push(line);\n if (lines.length > nLines) {\n lines.shift();\n }\n }\n\n // last EOL is for keeping backward compat with platforma implementation.\n return lines.toArray().join(os.EOL) + os.EOL;\n } finally {\n // Cleanup resources in finally block to ensure they're always cleaned up\n try {\n if (rl) {\n rl.close();\n }\n } catch (cleanupError) {\n console.error(\"Error closing readline interface:\", cleanupError);\n }\n\n try {\n if (inStream && !inStream.destroyed) {\n inStream.destroy();\n }\n } catch (cleanupError) {\n console.error(\"Error destroying read stream:\", cleanupError);\n }\n }\n}\n\nfunction validateDownloadableResourceType(methodName: string, rType: ResourceType) {\n if (!rType.name.startsWith(\"Blob/\")) {\n let message = `${methodName}: wrong resource type: ${rType.name}, expected: a resource of type that starts with 'Blob/'.`;\n if (rType.name == \"Blob\")\n message += ` If it's called from workflow, should a file be exported with 'file.exportFile' function?`;\n\n throw new WrongResourceTypeError(message);\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-drivers",
3
- "version": "1.16.0",
3
+ "version": "1.16.1",
4
4
  "description": "Drivers and a low-level clients for log streaming, downloading and uploading files from and to pl",
5
5
  "files": [
6
6
  "dist/**/*",
@@ -29,12 +29,12 @@
29
29
  "tar-fs": "^3.0.9",
30
30
  "undici": "~7.16.0",
31
31
  "zod": "~3.25.76",
32
- "@milaboratories/pl-client": "3.11.4",
33
32
  "@milaboratories/computable": "2.9.5",
34
- "@milaboratories/pl-model-common": "1.46.1",
33
+ "@milaboratories/pl-client": "3.11.5",
35
34
  "@milaboratories/helpers": "1.14.2",
35
+ "@milaboratories/pl-tree": "1.12.13",
36
36
  "@milaboratories/ts-helpers": "1.8.3",
37
- "@milaboratories/pl-tree": "1.12.12"
37
+ "@milaboratories/pl-model-common": "1.46.2"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/decompress": "^4.2.7",
@@ -44,10 +44,10 @@
44
44
  "openapi-typescript": "^7.10.0",
45
45
  "typescript": "~5.9.3",
46
46
  "vitest": "^4.1.3",
47
- "@milaboratories/ts-builder": "1.5.2",
48
47
  "@milaboratories/test-helpers": "1.2.2",
49
48
  "@milaboratories/ts-configs": "1.2.3",
50
- "@milaboratories/build-configs": "2.0.0"
49
+ "@milaboratories/build-configs": "2.0.0",
50
+ "@milaboratories/ts-builder": "1.5.2"
51
51
  },
52
52
  "engines": {
53
53
  "node": ">=22"
@@ -25,9 +25,12 @@ import { validateAbsolute } from "../helpers/validate";
25
25
  import type { DownloadAPI_GetDownloadURL_Response } from "../proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol";
26
26
  import { DownloadClient } from "../proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client";
27
27
  import type { DownloadApiPaths, DownloadRestClientType } from "../proto-rest";
28
- import { type GetContentOptions } from "@milaboratories/pl-model-common";
28
+ import { type GetContentOptions, type BlobDriverMetrics } from "@milaboratories/pl-model-common";
29
29
  import { DownloadUrlCache } from "./download_url_cache";
30
30
 
31
+ /** Subset of {@link BlobDriverMetrics} owned by the download client (presigned cache + in-flight downloads). */
32
+ type ClientDownloadMetrics = Omit<BlobDriverMetrics, "uncachedRequests" | "uncachedRequestBytes">;
33
+
31
34
  /** Gets URLs for downloading from pl-core, parses them and reads or downloads
32
35
  * files locally and from the web. */
33
36
  export class ClientDownload {
@@ -43,6 +46,13 @@ export class ClientDownload {
43
46
  /** Caches presigned download URLs by resource id until they (almost) expire. */
44
47
  private readonly urlCache: DownloadUrlCache;
45
48
 
49
+ /** Active remote downloads; in-flight gauges are summed over this set on read. */
50
+ private readonly inFlight = new Set<{ size: number; received: number }>();
51
+ private presignedUrlCacheHits = 0;
52
+ private presignedUrlCacheMisses = 0;
53
+ private presignedUrlStaleHits = 0;
54
+ private presignedUrlRequestSumLatencyMs = 0;
55
+
46
56
  constructor(
47
57
  wireClientProviderFactory: WireClientProviderFactory,
48
58
  public readonly httpClient: Dispatcher,
@@ -95,7 +105,7 @@ export class ClientDownload {
95
105
  const timer = PerfTimer.start();
96
106
  const result = isLocal(downloadUrl)
97
107
  ? await this.withLocalFileContent(downloadUrl, ops, handler)
98
- : await this.remoteFileDownloader.withContent(downloadUrl, remoteHeaders, ops, handler);
108
+ : await this.withTrackedRemoteContent(downloadUrl, remoteHeaders, ops, handler);
99
109
 
100
110
  this.logger.info(
101
111
  `blob ${stringifyWithResourceId(info)} download finished, ` + `took: ${timer.elapsed()}`,
@@ -106,22 +116,80 @@ export class ClientDownload {
106
116
  const cached = this.urlCache.get(info.id);
107
117
  if (cached !== undefined) {
108
118
  try {
109
- return await attempt(cached);
119
+ const result = await attempt(cached);
120
+ this.presignedUrlCacheHits++;
121
+ return result;
110
122
  } catch (error) {
111
123
  if (!isDownloadNetworkError400(error)) throw error;
112
124
  this.urlCache.delete(info.id);
125
+ this.presignedUrlStaleHits++;
113
126
  this.logger.info(
114
127
  `cached download URL for blob ${stringifyWithResourceId(info)} rejected ` +
115
128
  `(status ${error.statusCode}), re-fetching`,
116
129
  );
117
130
  }
131
+ } else {
132
+ this.presignedUrlCacheMisses++;
118
133
  }
119
134
 
135
+ const urlFetchStartMs = performance.now();
120
136
  const fresh = await this.grpcGetDownloadUrl(info, options, ops.signal);
137
+ this.presignedUrlRequestSumLatencyMs += performance.now() - urlFetchStartMs;
121
138
  this.urlCache.set(info.id, fresh);
122
139
  return await attempt(fresh);
123
140
  }
124
141
 
142
+ /** Download-client-owned slice of {@link BlobDriverMetrics}: presigned-URL cache + in-flight downloads. */
143
+ metrics(): ClientDownloadMetrics {
144
+ let inFlightBytesTotal = 0;
145
+ let inFlightBytesReceived = 0;
146
+ for (const rec of this.inFlight) {
147
+ inFlightBytesTotal += rec.size;
148
+ inFlightBytesReceived += rec.received;
149
+ }
150
+ return {
151
+ downloadsInFlight: this.inFlight.size,
152
+ inFlightBytesTotal,
153
+ inFlightBytesReceived,
154
+ presignedUrlCacheHits: this.presignedUrlCacheHits,
155
+ presignedUrlCacheMisses: this.presignedUrlCacheMisses,
156
+ presignedUrlStaleHits: this.presignedUrlStaleHits,
157
+ presignedUrlRequestSumLatencyMs: this.presignedUrlRequestSumLatencyMs,
158
+ };
159
+ }
160
+
161
+ /** Wraps a remote download so it appears in the in-flight gauges and its received bytes are counted live. */
162
+ private async withTrackedRemoteContent<T>(
163
+ url: string,
164
+ headers: Record<string, string>,
165
+ ops: GetContentOptions,
166
+ handler: ContentHandler<T>,
167
+ ): Promise<T> {
168
+ const rec = { size: 0, received: 0 };
169
+ this.inFlight.add(rec);
170
+ try {
171
+ return await this.remoteFileDownloader.withContent(
172
+ url,
173
+ headers,
174
+ ops,
175
+ async (content, size) => {
176
+ rec.size = size;
177
+ const counted = content.pipeThrough(
178
+ new TransformStream<Uint8Array, Uint8Array>({
179
+ transform: (chunk, controller) => {
180
+ rec.received += chunk.byteLength;
181
+ controller.enqueue(chunk);
182
+ },
183
+ }),
184
+ );
185
+ return await handler(counted, size);
186
+ },
187
+ );
188
+ } finally {
189
+ this.inFlight.delete(rec);
190
+ }
191
+ }
192
+
125
193
  async withLocalFileContent<T>(
126
194
  url: string,
127
195
  ops: GetContentOptions,
@@ -9,6 +9,7 @@ import {
9
9
  import type {
10
10
  AnyLogHandle,
11
11
  BlobDriver,
12
+ BlobDriverMetrics,
12
13
  ContentHandler,
13
14
  GetContentOptions,
14
15
  LocalBlobHandle,
@@ -101,6 +102,10 @@ export class DownloadDriver implements BlobDriver, AsyncDisposable {
101
102
 
102
103
  private readonly saveDir: string;
103
104
 
105
+ /** Downloads that bypassed the ranges cache; counted when issued. */
106
+ private uncachedRequests = 0;
107
+ private uncachedRequestBytes = 0;
108
+
104
109
  constructor(
105
110
  private readonly logger: MiLogger,
106
111
  private readonly clientDownload: ClientDownload,
@@ -341,6 +346,18 @@ export class DownloadDriver implements BlobDriver, AsyncDisposable {
341
346
  });
342
347
  }
343
348
 
349
+ /**
350
+ * Operational metrics for a monitoring panel. Serv cache metrics are reported separately
351
+ * (different owner) — the panel composes both.
352
+ */
353
+ public getMetrics(): BlobDriverMetrics {
354
+ return {
355
+ uncachedRequests: this.uncachedRequests,
356
+ uncachedRequestBytes: this.uncachedRequestBytes,
357
+ ...this.clientDownload.metrics(),
358
+ };
359
+ }
360
+
344
361
  private async getContentImpl({
345
362
  handle,
346
363
  options,
@@ -399,12 +416,24 @@ export class DownloadDriver implements BlobDriver, AsyncDisposable {
399
416
 
400
417
  if (filePath) return await withFileContent({ path: filePath, range, signal, handler });
401
418
 
419
+ if (bypassRangesCache) this.uncachedRequests++;
420
+
402
421
  return await this.clientDownload.withBlobContent(
403
422
  result.info,
404
423
  { signal },
405
424
  options,
406
425
  async (content, size) => {
407
- if (bypassRangesCache) return await handler(content, size);
426
+ if (bypassRangesCache) {
427
+ const counted = content.pipeThrough(
428
+ new TransformStream<Uint8Array, Uint8Array>({
429
+ transform: (chunk, controller) => {
430
+ this.uncachedRequestBytes += chunk.byteLength;
431
+ controller.enqueue(chunk);
432
+ },
433
+ }),
434
+ );
435
+ return await handler(counted, size);
436
+ }
408
437
 
409
438
  const [handlerStream, cacheStream] = content.tee();
410
439