sliftutils 1.7.41 → 1.7.43

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.
@@ -21,6 +21,8 @@ export declare class SourceWrapper {
21
21
  }): Promise<SourceWrapper>;
22
22
  getDebugName(): string;
23
23
  isConnected(): boolean;
24
+ /** A source whose window has passed is never read from or written to (see the window checks in ArchivesChain), and an intermediate only exists for the minutes of a deploy switchover - on either, being unreachable is expected, not a problem to report. */
25
+ private isConnectionProblemWorthReporting;
24
26
  /** Call after a request failed while isConnected() was false: starts (if not already running) the background reconnect loop. Never blocks - the failed request still throws. */
25
27
  noteFailure(): void;
26
28
  private reconnectLoop;
@@ -101,11 +101,19 @@ export class SourceWrapper {
101
101
  return this.remote.isConnected();
102
102
  }
103
103
 
104
+ /** A source whose window has passed is never read from or written to (see the window checks in ArchivesChain), and an intermediate only exists for the minutes of a deploy switchover - on either, being unreachable is expected, not a problem to report. */
105
+ private isConnectionProblemWorthReporting(): boolean {
106
+ if (this.config.intermediate) return false;
107
+ return this.config.validWindow[1] > Date.now();
108
+ }
109
+
104
110
  /** Call after a request failed while isConnected() was false: starts (if not already running) the background reconnect loop. Never blocks - the failed request still throws. */
105
111
  public noteFailure(): void {
106
112
  if (!this.background || this.disposed || this.reconnectRunning) return;
107
113
  if (this.isConnected()) return;
108
- console.error(`Cannot connect to storage ${this.getDebugName()}`);
114
+ if (this.isConnectionProblemWorthReporting()) {
115
+ console.error(`Cannot connect to storage ${this.getDebugName()}`);
116
+ }
109
117
  this.reconnectRunning = true;
110
118
  void this.reconnectLoop();
111
119
  }
@@ -126,7 +134,9 @@ export class SourceWrapper {
126
134
  } catch (e) {
127
135
  // Even a failing call (e.g. access denied) proves the connection is back
128
136
  if (this.isConnected()) break;
129
- console.warn(`Cannot connect to storage ${this.getDebugName()}, retrying in ${Math.round(retryDelay / 1000)}s. ${(e as Error).stack ?? e}`);
137
+ if (this.isConnectionProblemWorthReporting()) {
138
+ console.warn(`Cannot connect to storage ${this.getDebugName()}, retrying in ${Math.round(retryDelay / 1000)}s. ${(e as Error).stack ?? e}`);
139
+ }
130
140
  }
131
141
  retryDelay = Math.min(RETRY_MAX_DELAY, retryDelay * RETRY_GROWTH);
132
142
  }
@@ -182,6 +192,8 @@ export class SourceWrapper {
182
192
  public startPinging(): void {
183
193
  const remote = this.remote;
184
194
  if (!remote || this.pingTimer || this.disposed) return;
195
+ // Latency only decides which shard a variable-shard write materializes into, and a source whose window has passed never receives writes - so measuring it is pointless traffic
196
+ if (this.config.validWindow[1] <= Date.now()) return;
185
197
  let measure = async () => {
186
198
  let start = Date.now();
187
199
  try {
@@ -52,6 +52,10 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
52
52
  getAccessState: (account: string) => Promise<AccessState>;
53
53
  listRequestsForIP: (account: string, ip: string) => Promise<AccessRequest[]>;
54
54
  grantAccess: (requestId: string) => Promise<TrustRecord>;
55
+ adminListActiveBuckets: () => Promise<{
56
+ account: string;
57
+ bucketName: string;
58
+ }[]>;
55
59
  adminListRequests: (ip: string) => Promise<AccessRequest[]>;
56
60
  adminGrantAccess: (requestId: string) => Promise<TrustRecord>;
57
61
  get: (account: string, bucketName: string, path: string, range?: {
@@ -12,7 +12,7 @@ import {
12
12
  getStorageServerConfig, getTrust, getRequests, getLoadedBucket, writeBucketFile,
13
13
  deleteBucketFile, assertWritesAllowed, assertMutable, LoadedBucket,
14
14
  getBucketConfig, listAccountBuckets, ServerBucketInfo, clearAccountWriteStats,
15
- getActiveBucket, activateBucket, ActiveBucketInfo,
15
+ getActiveBucket, activateBucket, ActiveBucketInfo, getActiveBucketKeys,
16
16
  } from "./storageServerState";
17
17
  import { StorageClientController } from "./storageClientController";
18
18
 
@@ -257,6 +257,10 @@ class RemoteStorageControllerBase {
257
257
  throw new Error(`No access request found with id ${JSON.stringify(requestId)}. It may have already been granted or expired.`);
258
258
  }
259
259
 
260
+ /** Admin (so only this machine's own processes can call it): the buckets this process has loaded. A deploy successor asks its predecessor for this, so it activates exactly the buckets that are in use instead of every bucket on disk. */
261
+ async adminListActiveBuckets(): Promise<{ account: string; bucketName: string }[]> {
262
+ return getActiveBucketKeys();
263
+ }
260
264
  async adminListRequests(ip: string): Promise<AccessRequest[]> {
261
265
  let requests = await getRequests();
262
266
  return await requests.get(ip) || [];
@@ -511,6 +515,7 @@ export const RemoteStorageController = SocketFunction.register(
511
515
  listRequestsForIP: { hooks: [accountAccess] },
512
516
  grantAccess: {},
513
517
  adminListRequests: { hooks: [adminAccess] },
518
+ adminListActiveBuckets: { hooks: [adminAccess] },
514
519
  adminGrantAccess: { hooks: [adminAccess] },
515
520
  get: { hooks: [accountAccess] },
516
521
  get2: { hooks: [accountAccess] },
@@ -52,6 +52,11 @@ export declare function writeBucketFile(account: string, bucketName: string, fil
52
52
  lastModified?: number;
53
53
  }): Promise<void>;
54
54
  export declare function getBucketConfig(bucket: LoadedBucket): ArchivesConfig;
55
+ /** Which buckets this process currently has loaded - what a deploy successor asks its predecessor for, so it activates exactly the buckets that are actually in use. */
56
+ export declare function getActiveBucketKeys(): {
57
+ account: string;
58
+ bucketName: string;
59
+ }[];
55
60
  export declare function rebuildAllLoadedBuckets(): Promise<void>;
56
61
  /** Started by deployTakeover once we are actually a deploy successor listening on an alternate port. Until then there are no switchover windows to write or expire, so nothing polls. */
57
62
  export declare const startIntermediateMaintenance: {
@@ -15,7 +15,7 @@ import {
15
15
  import { ROUTING_FILE, parseRoutingData, serializeRemoteConfig, parseHostedUrl, replaceHostedUrlPort, buildFileUrl, getConfigVersion, getRoute, routeContains, routeIntersection } from "./remoteConfig";
16
16
  import { injectIntermediateSource, expireIntermediateSources, getIntermediateSources, findSplitUrl, nextIntermediateVersion, INTERMEDIATE_EXPIRE_GRACE } from "./intermediateSources";
17
17
  import { getTakeoverIntermediate } from "./deployTakeover";
18
- import { createApiArchives } from "./createArchives";
18
+ import { createApiArchives, listServerActiveBucketKeys } from "./createArchives";
19
19
  import type { IStorage } from "../IStorage";
20
20
  import { broadcastRoutingChanged } from "./storageController";
21
21
  import type { AccessRequest, TrustRecord } from "./storageController";
@@ -707,6 +707,14 @@ export function getBucketConfig(bucket: LoadedBucket): ArchivesConfig {
707
707
  };
708
708
  }
709
709
 
710
+ /** Which buckets this process currently has loaded - what a deploy successor asks its predecessor for, so it activates exactly the buckets that are actually in use. */
711
+ export function getActiveBucketKeys(): { account: string; bucketName: string }[] {
712
+ return [...buckets.keys()].map(key => {
713
+ let slash = key.indexOf("/");
714
+ return { account: key.slice(0, slash), bucketName: key.slice(slash + 1) };
715
+ });
716
+ }
717
+
710
718
  export async function rebuildAllLoadedBuckets(): Promise<void> {
711
719
  for (let key of [...buckets.keys()]) {
712
720
  let slash = key.indexOf("/");
@@ -729,7 +737,7 @@ async function writeOwnRoutingConfig(loaded: LoadedBucket, updated: RemoteConfig
729
737
  lastOwnConfigWrite.set(key, Date.now());
730
738
  let version = nextIntermediateVersion(getConfigVersion(loaded.routing));
731
739
  let next: RemoteConfig = { ...updated, version };
732
- console.log(`Writing our own routing config update for bucket ${key} (${reason}), version ${getConfigVersion(loaded.routing)} -> ${version}: ${JSON.stringify(next)}`);
740
+ console.log(`Writing ${ROUTING_FILE} for bucket ${key} (${reason}), version ${getConfigVersion(loaded.routing)} -> ${version}: ${JSON.stringify(next)}`);
733
741
  let data = Buffer.from(serializeRemoteConfig(next));
734
742
  await writeBucketFile(loaded.account, loaded.bucketName, ROUTING_FILE, data);
735
743
  await propagateRoutingConfig(loaded, next, data);
@@ -749,7 +757,7 @@ async function propagateRoutingConfig(loaded: LoadedBucket, next: RemoteConfig,
749
757
  for (let [url, source] of targets) {
750
758
  try {
751
759
  await createApiArchives(source).set(ROUTING_FILE, data);
752
- console.log(`Propagated our routing config update to ${url}`);
760
+ console.log(`Wrote ${ROUTING_FILE} for bucket ${loaded.account}/${loaded.bucketName} to ${url}`);
753
761
  } catch (e) {
754
762
  console.error(`Propagating our routing config update to ${url} failed: ${(e as Error).stack ?? e}`);
755
763
  }
@@ -759,8 +767,12 @@ async function propagateRoutingConfig(loaded: LoadedBucket, next: RemoteConfig,
759
767
  async function maintainIntermediates(): Promise<void> {
760
768
  let { domain, port } = getStorageServerConfig();
761
769
  let takeover = getTakeoverIntermediate();
770
+ let written: string[] = [];
771
+ let alreadyCorrect: string[] = [];
772
+ let notOurs: string[] = [];
762
773
  // ONLY buckets already in memory. A switchover must never load a bucket: loading starts its synchronization, and buckets nothing has touched (legacy ones especially) have no writes to hand off in the first place. One that does get used mid-switchover loads on that access, and the next pass gives it its window.
763
- for (let key of [...buckets.keys()]) {
774
+ let keys = [...buckets.keys()];
775
+ for (let key of keys) {
764
776
  let loadedPromise = buckets.get(key);
765
777
  if (!loadedPromise) continue;
766
778
  let loaded = await loadedPromise.catch(() => undefined);
@@ -768,6 +780,7 @@ async function maintainIntermediates(): Promise<void> {
768
780
  let expired = expireIntermediateSources(loaded.routing, Date.now());
769
781
  if (JSON.stringify(expired) !== JSON.stringify(loaded.routing)) {
770
782
  await writeOwnRoutingConfig(loaded, expired, `switchover windows expired more than ${INTERMEDIATE_EXPIRE_GRACE / 1000}s ago`);
783
+ written.push(key);
771
784
  continue;
772
785
  }
773
786
  if (!takeover) continue;
@@ -776,21 +789,64 @@ async function maintainIntermediates(): Promise<void> {
776
789
  let parsed = parseHostedUrl(x.url);
777
790
  return parsed.address === domain && parsed.port === port;
778
791
  }) as HostedConfig | undefined;
779
- if (!mainUrl) continue;
792
+ if (!mainUrl) {
793
+ notOurs.push(key);
794
+ continue;
795
+ }
780
796
  let injected = injectIntermediateSource(loaded.routing, {
781
797
  splitUrl: mainUrl.url,
782
798
  intermediateUrl: replaceHostedUrlPort(mainUrl.url, takeover.altPort),
783
799
  start: takeover.start,
784
800
  end: takeover.end,
785
801
  });
786
- if (JSON.stringify(injected) === JSON.stringify(loaded.routing)) continue;
802
+ if (JSON.stringify(injected) === JSON.stringify(loaded.routing)) {
803
+ alreadyCorrect.push(key);
804
+ continue;
805
+ }
787
806
  await writeOwnRoutingConfig(loaded, injected, `we are a deploy successor: writes route to our alternate port ${takeover.altPort} from ${new Date(takeover.start).toISOString()} until our predecessor is killed at ${new Date(takeover.end).toISOString()}`);
807
+ written.push(key);
808
+ }
809
+ if (!takeover) return;
810
+ if (!keys.length) {
811
+ console.log(`No active buckets, so no config files to write the intermediate into (a bucket is only active once something uses it)`);
812
+ return;
813
+ }
814
+ if (!written.length) {
815
+ console.log(`Wrote no config files: of ${keys.length} active buckets, ${alreadyCorrect.length} already contain the intermediate and ${notOurs.length} are not hosted by us`);
816
+ }
817
+ }
818
+
819
+ /** Our predecessor still holds the main port and has been serving all along, so it - not us - knows which buckets are in use. We activate exactly those, which is what makes their config files get the intermediate; nothing else on disk is touched. */
820
+ async function activatePredecessorBuckets(): Promise<void> {
821
+ let { domain, port } = getStorageServerConfig();
822
+ let url = `https://${domain}:${port}`;
823
+ let keys: { account: string; bucketName: string }[];
824
+ try {
825
+ keys = await listServerActiveBucketKeys({ url });
826
+ } catch (e) {
827
+ console.error(`Could not ask our predecessor on ${url} which buckets are active, so only buckets used on this process get the intermediate: ${(e as Error).stack ?? e}`);
828
+ return;
829
+ }
830
+ if (!keys.length) {
831
+ console.log(`Our predecessor on ${url} has no active buckets, so there are no config files to write the intermediate into`);
832
+ return;
833
+ }
834
+ console.log(`Our predecessor on ${url} has ${keys.length} active buckets, activating them here so their config files get the intermediate: ${keys.map(x => `${x.account}/${x.bucketName}`).join(", ")}`);
835
+ for (let { account, bucketName } of keys) {
836
+ try {
837
+ await getLoadedBucket(account, bucketName);
838
+ } catch (e) {
839
+ console.error(`Activating bucket ${account}/${bucketName} (active on our predecessor) failed: ${(e as Error).stack ?? e}`);
840
+ }
788
841
  }
789
842
  }
790
843
 
791
844
  /** Started by deployTakeover once we are actually a deploy successor listening on an alternate port. Until then there are no switchover windows to write or expire, so nothing polls. */
792
845
  export const startIntermediateMaintenance = lazy(() => {
793
- void maintainIntermediates().catch((e: Error) => console.error(`Maintaining switchover routing windows failed: ${e.stack ?? e}`));
846
+ void (async () => {
847
+ await activatePredecessorBuckets();
848
+ await maintainIntermediates();
849
+ })().catch((e: Error) => console.error(`Writing the intermediate into the config files failed: ${e.stack ?? e}`));
794
850
  runInfinitePoll(INTERMEDIATE_MAINTAIN_INTERVAL, maintainIntermediates);
795
851
  });
796
852