querysub 0.588.0 → 0.590.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.588.0",
3
+ "version": "0.590.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -71,8 +71,8 @@
71
71
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
72
72
  "pako": "^2.1.0",
73
73
  "peggy": "^5.0.6",
74
- "sliftutils": "^1.7.59",
75
- "socket-function": "^1.2.26",
74
+ "sliftutils": "^1.7.65",
75
+ "socket-function": "^1.2.27",
76
76
  "terser": "^5.31.0",
77
77
  "typenode": "^6.6.1",
78
78
  "typesafecss": "^0.32.0",
@@ -48,13 +48,15 @@ const getCacheDir = lazy(() => {
48
48
  const TEMP_SUFFIX = ".tmp";
49
49
  const TEMP_THRESHOLD = timeInHour * 3;
50
50
 
51
- function getTempFilePath() {
51
+ export function getTempFilePath() {
52
52
  return os.tmpdir() + "/" + nextId() + TEMP_SUFFIX;
53
53
  }
54
54
 
55
- const LARGE_FILE_CHUNK = 1024 * 1024 * 32;
55
+ export const LARGE_FILE_CHUNK = 1024 * 1024 * 32;
56
56
  const CACHE_SUFFIX = ".cache";
57
- export function getArchiveCachePath(archives: Archives, key: string): string {
57
+ /** The cache only needs the debug name (for cache-path hashing), so any archives flavor (old Archives or IArchives) works. */
58
+ export type CacheKeySource = { getDebugName(): string };
59
+ export function getArchiveCachePath(archives: CacheKeySource, key: string): string {
58
60
  // Remove all "/backblaze/" values from the path, as they make equivalent paths
59
61
  // hash differently, which we don't want.
60
62
  let fullFileName = (archives.getDebugName() + "/" + key).replaceAll("//", "/").replaceAll("backblaze/", "/");
@@ -134,7 +136,7 @@ const getDiskMetricsBase = async () => {
134
136
  }
135
137
 
136
138
  // We require our input to be a temp file, so our update can be atomic
137
- async function addCacheFile(archives: Archives, key: string, sourceTempFile: string): Promise<void> {
139
+ async function addCacheFile(archives: CacheKeySource, key: string, sourceTempFile: string): Promise<void> {
138
140
  let path = getArchiveCachePath(archives, key);
139
141
  let hasFile = fileSizes.some(x => x.path === path);
140
142
  if (hasFile) {
@@ -187,7 +189,7 @@ const getDiskMetricsBase = async () => {
187
189
  }
188
190
  }
189
191
  async function getCacheFile(
190
- archives: Archives,
192
+ archives: CacheKeySource,
191
193
  key: string,
192
194
  config?: { range?: { start: number; end: number } }
193
195
  ): Promise<Buffer | undefined> {
@@ -215,7 +217,7 @@ const getDiskMetricsBase = async () => {
215
217
  }
216
218
  return buffer;
217
219
  }
218
- async function delCacheFile(archives: Archives, key: string): Promise<void> {
220
+ async function delCacheFile(archives: CacheKeySource, key: string): Promise<void> {
219
221
  let path = getArchiveCachePath(archives, key);
220
222
  if (removeFile(path)) {
221
223
  try {
@@ -235,7 +237,7 @@ const getDiskMetricsBase = async () => {
235
237
  };
236
238
  let curSeqNum = 1;
237
239
  const getDiskMetricSeqNum = cache((seqNum: number) => getDiskMetricsBase());
238
- async function getDiskMetrics() {
240
+ export async function getDiskMetrics() {
239
241
  return getDiskMetricSeqNum(curSeqNum);
240
242
  }
241
243
 
@@ -0,0 +1,153 @@
1
+ import fs from "fs";
2
+ import { GetConfig, IArchives, SetConfig } from "sliftutils/storage/IArchives";
3
+ import { getArchiveCachePath, getDiskMetrics, getTempFilePath, LARGE_FILE_CHUNK } from "./archiveCache";
4
+
5
+ let cacheArchives2Symbol = Symbol("cacheArchives2");
6
+
7
+ /** IMPORTANT! The cache assumes file contents are immutable: files are only created and deleted, never mutated in place.
8
+ * - Shares the disk cache pool (directory, size limits, eviction) with the old wrapArchivesWithCache.
9
+ */
10
+ export function wrapArchivesWithCache2(archives: IArchives, rootConfig?: {
11
+ /** Files are never deleted either, so we can serve cache hits (and even getInfo) without checking the source. */
12
+ immutable?: boolean;
13
+ }): IArchives & {
14
+ debugGetPath(key: string): string;
15
+ } {
16
+ if (cacheArchives2Symbol in archives) {
17
+ return archives as any;
18
+ }
19
+
20
+ async function cacheBuffer(fileName: string, data: Buffer) {
21
+ let metrics = await getDiskMetrics();
22
+ const tempPath = getTempFilePath();
23
+ await fs.promises.writeFile(tempPath, data);
24
+ await metrics.addCacheFile(archives, fileName, tempPath);
25
+ }
26
+
27
+ async function setLargeFile(config: { path: string; lastModified?: number; getNextData(): Promise<Buffer | undefined>; }) {
28
+ const tempPath = getTempFilePath();
29
+ let handle: fs.promises.FileHandle | undefined;
30
+ try {
31
+ handle = await fs.promises.open(tempPath, "w");
32
+ let pos = 0;
33
+ while (true) {
34
+ let data = await config.getNextData();
35
+ if (!data?.length) break;
36
+ await handle.write(data, 0, data.length, pos);
37
+ pos += data.length;
38
+ }
39
+ } finally {
40
+ if (handle) {
41
+ try {
42
+ await handle.close();
43
+ } catch { }
44
+ }
45
+ }
46
+
47
+ let metrics = await getDiskMetrics();
48
+ await metrics.addCacheFile(archives, config.path, tempPath);
49
+ let cachePath = getArchiveCachePath(archives, config.path);
50
+
51
+ let pos = 0;
52
+ let cacheHandle: fs.promises.FileHandle = await fs.promises.open(cachePath, "r");
53
+ let data = Buffer.alloc(LARGE_FILE_CHUNK);
54
+ async function getNextData(): Promise<Buffer | undefined> {
55
+ try {
56
+ let read = await cacheHandle.read(data, 0, LARGE_FILE_CHUNK, pos);
57
+ if (read.bytesRead === 0) return undefined;
58
+ let curData = data;
59
+ if (read.bytesRead < LARGE_FILE_CHUNK) {
60
+ curData = curData.slice(0, read.bytesRead);
61
+ }
62
+ pos += read.bytesRead;
63
+ return curData;
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ }
68
+ try {
69
+ await archives.setLargeFile({
70
+ ...config,
71
+ getNextData,
72
+ });
73
+ } finally {
74
+ await cacheHandle.close();
75
+ }
76
+ }
77
+
78
+ return {
79
+ [cacheArchives2Symbol]: true,
80
+ debugGetPath: (key: string) => getArchiveCachePath(archives, key),
81
+
82
+ getDebugName: () => archives.getDebugName(),
83
+ hasWriteAccess: () => archives.hasWriteAccess(),
84
+ getConfig: () => archives.getConfig(),
85
+ getURL: (path: string) => archives.getURL(path),
86
+ find: (prefix, config) => archives.find(prefix, config),
87
+ findInfo: (prefix, config) => archives.findInfo(prefix, config),
88
+ getChangesAfter2: (config) => archives.getChangesAfter2(config),
89
+ getSyncStatus: archives.getSyncStatus && (() => archives.getSyncStatus!()),
90
+
91
+ get: async (fileName: string, config?: GetConfig) => {
92
+ let metrics = await getDiskMetrics();
93
+ // Get info to at least see if it is removed or not. This makes sure we don't get in
94
+ // a bad state, at least for immutable files (for mutable files we don't have an easy
95
+ // way to check)
96
+ if (!rootConfig?.immutable) {
97
+ let info = await archives.getInfo(fileName);
98
+ if (!info) {
99
+ // If it is gone remotely, remove it from the cache, to save space.
100
+ await metrics.delCacheFile(archives, fileName);
101
+ return undefined;
102
+ }
103
+ }
104
+ let buffer = await metrics.getCacheFile(archives, fileName, config);
105
+ if (buffer) return buffer;
106
+ let result = await archives.get(fileName);
107
+ if (result) {
108
+ await cacheBuffer(fileName, result);
109
+ let range = config?.range;
110
+ if (range) {
111
+ result = result.slice(range.start, range.end);
112
+ }
113
+ }
114
+ return result;
115
+ },
116
+ get2: async (fileName: string, config?: GetConfig) => {
117
+ let result = await archives.get2(fileName, config);
118
+ // Range reads are partial, so they can't populate the cache.
119
+ if (result && !config?.range) {
120
+ await cacheBuffer(fileName, result.data);
121
+ }
122
+ return result;
123
+ },
124
+ getInfo: async (fileName: string) => {
125
+ // When the archives are immutable, the local cache file is a byte-for-byte copy, so we can
126
+ // answer getInfo from a local stat and skip the (slow, network) getInfo on the underlying archives.
127
+ if (rootConfig?.immutable) {
128
+ try {
129
+ let stat = await fs.promises.stat(getArchiveCachePath(archives, fileName));
130
+ return { writeTime: stat.mtimeMs, size: stat.size };
131
+ } catch {
132
+ // Not in our cache yet — fall through to the source.
133
+ }
134
+ }
135
+ return archives.getInfo(fileName);
136
+ },
137
+ set: async (fileName: string, data: Buffer, config?: SetConfig) => {
138
+ let writtenKey = await archives.set(fileName, data, config);
139
+ // VARIABLE_SHARD keys materialize into a different key on write, in which case
140
+ // fileName isn't the readable key, so caching under it would be wrong.
141
+ if (writtenKey === fileName) {
142
+ await cacheBuffer(fileName, data);
143
+ }
144
+ return writtenKey;
145
+ },
146
+ setLargeFile,
147
+ del: async (fileName: string) => {
148
+ let metrics = await getDiskMetrics();
149
+ await metrics.delCacheFile(archives, fileName);
150
+ await archives.del(fileName);
151
+ },
152
+ };
153
+ }
@@ -8,6 +8,7 @@ import debugbreak from "debugbreak";
8
8
  import { isClient } from "../config2";
9
9
  import { wrapArchivesWithCache } from "./archiveCache";
10
10
  import { Args } from "socket-function/src/types";
11
+ import { IArchives } from "sliftutils/storage/IArchives";
11
12
 
12
13
  export interface Archives {
13
14
  getDebugName(): string;
@@ -104,8 +105,8 @@ export function createArchivesOverride<T extends Partial<Archives>>(
104
105
  };
105
106
  }
106
107
 
107
- export function nestArchives(path: string, archives: Archives): Archives {
108
- if (!path) return archives;
108
+ export function nestArchives(path: string, archives: Archives): Archives & IArchives {
109
+ if (!path) return archives as any;
109
110
  if (!path.endsWith("/")) {
110
111
  path = path + "/";
111
112
  }
@@ -115,6 +116,7 @@ export function nestArchives(path: string, archives: Archives): Archives {
115
116
  }
116
117
  return file;
117
118
  }
119
+ // NOTE: Just ignore the errors in here. We're going to be getting rid of this soon enough.
118
120
  return {
119
121
  getDebugName: () => archives.getDebugName() + "/" + path,
120
122
  get: (fileName: string, config) => archives.get(path + stripFilePrefix(fileName), config),
@@ -1,4 +1,4 @@
1
- import { IArchives, RemoteConfigBase } from "sliftutils/storage/IArchives";
1
+ import { ChangesAfterConfig, GetConfig, IArchives, RemoteConfigBase, SetConfig } from "sliftutils/storage/IArchives";
2
2
  import { createArchives } from "sliftutils/storage/remoteStorage/createArchives";
3
3
  import { cache, cacheJSONArgsEqual } from "socket-function/src/caching";
4
4
  import { formatDateTime } from "socket-function/src/formatting/format";
@@ -39,8 +39,10 @@ function createSourceWindows(
39
39
  }
40
40
 
41
41
  function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
42
+ // REMEMBER TO INCREMENT VERSION WHEN YOU MAKE CHANGES!
43
+ // REMEMBER TO RERUN initArchives2 if you change this (It's probably something you should call in your entry point.
42
44
  const ONTARIO = `https://99-250-124-91.querysubtest.com:5234/file/${STORAGE_ACCOUNT}/${bucket}/storage/storagerouting.json`;
43
- const HETZNER = `https://65-109-93-113.querysubtest.com:5233/file/${STORAGE_ACCOUNT}/${bucket}/storage/storagerouting.json`;
45
+ const HETZNER = `https://65-109-93-113.querysubtest.com:5234/file/${STORAGE_ACCOUNT}/${bucket}/storage/storagerouting.json`;
44
46
  const BACKBLAZE = `https://f002.backblazeb2.com/file/${bucket}/storage/storagerouting.json`;
45
47
  let sources = createSourceWindows(overrides, [
46
48
  {
@@ -66,7 +68,7 @@ function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
66
68
  ]);
67
69
 
68
70
  return createArchives({
69
- version: 15,
71
+ version: 16,
70
72
  sources,
71
73
  });
72
74
  }
@@ -79,17 +81,18 @@ const archivesBuilderCache = cacheJSONArgsEqual((domain: string, overrides: Part
79
81
  });
80
82
  function nestBucket(folder: string, archives: ReturnType<typeof createArchives>) {
81
83
  if (!folder) return archives;
84
+ folder = folder.replaceAll("\\", "/");
82
85
  if (!folder.endsWith("/")) {
83
86
  folder = folder + "/";
84
87
  }
85
88
  return {
86
- get: (path: string) => archives.get(folder + path),
87
- set: (path: string, data: Buffer) => archives.set(folder + path, data),
89
+ get: (path: string, config?: GetConfig) => archives.get(folder + path, config),
90
+ set: (path: string, data: Buffer, config?: SetConfig) => archives.set(folder + path, data, config),
88
91
  del: (path: string) => archives.del(folder + path),
89
- find: (prefix: string) => archives.find(folder + prefix),
90
- findInfo: (prefix: string) => archives.findInfo(folder + prefix),
91
- get2: (path: string) => archives.get2(folder + path),
92
- setLargeFile: (config: { path: string; getNextData(): Promise<Buffer | undefined>; }) => archives.setLargeFile({ path: folder + config.path, getNextData: config.getNextData }),
92
+ find: async (prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }) => (await archives.find(folder + prefix, config)).map(x => x.slice(folder.length)),
93
+ findInfo: async (prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }) => (await archives.findInfo(folder + prefix, config)).map(x => ({ ...x, path: x.path.slice(folder.length) })),
94
+ get2: (path: string, config?: GetConfig) => archives.get2(folder + path, config),
95
+ setLargeFile: (config: { path: string; lastModified?: number; getNextData(): Promise<Buffer | undefined>; }) => archives.setLargeFile({ ...config, path: folder + config.path }),
93
96
  getInfo: (path: string) => archives.getInfo(folder + path),
94
97
  getDebugName: () => archives.getDebugName() + "/" + folder,
95
98
  hasWriteAccess: () => archives.hasWriteAccess(),
@@ -101,6 +104,19 @@ function nestBucket(folder: string, archives: ReturnType<typeof createArchives>)
101
104
  return (path: string) => getUrls(folder + path);
102
105
  },
103
106
  getShardKey: async (path: string) => (await archives.getShardKey(folder + path)).slice(folder.length),
107
+ // NOTE: It's recommended that you only store the keys and don't store the URLs. If you absolutely need a URL, it's best to call it lazily.
108
+ getGetFastURLs: async () => {
109
+ let getFastUrls = await archives.getGetFastURLs();
110
+ return (path: string) => getFastUrls(folder + path);
111
+ },
112
+ // getFast has less latency than get. Writes won't be available immediately after sets however. If it's a buffered archives the delay will be bufferDelay, ex 5 minutes, instead of a few seconds.
113
+ getFast: async (path: string, config?: GetConfig) => {
114
+ return await archives.getFast(folder + path, config);
115
+ },
116
+ getChangesAfter2: async (config: ChangesAfterConfig) => {
117
+ let changes = await archives.getChangesAfter2(config);
118
+ return changes.filter(x => x.path.startsWith(folder)).map(x => ({ ...x, path: x.path.slice(folder.length) }));
119
+ },
104
120
  };
105
121
  }
106
122
 
@@ -1,6 +1,7 @@
1
1
  import { lazy } from "socket-function/src/caching";
2
2
  import { Archives } from "./archives";
3
3
  import { green } from "socket-function/src/formatting/logColors";
4
+ import { IArchives } from "sliftutils/storage/IArchives";
4
5
 
5
6
  export type ArchiveT<T> = {
6
7
  get(key: string): Promise<T | undefined>;
@@ -12,7 +13,7 @@ export type ArchiveT<T> = {
12
13
  [Symbol.asyncIterator](): AsyncIterator<[string, T]>;
13
14
  };
14
15
 
15
- export function archiveJSONT<T>(archives: () => Archives): ArchiveT<T> {
16
+ export function archiveJSONT<T>(archives: () => IArchives): ArchiveT<T> {
16
17
  archives = lazy(archives);
17
18
 
18
19
  let valuesCache = new Map<string, {
@@ -60,18 +61,28 @@ export function archiveJSONT<T>(archives: () => Archives): ArchiveT<T> {
60
61
 
61
62
  if (needsUpdate) {
62
63
  let maxRetries = 10;
64
+ let updated = false;
63
65
  for (let attempt = 0; attempt < maxRetries; attempt++) {
64
66
  infos = await archives().findInfo("");
65
67
 
66
- valuesCache.clear();
68
+ let newCache = new Map<string, {
69
+ createTime: number;
70
+ size: number;
71
+ value: Buffer;
72
+ }>();
67
73
  let allFound = true;
68
74
 
69
75
  await Promise.all(infos.map(async info => {
76
+ let cached = valuesCache.get(info.path);
77
+ if (cached && cached.createTime === info.createTime && cached.size === info.size) {
78
+ newCache.set(info.path, cached);
79
+ return;
80
+ }
70
81
  let buffer = await archives().get(info.path);
71
82
  if (!buffer) {
72
83
  allFound = false;
73
84
  } else {
74
- valuesCache.set(info.path, {
85
+ newCache.set(info.path, {
75
86
  createTime: info.createTime,
76
87
  size: info.size,
77
88
  value: buffer
@@ -94,10 +105,14 @@ export function archiveJSONT<T>(archives: () => Archives): ArchiveT<T> {
94
105
  }
95
106
  }
96
107
  }
97
- if (!anyChanged()) break;
108
+ if (!anyChanged()) {
109
+ valuesCache = newCache;
110
+ updated = true;
111
+ break;
112
+ }
98
113
  }
99
114
 
100
- if (needsUpdate && valuesCache.size === 0) {
115
+ if (!updated) {
101
116
  throw new Error(`Failed to get consistent snapshot of values after ${maxRetries} attempts`);
102
117
  }
103
118
  }
@@ -25,6 +25,7 @@ import { PromiseObj } from "../promise";
25
25
  import { getTrueTimeOffset } from "socket-function/time/trueTimeShim";
26
26
  import { getGitRefLive } from "../4-deploy/git";
27
27
  import { getScheduledShutdownTime } from "./scheduledShutdown";
28
+ import type { MeasureChunk, MeasureSummary } from "../diagnostics/watchdog";
28
29
  setImmediate(() => {
29
30
  import("../diagnostics/MachineThreadInfo");
30
31
  });
@@ -164,6 +165,16 @@ class NodeCapabilitiesControllerBase {
164
165
  return process.memoryUsage();
165
166
  }
166
167
 
168
+ public async getMeasureSummary(): Promise<MeasureSummary | undefined> {
169
+ let { getLastMeasureSummary } = await import("../diagnostics/watchdog");
170
+ return getLastMeasureSummary();
171
+ }
172
+
173
+ public async getMeasureBreakdown(range: { startTime: number; endTime: number }): Promise<MeasureChunk | undefined> {
174
+ let { getMeasureBreakdown } = await import("../diagnostics/watchdog");
175
+ return getMeasureBreakdown(range);
176
+ }
177
+
167
178
  public async getInspectURL() {
168
179
  return await getDebuggerUrl();
169
180
  }
@@ -204,6 +215,8 @@ export const NodeCapabilitiesController = SocketFunction.register(
204
215
  () => ({
205
216
  getMetadata: {},
206
217
  getMemoryUsage: {},
218
+ getMeasureSummary: {},
219
+ getMeasureBreakdown: {},
207
220
  getInspectURL: { hooks: [requiresNetworkTrustHook] },
208
221
  exposeExternalDebugPortOnce: { hooks: [requiresNetworkTrustHook] },
209
222
  }),
@@ -4,7 +4,7 @@ import { decodeNodeId } from "sliftutils/misc/https/certs";
4
4
  import { getOwnNodeId, getOwnNodeIdAssert } from "../../-f-node-discovery/NodeDiscovery";
5
5
  import { pathValueArchives } from "../pathValueArchives";
6
6
  import { ArchiveLocker, ArchiveTransaction } from "./ArchiveLocks";
7
- import { Archives } from "../../-a-archives/archives";
7
+ import { copyArchiveFile, IArchives } from "sliftutils/storage/IArchives";
8
8
  import debugbreak from "debugbreak";
9
9
  import { formatNumber, formatTime } from "socket-function/src/formatting/format";
10
10
  import { blue, green, magenta, red } from "socket-function/src/formatting/logColors";
@@ -33,13 +33,20 @@ const MAX_APPLY_TRIES = 10;
33
33
  const CONCURRENT_READ_COUNT = 32;
34
34
  const CONCURRENT_WRITE_COUNT = 16;
35
35
 
36
+ /** Moves by copy-then-delete (IArchives has no move). copyArchiveFile stamps the target with the source's write time, so ordering-by-writeTime survives the move. A missing source means another process already moved it, which is fine. */
37
+ async function moveArchiveFile(from: IArchives, to: IArchives, path: string): Promise<void> {
38
+ let copied = await copyArchiveFile({ from, to, path });
39
+ if (!copied) return;
40
+ await from.del(path);
41
+ }
42
+
36
43
  export function createArchiveLocker2(config: {
37
- archiveValues: Archives;
44
+ archiveValues: IArchives;
38
45
  /** IMPORTANT! This must be the same independent of archiveValues, otherwise we won't
39
46
  * lock moves between directories.
40
47
  */
41
- archiveLocks: Archives;
42
- archiveRecycleBin: Archives;
48
+ archiveLocks: IArchives;
49
+ archiveRecycleBin: IArchives;
43
50
  }): ArchiveLocker {
44
51
  let { archiveValues, archiveLocks, archiveRecycleBin } = config;
45
52
  function getArchives(key: string) {
@@ -85,11 +92,7 @@ export function createArchiveLocker2(config: {
85
92
  return;
86
93
  }
87
94
  try {
88
- await archives.move({
89
- path: key,
90
- targetPath: key,
91
- target: archiveRecycleBin,
92
- });
95
+ await moveArchiveFile(archives, archiveRecycleBin, key);
93
96
  logNodeStats(`archives|Deleted TΔ`, formatNumber, 1);
94
97
  } catch {
95
98
  // It was probably just moved by another process
@@ -126,11 +129,7 @@ export function createArchiveLocker2(config: {
126
129
  if (correctFiles.has(file)) return;
127
130
  let info = await archiveValues.getInfo(file);
128
131
  console.log(`Moving file ${file}, ${moveCount}/${valuePaths.size}, ${formatNumber(info?.size || 0)} bytes`);
129
- await archiveValues.move({
130
- path: file,
131
- targetPath: file,
132
- target: archiveRecycleBin,
133
- });
132
+ await moveArchiveFile(archiveValues, archiveRecycleBin, file);
134
133
  console.log(`Moved file ${file}, ${moveCount}/${valuePaths.size}`);
135
134
  }
136
135
  let moveFileParallel = runInParallel({ parallelCount: 8 }, moveFile);
@@ -151,11 +150,7 @@ export function createArchiveLocker2(config: {
151
150
  let isInRecycleBin = !!(await archiveRecycleBin.getInfo(file));
152
151
  if (isInRecycleBin) {
153
152
  console.log(`Restoring ${file}, ${restoreCount++}/${files.length}`);
154
- await archiveRecycleBin.move({
155
- path: file,
156
- targetPath: file,
157
- target: archiveValues,
158
- });
153
+ await moveArchiveFile(archiveRecycleBin, archiveValues, file);
159
154
  } else {
160
155
  console.log(`File ${file} is not in the recycle bin, skipping, ${restoreCount++}/${files.length}`);
161
156
  }
@@ -312,6 +307,36 @@ type StorageType = {
312
307
  * all writes should now be readable.
313
308
  */
314
309
  };
310
+ function createCheckpointTimer() {
311
+ let last = Date.now();
312
+ let spans = new Map<string, { time: number; count: number }>();
313
+ return {
314
+ start() {
315
+ last = Date.now();
316
+ spans.clear();
317
+ },
318
+ /** Attributes the time since the previous checkpoint (or start) to name, accumulating across repeat visits. */
319
+ checkpoint(name: string) {
320
+ let now = Date.now();
321
+ let span = spans.get(name);
322
+ if (!span) {
323
+ span = { time: 0, count: 0 };
324
+ spans.set(name, span);
325
+ }
326
+ span.time += now - last;
327
+ span.count++;
328
+ last = now;
329
+ },
330
+ logAndFlush(prefix: string) {
331
+ let total = Array.from(spans.values()).reduce((sum, span) => sum + span.time, 0);
332
+ let parts = Array.from(spans.entries()).map(([name, span]) => `${name}=${formatTime(span.time)}${span.count > 1 ? ` (x${span.count})` : ""}`);
333
+ console.log(blue(`${prefix} time breakdown, total ${formatTime(total)}: ${parts.join(" | ")}`));
334
+ spans.clear();
335
+ last = Date.now();
336
+ },
337
+ };
338
+ }
339
+
315
340
  export type Transaction = {
316
341
  ops: ({
317
342
  type: "create";
@@ -355,6 +380,8 @@ class TransactionLocker {
355
380
  private isTransactionValidOverride?: (transaction: Transaction, dataFiles: FileInfo[], rawDataFiles: FileInfo[]) => boolean
356
381
  ) { }
357
382
 
383
+ private perf = createCheckpointTimer();
384
+
358
385
  // #region Base File Ops
359
386
  public getConfirmKey(key: string): string {
360
387
  let { dir, name } = parsePath(key);
@@ -444,6 +471,7 @@ class TransactionLocker {
444
471
  const tryToRead = async () => {
445
472
  let time = Date.now();
446
473
  let files = await this.storage.getKeys();
474
+ this.perf.checkpoint("getKeys");
447
475
  if (this.lastFilesRead && this.lastFilesReadTime) {
448
476
  let prevFiles = new Set(this.lastFilesRead.map(a => a.file));
449
477
  let suspiciousThreshold = this.lastFilesReadTime - ARCHIVE_PROPAGATION_TIME * 10;
@@ -491,6 +519,7 @@ class TransactionLocker {
491
519
  source: tFile,
492
520
  });
493
521
  }
522
+ this.perf.checkpoint("read transaction files");
494
523
 
495
524
  // Check all of our files to see if any have changed (or if there a new files).
496
525
  // Because we don't reuse file names, or change them, it means if a file exists
@@ -501,6 +530,7 @@ class TransactionLocker {
501
530
  // (it pretty much only depends on files existing).
502
531
  {
503
532
  let filesVerify = await this.storage.getKeys();
533
+ this.perf.checkpoint("verify getKeys");
504
534
  let filesVerifySet = new Set(filesVerify.map(a => a.file));
505
535
  // If it changes while reading, read again. Otherwise, if there were no changes while reading,
506
536
  // we know the state was all the state we read, at one time (maybe not now, but at an instant
@@ -563,6 +593,7 @@ class TransactionLocker {
563
593
  //rawFiles: files.map(a => a.file),
564
594
  //confirmedFiles: Array.from(currentDataFiles.values()).map(a => a.file),
565
595
  });
596
+ this.perf.checkpoint("categorize files");
566
597
 
567
598
  return {
568
599
  rawDataFiles: files,
@@ -672,8 +703,10 @@ class TransactionLocker {
672
703
  * - Might run a transaction
673
704
  */
674
705
  public async getFiles(): Promise<FileInfo[]> {
706
+ this.perf.start();
675
707
  let obj = await this.getFilesBase();
676
708
  logNodeStateStats(`ArchiveLock Data File`, formatNumber, obj.dataFiles.length);
709
+ this.perf.logAndFlush(`getFiles (${this.storage.debugKey})`);
677
710
  return obj.dataFiles;
678
711
  }
679
712
  private transactionAppliedCount = new Map<number, number>();
@@ -731,6 +764,7 @@ class TransactionLocker {
731
764
  let waitTime = threshold - Date.now();
732
765
  console.info(`Waiting ${formatTime(waitTime)} for transaction ${activeT.seqNum} to settle.`);
733
766
  await new Promise(resolve => setTimeout(resolve, waitTime));
767
+ this.perf.checkpoint("settle wait");
734
768
  return this.getFilesBase();
735
769
  }
736
770
 
@@ -752,6 +786,7 @@ class TransactionLocker {
752
786
  await this.storage.deleteKey(t.source.file);
753
787
  }
754
788
  }
789
+ this.perf.checkpoint("delete old transactions");
755
790
 
756
791
  // Delete any create files that are WAY too old, and not confirmed
757
792
  {
@@ -780,6 +815,7 @@ class TransactionLocker {
780
815
  console.warn(`Almost deleted ${unconfirmedOldFiles.length} very old unconfirmed files. This is bad, did we miss their confirmations that first time? If we missed them twice in a row, we might literally delete the database, and need to enter recovery mode to fix it...`, { files: unconfirmedOldFiles });
781
816
  }
782
817
  }
818
+ this.perf.checkpoint("cleanup old unconfirmed files");
783
819
  }
784
820
 
785
821
 
@@ -805,6 +841,7 @@ class TransactionLocker {
805
841
  }
806
842
  }
807
843
  }
844
+ this.perf.checkpoint("cleanup old confirms");
808
845
  }
809
846
 
810
847
  return {
@@ -818,6 +855,7 @@ class TransactionLocker {
818
855
  console.log(`Applying transaction ${activeT.seqNum}, cur try ${applyCount} / ${MAX_APPLY_TRIES}`);
819
856
  }
820
857
  await this.applyTransaction(activeT);
858
+ this.perf.checkpoint("apply transaction");
821
859
  this.transactionAppliedCount.set(activeT.seqNum, applyCount + 1);
822
860
 
823
861
  // Run again, until we can be reasonable sure activeT isn't changing. We can be wrong,