From 99ff75bad91e7c66c277cf7de8709aa5af337642 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 2 Sep 2025 16:27:17 -0700 Subject: [PATCH] Revert "devenv: remove for now, it broke @atproto versioning" This reverts commit d3bce1d0304bd318f1d22412a910f8b428c9b9fb. --- js/dev-env/README.md | 5 + js/dev-env/lib/constants.ts | 2 + js/dev-env/lib/index.ts | 5 + js/dev-env/lib/network.ts | 32 +++ js/dev-env/lib/pds.ts | 122 +++++++++++ js/dev-env/lib/plc.ts | 35 +++ js/dev-env/lib/utils.ts | 49 +++++ js/dev-env/package.json | 23 ++ js/dev-env/run.mjs | 8 + js/dev-env/run.ts | 6 + js/dev-env/tsconfig.json | 22 ++ pkg/atproto/chat_message_test.go | 357 ++++++++++++++++--------------- 12 files changed, 498 insertions(+), 168 deletions(-) create mode 100644 js/dev-env/README.md create mode 100644 js/dev-env/lib/constants.ts create mode 100644 js/dev-env/lib/index.ts create mode 100644 js/dev-env/lib/network.ts create mode 100644 js/dev-env/lib/pds.ts create mode 100644 js/dev-env/lib/plc.ts create mode 100644 js/dev-env/lib/utils.ts create mode 100644 js/dev-env/package.json create mode 100644 js/dev-env/run.mjs create mode 100644 js/dev-env/run.ts create mode 100644 js/dev-env/tsconfig.json diff --git a/js/dev-env/README.md b/js/dev-env/README.md new file mode 100644 index 000000000..8ac0a25d0 --- /dev/null +++ b/js/dev-env/README.md @@ -0,0 +1,5 @@ +# dev-env + +Borrowed directly from +[atcute](https://github.com/mary-ext/atcute/tree/trunk/packages/internal/dev-env), +MIT licensed. Used primarily as a CI environment. diff --git a/js/dev-env/lib/constants.ts b/js/dev-env/lib/constants.ts new file mode 100644 index 000000000..5ebd523ce --- /dev/null +++ b/js/dev-env/lib/constants.ts @@ -0,0 +1,2 @@ +export const ADMIN_PASSWORD = "admin-pass"; +export const JWT_SECRET = "jwt-secret"; diff --git a/js/dev-env/lib/index.ts b/js/dev-env/lib/index.ts new file mode 100644 index 000000000..4a2391f8a --- /dev/null +++ b/js/dev-env/lib/index.ts @@ -0,0 +1,5 @@ +export * from "./constants.js"; +export * from "./network.js"; +export * from "./pds.js"; +export * from "./plc.js"; +export * from "./utils.js"; diff --git a/js/dev-env/lib/network.ts b/js/dev-env/lib/network.ts new file mode 100644 index 000000000..74c3e154b --- /dev/null +++ b/js/dev-env/lib/network.ts @@ -0,0 +1,32 @@ +import { TestPdsServer, type PdsServerOptions } from "./pds.js"; +import { TestPlcServer, type PlcServerOptions } from "./plc.js"; +import { mockNetworkUtilities } from "./utils.js"; + +export type NetworkConfig = { + pds: Partial; + plc: Partial; +}; + +export class TestNetwork { + constructor( + public readonly plc: TestPlcServer, + public readonly pds: TestPdsServer, + ) {} + + static async create(cfg: Partial): Promise { + const plc = await TestPlcServer.create(cfg.plc ?? {}); + const pds = await TestPdsServer.create({ didPlcUrl: plc.url, ...cfg.pds }); + + mockNetworkUtilities(pds); + + return new TestNetwork(plc, pds); + } + + async processAll() { + await this.pds.processAll(); + } + + async close() { + await Promise.all([this.plc.close(), this.pds.close()]); + } +} diff --git a/js/dev-env/lib/pds.ts b/js/dev-env/lib/pds.ts new file mode 100644 index 000000000..479406fae --- /dev/null +++ b/js/dev-env/lib/pds.ts @@ -0,0 +1,122 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { Secp256k1Keypair, randomStr } from "@atproto/crypto"; +import * as pds from "@atproto/pds"; + +import getPort from "get-port"; +import * as ui8 from "uint8arrays"; + +import { ADMIN_PASSWORD, JWT_SECRET } from "./constants.js"; + +export interface PdsServerOptions extends Partial { + didPlcUrl: string; +} + +export interface AdditionalPdsContext { + dataDirectory: string; + blobstoreLoc: string; +} + +export class TestPdsServer { + constructor( + public readonly server: pds.PDS, + public readonly url: string, + public readonly port: number, + public readonly additional: AdditionalPdsContext, + ) {} + + static async create(config: PdsServerOptions): Promise { + const plcRotationKey = await Secp256k1Keypair.create({ exportable: true }); + const plcRotationPriv = ui8.toString(await plcRotationKey.export(), "hex"); + const recoveryKey = (await Secp256k1Keypair.create()).did(); + + const port = config.port || (await getPort()); + const url = `http://localhost:${port}`; + + const blobstoreLoc = path.join(os.tmpdir(), randomStr(8, "base32")); + const dataDirectory = path.join(os.tmpdir(), randomStr(8, "base32")); + + await fs.mkdir(dataDirectory, { recursive: true }); + + const env: pds.ServerEnvironment = { + devMode: true, + port, + dataDirectory: dataDirectory, + blobstoreDiskLocation: blobstoreLoc, + recoveryDidKey: recoveryKey, + adminPassword: ADMIN_PASSWORD, + jwtSecret: JWT_SECRET, + serviceHandleDomains: [".test"], + bskyAppViewUrl: "https://appview.invalid", + bskyAppViewDid: "did:example:invalid", + bskyAppViewCdnUrlPattern: "http://cdn.appview.com/%s/%s/%s", + modServiceUrl: "https://moderator.invalid", + modServiceDid: "did:example:invalid", + plcRotationKeyK256PrivateKeyHex: plcRotationPriv, + inviteRequired: false, + disableSsrfProtection: true, + serviceName: "Development PDS", + // brandColor: "#ffcb1e", + errorColor: undefined, + logoUrl: + "https://uxwing.com/wp-content/themes/uxwing/download/animals-and-birds/bee-icon.png", + homeUrl: "https://bsky.social/", + termsOfServiceUrl: "https://bsky.social/about/support/tos", + privacyPolicyUrl: "https://bsky.social/about/support/privacy-policy", + supportUrl: "https://blueskyweb.zendesk.com/hc/en-us", + ...config, + }; + + const cfg = pds.envToCfg(env); + const secrets = pds.envToSecrets(env); + + const server = await pds.PDS.create(cfg, secrets); + + await server.start(); + + return new TestPdsServer(server, url, port, { + dataDirectory: dataDirectory, + blobstoreLoc: blobstoreLoc, + }); + } + + get ctx(): pds.AppContext { + return this.server.ctx; + } + + adminAuth(): string { + return ( + "Basic " + + ui8.toString( + ui8.fromString(`admin:${ADMIN_PASSWORD}`, "utf8"), + "base64pad", + ) + ); + } + + adminAuthHeaders() { + return { + authorization: this.adminAuth(), + }; + } + + jwtSecretKey() { + return pds.createSecretKeyObject(JWT_SECRET); + } + + async processAll() { + await this.ctx.backgroundQueue.processAll(); + } + + async close() { + await this.server.destroy(); + + await fs.rm(this.additional.dataDirectory, { + recursive: true, + force: true, + }); + await fs.rm(this.additional.blobstoreLoc, { force: true }); + } +} diff --git a/js/dev-env/lib/plc.ts b/js/dev-env/lib/plc.ts new file mode 100644 index 000000000..ba89dc5ab --- /dev/null +++ b/js/dev-env/lib/plc.ts @@ -0,0 +1,35 @@ +import { AppContext, Database, PlcServer } from "@did-plc/server"; + +import getPort from "get-port"; + +export interface PlcServerOptions { + port?: number; +} + +export class TestPlcServer { + constructor( + public readonly server: PlcServer, + public readonly url: string, + public readonly port: number, + ) {} + + static async create(cfg: PlcServerOptions = {}): Promise { + const port = cfg.port ?? (await getPort()); + const url = `http://localhost:${port}`; + + const db = Database.mock(); + const server = PlcServer.create({ db, port }); + + await server.start(); + + return new TestPlcServer(server, url, port); + } + + get context(): AppContext { + return this.server.ctx; + } + + async close() { + await this.server.destroy(); + } +} diff --git a/js/dev-env/lib/utils.ts b/js/dev-env/lib/utils.ts new file mode 100644 index 000000000..2c93df36d --- /dev/null +++ b/js/dev-env/lib/utils.ts @@ -0,0 +1,49 @@ +import type { IdResolver } from "@atproto/identity"; + +import axios from "axios"; + +import type { TestPdsServer } from "./pds.js"; + +export const mockNetworkUtilities = (pds: TestPdsServer) => { + mockResolvers(pds.ctx.idResolver, pds); +}; + +export const mockResolvers = (idResolver: IdResolver, pds: TestPdsServer) => { + // Map pds public url to its local url when resolving from plc + const origResolveDid = idResolver.did.resolveNoCache; + idResolver.did.resolveNoCache = async (did: string) => { + const result = await (origResolveDid.call( + idResolver.did, + did, + ) as ReturnType); + const service = result?.service?.find((svc) => svc.id === "#atproto_pds"); + + if (typeof service?.serviceEndpoint === "string") { + service.serviceEndpoint = service.serviceEndpoint.replace( + pds.ctx.cfg.service.publicUrl, + `http://localhost:${pds.port}`, + ); + } + + return result; + }; + + const origResolveHandleDns = idResolver.handle.resolveDns; + idResolver.handle.resolve = async (handle: string) => { + const isPdsHandle = pds.ctx.cfg.identity.serviceHandleDomains.some( + (domain) => handle.endsWith(domain), + ); + + if (!isPdsHandle) { + return origResolveHandleDns.call(idResolver.handle, handle); + } + + const url = `${pds.url}/.well-known/atproto-did`; + try { + const res = await axios.get(url, { headers: { host: handle } }); + return res.data; + } catch (err) { + return undefined; + } + }; +}; diff --git a/js/dev-env/package.json b/js/dev-env/package.json new file mode 100644 index 000000000..ea7deb7ff --- /dev/null +++ b/js/dev-env/package.json @@ -0,0 +1,23 @@ +{ + "private": true, + "type": "module", + "name": "@atcute/internal-dev-env", + "version": "0.7.21", + "dependencies": { + "@atproto/crypto": "^0.4.3", + "@atproto/identity": "^0.4.5", + "@atproto/pds": "^0.4.90", + "@did-plc/server": "^0.0.1", + "axios": "^1.7.9", + "better-sqlite3": "10.1.0", + "get-port": "^7.1.0", + "typescript": "^5.8.2", + "uint8arrays": "^5.1.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "scripts": { + "prepare": "tsc" + } +} diff --git a/js/dev-env/run.mjs b/js/dev-env/run.mjs new file mode 100644 index 000000000..d443d6b5b --- /dev/null +++ b/js/dev-env/run.mjs @@ -0,0 +1,8 @@ +import { TestNetwork } from "./dist/index.js"; + +(async () => { + const network = await TestNetwork.create({}); + console.log( + JSON.stringify({ "pds-url": network.pds.url, "plc-url": network.plc.url }), + ); +})(); diff --git a/js/dev-env/run.ts b/js/dev-env/run.ts new file mode 100644 index 000000000..c140b9b1a --- /dev/null +++ b/js/dev-env/run.ts @@ -0,0 +1,6 @@ +import { TestNetwork } from "./lib"; + +(async () => { + const network = await TestNetwork.create({}); + console.log("hi"); +})(); diff --git a/js/dev-env/tsconfig.json b/js/dev-env/tsconfig.json new file mode 100644 index 000000000..495a111f4 --- /dev/null +++ b/js/dev-env/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "outDir": "dist/", + "esModuleInterop": true, + "skipLibCheck": true, + "target": "ESNext", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "verbatimModuleSyntax": true, + "strict": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "module": "NodeNext", + "sourceMap": true, + "declaration": true + }, + "include": ["lib"] +} diff --git a/pkg/atproto/chat_message_test.go b/pkg/atproto/chat_message_test.go index 4323eb2b0..3b3c298c7 100644 --- a/pkg/atproto/chat_message_test.go +++ b/pkg/atproto/chat_message_test.go @@ -1,170 +1,191 @@ package atproto -// func TestChatMessage(t *testing.T) { -// dev := devenv.WithDevEnv(t) -// t.Logf("dev: %+v", dev) -// cli := config.CLI{ -// PublicHost: "example.com", -// DBURL: ":memory:", -// RelayHost: strings.ReplaceAll(dev.PDSURL, "http://", "ws://"), -// PLCURL: dev.PLCURL, -// } -// t.Logf("cli: %+v", cli) -// b := bus.NewBus() -// cli.DataDir = t.TempDir() -// mod, err := model.MakeDB(":memory:") -// require.NoError(t, err) -// state, err := statedb.MakeDB(&cli, nil, mod) -// require.NoError(t, err) -// atsync := &ATProtoSynchronizer{ -// CLI: &cli, -// StatefulDB: state, -// Model: mod, -// Bus: b, -// } - -// ctx, cancel := context.WithCancel(context.Background()) - -// done := make(chan struct{}) - -// go func() { -// err := atsync.StartFirehose(ctx) -// require.NoError(t, err) -// close(done) -// }() - -// user := dev.CreateAccount(t) -// user2 := dev.CreateAccount(t) - -// ch := b.Subscribe(user.DID) -// defer b.Unsubscribe(user.DID, ch) - -// busMessages := []bus.Message{} -// go func() { -// for msg := range ch { -// t.Logf("message: %+v", msg) -// busMessages = append(busMessages, msg) -// } -// }() - -// msg := &streamplace.ChatMessage{ -// LexiconTypeID: "place.stream.chat.message", -// Text: "Hello, world!", -// CreatedAt: time.Now().Add(-time.Second).Format(util.ISO8601), -// Streamer: user.DID, -// } - -// rec1, err := comatproto.RepoCreateRecord(ctx, user.XRPC, &comatproto.RepoCreateRecord_Input{ -// Collection: "place.stream.chat.message", -// Repo: user.DID, -// Record: &lexutil.LexiconTypeDecoder{Val: msg}, -// }) -// require.NoError(t, err) - -// msg2 := &streamplace.ChatMessage{ -// LexiconTypeID: "place.stream.chat.message", -// Text: "Hello, world 2!", -// CreatedAt: time.Now().Format(util.ISO8601), -// Streamer: user.DID, -// } - -// _, err = comatproto.RepoCreateRecord(ctx, user2.XRPC, &comatproto.RepoCreateRecord_Input{ -// Collection: "place.stream.chat.message", -// Repo: user2.DID, -// Record: &lexutil.LexiconTypeDecoder{Val: msg2}, -// }) -// require.NoError(t, err) - -// messages := []*streamplace.ChatDefs_MessageView{} -// err = untilNoErrors(t, func() error { -// messages, err = mod.MostRecentChatMessages(user.DID) -// if err != nil { -// return err -// } -// if len(messages) != 2 { -// return fmt.Errorf("expected 2 messages, got %d", len(messages)) -// } -// if len(busMessages) != 2 { -// return fmt.Errorf("expected 2 bus messages, got %d", len(busMessages)) -// } -// return nil -// }) -// // Reverse the messages slice to match expected order (most recent first) -// slices.SortFunc(messages, func(a, b *streamplace.ChatDefs_MessageView) int { -// aTime := a.Record.Val.(*streamplace.ChatMessage).CreatedAt -// bTime := b.Record.Val.(*streamplace.ChatMessage).CreatedAt -// if aTime < bTime { -// return -1 -// } else if aTime > bTime { -// return 1 -// } -// return 0 -// }) -// require.Equal(t, msg.Text, messages[0].Record.Val.(*streamplace.ChatMessage).Text) -// require.Equal(t, msg2.Text, messages[1].Record.Val.(*streamplace.ChatMessage).Text) -// busMessage1 := busMessages[0].(*streamplace.ChatDefs_MessageView) -// busMessage2 := busMessages[1].(*streamplace.ChatDefs_MessageView) -// require.Equal(t, msg.Text, busMessage1.Record.Val.(*streamplace.ChatMessage).Text) -// require.Equal(t, msg2.Text, busMessage2.Record.Val.(*streamplace.ChatMessage).Text) - -// rkey := strings.TrimPrefix(rec1.Uri, fmt.Sprintf("at://%s/place.stream.chat.message/", user.DID)) - -// _, err = comatproto.RepoDeleteRecord(ctx, user.XRPC, &comatproto.RepoDeleteRecord_Input{ -// Collection: "place.stream.chat.message", -// Repo: user.DID, -// Rkey: rkey, -// }) - -// require.NoError(t, err) - -// err = untilNoErrors(t, func() error { -// messages, err = mod.MostRecentChatMessages(user.DID) -// if err != nil { -// return err -// } -// if len(messages) != 1 { -// return fmt.Errorf("expected 1 message, got %d", len(messages)) -// } -// if len(busMessages) != 3 { -// return fmt.Errorf("expected 3 bus messages, got %d", len(busMessages)) -// } -// return nil -// }) -// require.NoError(t, err) -// require.Equal(t, msg2.Text, messages[0].Record.Val.(*streamplace.ChatMessage).Text) -// busMessage3 := busMessages[2].(*streamplace.ChatDefs_MessageView) -// require.Equal(t, true, *busMessage3.Deleted) - -// cancel() -// <-done -// } - -// func untilNoErrors(t *testing.T, f func() error) error { -// ticker := backoff.NewTicker(NewExponentialBackOff()) -// defer ticker.Stop() -// var err error -// for i := 0; i < 10; i++ { -// err = f() -// if err == nil { -// return err -// } -// if i < 9 { -// <-ticker.C -// } -// } -// return err -// } - -// // More aggressive backoff for tests -// func NewExponentialBackOff() *backoff.ExponentialBackOff { -// b := &backoff.ExponentialBackOff{ -// InitialInterval: 100 * time.Millisecond, -// RandomizationFactor: backoff.DefaultRandomizationFactor, -// Multiplier: backoff.DefaultMultiplier, -// MaxInterval: 2 * time.Second, -// MaxElapsedTime: 10 * time.Second, -// Clock: backoff.SystemClock, -// } -// b.Reset() -// return b -// } +import ( + "context" + "fmt" + "slices" + "strings" + "testing" + "time" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + lexutil "github.com/bluesky-social/indigo/lex/util" + "github.com/bluesky-social/indigo/util" + "github.com/cenkalti/backoff" + "github.com/stretchr/testify/require" + "stream.place/streamplace/pkg/bus" + "stream.place/streamplace/pkg/config" + "stream.place/streamplace/pkg/devenv" + "stream.place/streamplace/pkg/model" + "stream.place/streamplace/pkg/statedb" + "stream.place/streamplace/pkg/streamplace" +) + +func TestChatMessage(t *testing.T) { + dev := devenv.WithDevEnv(t) + t.Logf("dev: %+v", dev) + cli := config.CLI{ + PublicHost: "example.com", + DBURL: ":memory:", + RelayHost: strings.ReplaceAll(dev.PDSURL, "http://", "ws://"), + PLCURL: dev.PLCURL, + } + t.Logf("cli: %+v", cli) + b := bus.NewBus() + cli.DataDir = t.TempDir() + mod, err := model.MakeDB(":memory:") + require.NoError(t, err) + state, err := statedb.MakeDB(&cli, nil, mod) + require.NoError(t, err) + atsync := &ATProtoSynchronizer{ + CLI: &cli, + StatefulDB: state, + Model: mod, + Bus: b, + } + + ctx, cancel := context.WithCancel(context.Background()) + + done := make(chan struct{}) + + go func() { + err := atsync.StartFirehose(ctx) + require.NoError(t, err) + close(done) + }() + + user := dev.CreateAccount(t) + user2 := dev.CreateAccount(t) + + ch := b.Subscribe(user.DID) + defer b.Unsubscribe(user.DID, ch) + + busMessages := []bus.Message{} + go func() { + for msg := range ch { + t.Logf("message: %+v", msg) + busMessages = append(busMessages, msg) + } + }() + + msg := &streamplace.ChatMessage{ + LexiconTypeID: "place.stream.chat.message", + Text: "Hello, world!", + CreatedAt: time.Now().Add(-time.Second).Format(util.ISO8601), + Streamer: user.DID, + } + + rec1, err := comatproto.RepoCreateRecord(ctx, user.XRPC, &comatproto.RepoCreateRecord_Input{ + Collection: "place.stream.chat.message", + Repo: user.DID, + Record: &lexutil.LexiconTypeDecoder{Val: msg}, + }) + require.NoError(t, err) + + msg2 := &streamplace.ChatMessage{ + LexiconTypeID: "place.stream.chat.message", + Text: "Hello, world 2!", + CreatedAt: time.Now().Format(util.ISO8601), + Streamer: user.DID, + } + + _, err = comatproto.RepoCreateRecord(ctx, user2.XRPC, &comatproto.RepoCreateRecord_Input{ + Collection: "place.stream.chat.message", + Repo: user2.DID, + Record: &lexutil.LexiconTypeDecoder{Val: msg2}, + }) + require.NoError(t, err) + + messages := []*streamplace.ChatDefs_MessageView{} + err = untilNoErrors(t, func() error { + messages, err = mod.MostRecentChatMessages(user.DID) + if err != nil { + return err + } + if len(messages) != 2 { + return fmt.Errorf("expected 2 messages, got %d", len(messages)) + } + if len(busMessages) != 2 { + return fmt.Errorf("expected 2 bus messages, got %d", len(busMessages)) + } + return nil + }) + // Reverse the messages slice to match expected order (most recent first) + slices.SortFunc(messages, func(a, b *streamplace.ChatDefs_MessageView) int { + aTime := a.Record.Val.(*streamplace.ChatMessage).CreatedAt + bTime := b.Record.Val.(*streamplace.ChatMessage).CreatedAt + if aTime < bTime { + return -1 + } else if aTime > bTime { + return 1 + } + return 0 + }) + require.Equal(t, msg.Text, messages[0].Record.Val.(*streamplace.ChatMessage).Text) + require.Equal(t, msg2.Text, messages[1].Record.Val.(*streamplace.ChatMessage).Text) + busMessage1 := busMessages[0].(*streamplace.ChatDefs_MessageView) + busMessage2 := busMessages[1].(*streamplace.ChatDefs_MessageView) + require.Equal(t, msg.Text, busMessage1.Record.Val.(*streamplace.ChatMessage).Text) + require.Equal(t, msg2.Text, busMessage2.Record.Val.(*streamplace.ChatMessage).Text) + + rkey := strings.TrimPrefix(rec1.Uri, fmt.Sprintf("at://%s/place.stream.chat.message/", user.DID)) + + _, err = comatproto.RepoDeleteRecord(ctx, user.XRPC, &comatproto.RepoDeleteRecord_Input{ + Collection: "place.stream.chat.message", + Repo: user.DID, + Rkey: rkey, + }) + + require.NoError(t, err) + + err = untilNoErrors(t, func() error { + messages, err = mod.MostRecentChatMessages(user.DID) + if err != nil { + return err + } + if len(messages) != 1 { + return fmt.Errorf("expected 1 message, got %d", len(messages)) + } + if len(busMessages) != 3 { + return fmt.Errorf("expected 3 bus messages, got %d", len(busMessages)) + } + return nil + }) + require.NoError(t, err) + require.Equal(t, msg2.Text, messages[0].Record.Val.(*streamplace.ChatMessage).Text) + busMessage3 := busMessages[2].(*streamplace.ChatDefs_MessageView) + require.Equal(t, true, *busMessage3.Deleted) + + cancel() + <-done +} + +func untilNoErrors(t *testing.T, f func() error) error { + ticker := backoff.NewTicker(NewExponentialBackOff()) + defer ticker.Stop() + var err error + for i := 0; i < 10; i++ { + err = f() + if err == nil { + return err + } + if i < 9 { + <-ticker.C + } + } + return err +} + +// More aggressive backoff for tests +func NewExponentialBackOff() *backoff.ExponentialBackOff { + b := &backoff.ExponentialBackOff{ + InitialInterval: 100 * time.Millisecond, + RandomizationFactor: backoff.DefaultRandomizationFactor, + Multiplier: backoff.DefaultMultiplier, + MaxInterval: 2 * time.Second, + MaxElapsedTime: 10 * time.Second, + Clock: backoff.SystemClock, + } + b.Reset() + return b +} -- 2.51.2