querysub 0.589.0 → 0.591.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.
@@ -1,6 +1,4 @@
1
1
  import { cacheLimited, lazy } from "socket-function/src/caching";
2
- import { nestArchives } from "../../../-a-archives/archives";
3
- import { getArchivesBackblaze } from "../../../-a-archives/archivesBackBlaze";
4
2
  import { archiveJSONT } from "../../../-a-archives/archivesJSONT";
5
3
  import { Querysub } from "../../../4-querysub/Querysub";
6
4
  import { getDomain, isPublic } from "../../../config";
@@ -23,6 +21,7 @@ import { showingManagementURL, managementPageURL } from "../../managementPages";
23
21
  import { atomic } from "../../../2-proxy/PathValueProxyWatcher";
24
22
  import { magenta } from "socket-function/src/formatting/logColors";
25
23
  import { TicketServiceBase } from "../errorTickets/tickets";
24
+ import { getArchives2 } from "../../../-a-archives/archives2";
26
25
 
27
26
 
28
27
  export type SuppressionEntry = {
@@ -36,8 +35,7 @@ export type SuppressionEntry = {
36
35
  createdTime: number;
37
36
  lastUpdatedTime: number;
38
37
  };
39
- export const suppression = archiveJSONT<SuppressionEntry>(() => nestArchives("logs/error-suppression/", getArchivesBackblaze(getDomain())));
40
- let suppressionCache: SuppressionEntry[] = [];
38
+ const suppression = archiveJSONT<SuppressionEntry>(() => getArchives2("logs/error-suppression/"));
41
39
 
42
40
  // In-memory Discord notification throttling
43
41
  const timeInHour = 60 * 60 * 1000;
@@ -50,17 +48,8 @@ let pendingDiscordNotifications = new Map<string, {
50
48
  errorCount: number;
51
49
  isNew: boolean;
52
50
  }>();
53
- let ensureWatching = lazy(async () => {
54
- await runInfinitePollCallAtStart(timeInMinute * 5, updateNow);
55
- });
56
- async function updateNow() {
57
- suppressionCache = await suppression.values();
58
- // Can't wait, because that will cause a cyclic loop
59
- void reapplySuppressions();
60
- }
61
51
  async function getSuppressionEntries(): Promise<SuppressionEntry[]> {
62
- await ensureWatching();
63
- return suppressionCache;
52
+ return await suppression.values();
64
53
  }
65
54
 
66
55
  let getMatcher = cacheLimited(10000, (text: string) => createMatchesPattern(Buffer.from(text), false));
@@ -157,12 +146,6 @@ async function reapplySuppressions() {
157
146
  }
158
147
  }
159
148
 
160
- async function onSuppressionsChanged() {
161
- // NOTE: Update Now also reapplies suppression internally. after it finishes the update.
162
- void updateNow();
163
- await reapplySuppressions();
164
- }
165
-
166
149
  type PatternResponse = {
167
150
  pattern: string;
168
151
  };
@@ -246,12 +229,13 @@ const trySendDiscordNotificationsBase = batchFunction({ delay: 1000 * 3 }, async
246
229
  return;
247
230
  }
248
231
 
232
+ const suppressionEntries = await getSuppressionEntries();
249
233
 
250
234
  const newSuppressions: Array<{ id: string; count: number; pattern: string }> = [];
251
235
  const existingSuppressions: Array<{ id: string; count: number; pattern: string }> = [];
252
236
 
253
237
  for (const [suppressionId, data] of pendingDiscordNotifications.entries()) {
254
- const suppressionEntry = suppressionCache.find(e => e.id === suppressionId);
238
+ const suppressionEntry = suppressionEntries.find(e => e.id === suppressionId);
255
239
  if (!suppressionEntry) continue;
256
240
 
257
241
  if (data.isNew) {
@@ -309,6 +293,9 @@ export async function exposeErrorWatchService() {
309
293
  SocketFunction.expose(ErrorNotificationServiceBase);
310
294
  SocketFunction.expose(TicketServiceBase);
311
295
 
296
+ // Other nodes edit suppressions too, so periodically re-check the unmatched errors against the current entries.
297
+ void runInfinitePollCallAtStart(timeInMinute * 5, reapplySuppressions);
298
+
312
299
  let errorLogs = await getErrorLogs();
313
300
  for await (let error of watchAllValues(errorLogs)) {
314
301
  try {
@@ -348,8 +335,7 @@ export async function exposeErrorWatchService() {
348
335
  lastUpdatedTime: Date.now(),
349
336
  };
350
337
 
351
- suppressionCache.push(newEntry);
352
- void suppression.set(newEntry.id, newEntry);
338
+ await suppression.set(newEntry.id, newEntry);
353
339
  applySuppression(error, newEntry);
354
340
  queueDiscordNotification(newEntry.id, 1, true);
355
341
 
@@ -426,39 +412,22 @@ class ErrorNotificationService {
426
412
  }
427
413
 
428
414
  public async setSuppressionEntry(entry: SuppressionEntry) {
429
- let suppressionPromise = suppression.set(entry.id, entry);
430
- let prevEntry = suppressionCache.findIndex(e => e.id === entry.id);
431
- if (prevEntry !== -1) {
432
- suppressionCache[prevEntry] = entry;
433
- } else {
434
- suppressionCache.push(entry);
435
- }
436
- await onSuppressionsChanged();
437
- void suppressionPromise.finally(() => {
438
- void onSuppressionsChanged();
439
- });
415
+ await suppression.set(entry.id, entry);
416
+ await reapplySuppressions();
440
417
  }
441
418
 
442
419
  public async deleteSuppressionEntry(id: string) {
443
- let suppressionPromise = suppression.delete(id);
444
- let prevEntry = suppressionCache.findIndex(e => e.id === id);
445
- if (prevEntry !== -1) {
446
- suppressionCache.splice(prevEntry, 1);
447
- }
448
- await onSuppressionsChanged();
449
- void suppressionPromise.finally(() => {
450
- void onSuppressionsChanged();
451
- });
420
+ await suppression.delete(id);
421
+ await reapplySuppressions();
452
422
  }
453
423
 
454
424
  public async updateSuppressionNotes(id: string, notes: string | undefined) {
455
- let entry = suppressionCache.find(e => e.id === id);
425
+ let entries = await getSuppressionEntries();
426
+ let entry = entries.find(e => e.id === id);
456
427
  if (!entry) {
457
428
  throw new Error(`Suppression entry ${id} not found`);
458
429
  }
459
- let updatedEntry = { ...entry, notes, lastUpdatedTime: Date.now() };
460
- suppressionCache[suppressionCache.findIndex(e => e.id === id)] = updatedEntry;
461
- void suppression.set(id, updatedEntry);
430
+ await suppression.set(id, { ...entry, notes, lastUpdatedTime: Date.now() });
462
431
  }
463
432
  }
464
433
 
@@ -384,6 +384,9 @@ class ManagementRoot extends qreact.Component {
384
384
  .ManagementRoot-page a {
385
385
  color: hsl(210, 75%, 50%);
386
386
  }
387
+ .ATag-current {
388
+ color: hsl(270, 75%, 50%)!important;
389
+ }
387
390
  `}
388
391
  </style>
389
392
  <div class={css.fillWidth.hbox(20, 4).wrap.hsl(245, 25, 80).pad2(20, 4).pointerEvents("all")}>
@@ -1,6 +1,6 @@
1
1
  module.allowclient = true;
2
2
 
3
- import { ArchiveSnapshotOverview, ArchiveSnapshotRead, downloadSnapshot, getSnapshot, getSnapshotList, loadSnapshot, saveSnapshot, uploadSnapshot } from "../../0-path-value-core/archiveLocks/archiveSnapshots";
3
+ import { ArchiveSnapshotList, ArchiveSnapshotOverview, ArchiveSnapshotRead, downloadSnapshot, getLiveSnapshotList, getSavedSnapshotList, getSnapshot, loadSnapshot, saveSnapshot, uploadSnapshot } from "../../0-path-value-core/archiveLocks/archiveSnapshots";
4
4
  import { qreact } from "../../4-dom/qreact";
5
5
  import { SocketFunction } from "socket-function/SocketFunction";
6
6
  import { getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
@@ -9,7 +9,8 @@ import { css } from "typesafecss";
9
9
  import { Querysub, t } from "../../4-querysub/Querysub";
10
10
  import { ATag, Anchor } from "../../library-components/ATag";
11
11
  import { URLParam } from "../../library-components/URLParam";
12
- import { formatDateTime, formatNumber } from "socket-function/src/formatting/format";
12
+ import { formatDateTime, formatNumber, formatTime } from "socket-function/src/formatting/format";
13
+ import { sort } from "socket-function/src/misc";
13
14
  import { Button } from "../../library-components/Button";
14
15
  import { showModal } from "../../5-diagnostics/Modal";
15
16
  import { FullscreenModal } from "../../5-diagnostics/FullscreenModal";
@@ -26,6 +27,7 @@ import { getSyncedController } from "../../library-components/SyncedController";
26
27
 
27
28
  const selectedSnapshot = new URLParam("selectedSnapshot", "");
28
29
  export class SnapshotViewer extends qreact.Component {
30
+ static renderInProgress = true;
29
31
  state = t.state({
30
32
  // file => true
31
33
  expanded: t.lookup(t.boolean),
@@ -35,12 +37,23 @@ export class SnapshotViewer extends qreact.Component {
35
37
  });
36
38
  render() {
37
39
  let controller = SnapshotViewerSynced(getBrowserUrlNode());
38
- let snapshotList = controller.getSnapshotList();
39
- if (!snapshotList) return <div>Loading...</div>;
40
+ let savedList = controller.getSavedSnapshotList();
41
+ let liveList = controller.getLiveSnapshotList();
42
+ let snapshotList: ArchiveSnapshotOverview[] = [];
43
+ if (liveList) snapshotList.push(...liveList.snapshots);
44
+ if (savedList) snapshotList.push(...savedList.snapshots);
45
+ sort(snapshotList, x => -x.time);
46
+ let loadStatus = (
47
+ <div class={css.vbox(2)}>
48
+ <div>{liveList ? `Live snapshot list loaded in ${formatTime(liveList.loadTime)}` : "Loading live snapshot list..."}</div>
49
+ <div>{savedList ? `Snapshot list loaded in ${formatTime(savedList.loadTime)}` : "Loading snapshot list..."}</div>
50
+ </div>
51
+ );
40
52
  let file = selectedSnapshot.value;
41
53
  if (!file || typeof file !== "string") {
42
54
  return (
43
55
  <div class={css.pad2(10).vbox(10)}>
56
+ {loadStatus}
44
57
  <h1>Snapshots are taken before every change. "zombie" means the file exists, but it has no confirm (so it will be ignored).</h1>
45
58
  <div class={css.hbox(10)}>
46
59
  <Button onClick={() => triggerSnapshotUpload(this)}>
@@ -90,7 +103,11 @@ export class SnapshotViewer extends qreact.Component {
90
103
 
91
104
  const entry = snapshotList.find(x => x.file === file);
92
105
  let snapshotInfo = controller.getSnapshot(file);
93
- if (!entry) return <div>Snapshot not found, {file}</div>;
106
+ if (!entry) {
107
+ let stillLoading = file === "live" ? !liveList : !savedList;
108
+ if (stillLoading) return <div>Loading...</div>;
109
+ return <div>Snapshot not found, {file}</div>;
110
+ }
94
111
  return (
95
112
  <div class={css.pad2(10).vbox(10)}>
96
113
  <div class={css.hbox(10)}>
@@ -237,8 +254,12 @@ async function seriousConfirm(config: {
237
254
  }
238
255
 
239
256
  class SnapshotViewerControllerBase {
240
- public async getSnapshotList(): Promise<ArchiveSnapshotOverview[]> {
241
- return await getSnapshotList();
257
+ public async getSavedSnapshotList(): Promise<ArchiveSnapshotList> {
258
+ return await getSavedSnapshotList();
259
+ }
260
+
261
+ public async getLiveSnapshotList(): Promise<ArchiveSnapshotList> {
262
+ return await getLiveSnapshotList();
242
263
  }
243
264
 
244
265
  public async loadSnapshot(config: {
@@ -271,7 +292,8 @@ export const SnapshotViewerController = SocketFunction.register(
271
292
  "SnapshotViewerController-a78ae3cf-5024-49da-b71a-f064a5d4b5f4",
272
293
  new SnapshotViewerControllerBase(),
273
294
  () => ({
274
- getSnapshotList: {},
295
+ getSavedSnapshotList: {},
296
+ getLiveSnapshotList: {},
275
297
  loadSnapshot: {},
276
298
  getSnapshot: {},
277
299
  saveLiveSnapshot: {},
@@ -288,7 +310,8 @@ export const SnapshotViewerController = SocketFunction.register(
288
310
  );
289
311
  const SnapshotViewerSynced = getSyncedController(SnapshotViewerController, {
290
312
  reads: {
291
- getSnapshotList: ["snapshots"],
313
+ getSavedSnapshotList: ["snapshots"],
314
+ getLiveSnapshotList: ["snapshots"],
292
315
  getSnapshot: ["snapshots"],
293
316
  },
294
317
  writes: {
@@ -1,7 +1,8 @@
1
1
  import { runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
2
2
  import { FormattedMeasureTable, MeasureProfile, logMeasureTable, startMeasure } from "socket-function/src/profiling/measure";
3
+ import { StatsValue, addToStats, createStatsValue } from "socket-function/src/profiling/stats";
3
4
  import { logErrors } from "../errors";
4
- import { isNode, sort } from "socket-function/src/misc";
5
+ import { isNode, sort, timeInHour } from "socket-function/src/misc";
5
6
  import debugbreak from "debugbreak";
6
7
  import { registerPeriodic } from "./periodic";
7
8
  import { getOwnMachineId } from "sliftutils/misc/https/certs";
@@ -15,6 +16,70 @@ import { isPublic } from "../config";
15
16
 
16
17
  let lastProfile: MeasureProfile | undefined;
17
18
 
19
+ const MEASURE_CHUNK_MAX_ENTRIES = 100;
20
+ // Matches the deeper of the two watchdog console tables, so the stored breakdown groups the same way the logs do.
21
+ const MEASURE_CHUNK_MERGE_DEPTH = 2;
22
+ const MEASURE_CHUNK_RETENTION = timeInHour;
23
+
24
+ export type MeasureChunkEntry = {
25
+ name: string;
26
+ ownTime: StatsValue;
27
+ totalTime: StatsValue;
28
+ stillOpenCount: number;
29
+ };
30
+ export type MeasureChunk = {
31
+ startTime: number;
32
+ endTime: number;
33
+ /** Sum over ALL groupings' own time, including groupings dropped past MEASURE_CHUNK_MAX_ENTRIES. */
34
+ profiledTime: number;
35
+ entries: MeasureChunkEntry[];
36
+ };
37
+ export type MeasureSummary = {
38
+ startTime: number;
39
+ endTime: number;
40
+ profiledTime: number;
41
+ };
42
+
43
+ let measureChunks: MeasureChunk[] = [];
44
+
45
+ function storeMeasureChunk(profile: MeasureProfile) {
46
+ let merged = new Map<string, MeasureChunkEntry>();
47
+ for (let entry of Object.values(profile.entries)) {
48
+ let key = entry.name.split("|").slice(0, MEASURE_CHUNK_MERGE_DEPTH).join("|");
49
+ let existing = merged.get(key);
50
+ if (!existing) {
51
+ existing = { name: key, ownTime: createStatsValue(), totalTime: createStatsValue(), stillOpenCount: 0 };
52
+ merged.set(key, existing);
53
+ }
54
+ addToStats(existing.ownTime, entry.ownTime);
55
+ addToStats(existing.totalTime, entry.totalTime);
56
+ existing.stillOpenCount += entry.stillOpenCount;
57
+ }
58
+ let entries = sort(Array.from(merged.values()), x => -x.ownTime.sum);
59
+ let profiledTime = entries.reduce((total, entry) => total + entry.ownTime.sum, 0);
60
+ measureChunks.push({
61
+ startTime: profile.startTime,
62
+ endTime: profile.endTime,
63
+ profiledTime,
64
+ entries: entries.slice(0, MEASURE_CHUNK_MAX_ENTRIES),
65
+ });
66
+ let cutoff = Date.now() - MEASURE_CHUNK_RETENTION;
67
+ measureChunks = measureChunks.filter(x => x.endTime >= cutoff);
68
+ }
69
+
70
+ export function getLastMeasureSummary(): MeasureSummary | undefined {
71
+ let chunk = measureChunks[measureChunks.length - 1];
72
+ if (!chunk) return undefined;
73
+ return {
74
+ startTime: chunk.startTime,
75
+ endTime: chunk.endTime,
76
+ profiledTime: chunk.profiledTime,
77
+ };
78
+ }
79
+ export function getMeasureBreakdown(range: { startTime: number; endTime: number }): MeasureChunk | undefined {
80
+ return measureChunks.find(x => x.startTime === range.startTime && x.endTime === range.endTime);
81
+ }
82
+
18
83
  addStatPeriodic({
19
84
  title: "Performance|% Profiled",
20
85
  getValue: () => {
@@ -94,6 +159,7 @@ function logProfileMeasuresTimingsNow() {
94
159
  process.stdout.write(`\n${debuggerValue}\n`);
95
160
  }
96
161
 
162
+ storeMeasureChunk(profile);
97
163
  lastProfile = profile;
98
164
  // if (isNode()) {
99
165
  // if (SocketFunction.mountedNodeId) {
@@ -76,6 +76,7 @@ export class ATag extends qreact.Component<ATagProps> {
76
76
  .color("inherit", "important")
77
77
  )
78
78
 
79
+ + (isCurrent && " ATag-current" || "")
79
80
  }
80
81
  onClick={e => {
81
82
  if (this.props.rawLink) return;
@@ -275,6 +275,7 @@ export function getSyncedController<T extends {
275
275
  let promise = atomic(obj.promise);
276
276
 
277
277
  // NOTE: If we are invalidated when the promise is running, nothing happens (as we don't watch invalidated if we are running with a promise). BUT, if the promise isn't running, we will run again, and start running again. In this way we don't queue up a lot if we invalidate a lot, but we do always run again after the invalidation to get the latest result!
278
+ let currentPromise: Promise<unknown> | undefined;
278
279
  if (!promise && (!result || atomic(obj.invalidated))) {
279
280
  obj.invalidated = false;
280
281
  let timeStart = Date.now();
@@ -285,6 +286,7 @@ export function getSyncedController<T extends {
285
286
  }
286
287
  }
287
288
  let promiseObjBase = new PromiseObj<unknown>();
289
+ currentPromise = promiseObjBase.promise;
288
290
  let promiseObj: { promise: Promise<unknown> } = {
289
291
  promise: promiseObjBase.promise,
290
292
  };
@@ -348,6 +350,12 @@ export function getSyncedController<T extends {
348
350
  return result.result;
349
351
  }
350
352
  }
353
+ currentPromise = currentPromise || promise?.promise;
354
+ if (currentPromise) {
355
+ Querysub.triggerOnPromiseFinish(currentPromise, {
356
+ waitReason: `Waiting for ${fncName} to finish`,
357
+ });
358
+ }
351
359
  return undefined;
352
360
  }
353
361
  call.promise = (...args: any[]) => {