From 8df4c106e84882059e5b612bcd13bf0b3f2d72bc Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Wed, 20 May 2026 08:54:02 -0700 Subject: [PATCH 1/2] External storage and codec server sample --- README.md | 4 + external-storage/README.md | 133 ++++++++++++++++++++++++ external-storage/codec-server/main.go | 141 +++++++++++++++++++++++++ external-storage/data_converter.go | 38 +++++++ external-storage/s3-mock/main.go | 76 ++++++++++++++ external-storage/s3.go | 57 ++++++++++ external-storage/sample_data.go | 94 +++++++++++++++++ external-storage/starter/main.go | 55 ++++++++++ external-storage/worker/main.go | 27 +++++ external-storage/workflows.go | 143 ++++++++++++++++++++++++++ go.mod | 24 +++++ go.sum | 58 +++++++++++ 12 files changed, 850 insertions(+) create mode 100644 external-storage/README.md create mode 100644 external-storage/codec-server/main.go create mode 100644 external-storage/data_converter.go create mode 100644 external-storage/s3-mock/main.go create mode 100644 external-storage/s3.go create mode 100644 external-storage/sample_data.go create mode 100644 external-storage/starter/main.go create mode 100644 external-storage/worker/main.go create mode 100644 external-storage/workflows.go diff --git a/README.md b/README.md index 13569a82..b80d5b29 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,10 @@ with an external configuration file, like TOML, decoupling connection settings f server to decode payloads for display in Temporal CLI and Temporal Web. This setup can be used for any kind of codec, common examples are compression or encryption. +- [**External Storage**](./external-storage): Offload large payloads to + S3-compatible object storage plus a codec server built on + the SDK's payload HTTP handler so the Web UI and CLI can decode and download the externally-stored payloads. + - [**Query Example**](./query): Demonstrates how to Query the state of a single Workflow Execution using the `QueryWorkflow` and `SetQueryHandler` APIs. Additional documentation: [How to Query a Workflow Execution in Go](https://docs.temporal.io/application-development/features/#queries). diff --git a/external-storage/README.md b/external-storage/README.md new file mode 100644 index 00000000..8e14f510 --- /dev/null +++ b/external-storage/README.md @@ -0,0 +1,133 @@ +# External Storage Sample + +This sample demonstrates how to offload large workflow payloads to S3-compatible +object storage using the Temporal Go SDK's built-in `ExternalStorage` system, +combined with the SDK's zlib `PayloadCodec` so the payloads stored both inline +in Temporal and in S3 are compressed. + +**Scenario:** A fulfillment center processes batches of shipping orders. The +workflow receives a small request (a batch ID and order count), then internally +calls a `FetchOrders` activity that returns the full list of orders with +customer records, line items, and handling notes. That list — several hundred +kilobytes even after compression — is passed to a second `ProcessOrders` +activity. Finally the workflow returns a small `BatchSummary` with totals. + +Each payload is first compressed by the SDK's `NewZlibCodec`. The SDK then +checks the compressed size against the default 256 KiB threshold; payloads +still above it are stored in S3 and replaced inline with compact claim-check +references. The workflow's own input (`OrderBatchRequest`) and result +(`BatchSummary`) compress to a few hundred bytes and remain inline. + +A mock S3 service ([s3-mock](./s3-mock)) is included so you can run the sample +locally without an AWS account or Docker. A codec server +([codec-server](./codec-server)) is included to retrieve and decompress payloads +on demand for the Temporal Web UI. + +## Steps to run this sample + +1. Run a [Temporal service](https://github.com/temporalio/samples-go/tree/main/#how-to-use). +2. In a separte terminal, run the mock S3 server. It listens on `:5000` and + creates the `temporal-payloads` bucket. Leave it running. + ``` + go run ./external-storage/s3-mock + ``` +3. In a separate terminal, run the worker: + ``` + go run ./external-storage/worker + ``` +4. In a separate terminal, run the starter: + ``` + go run ./external-storage/starter + ``` + Example output: + ``` + Starting workflow external-storage-20260515-120000 (batch_id=BATCH-20260515-120000, order_count=200) + + Batch BATCH-20260515-120000: 200 orders processed + Total shipping cost: $28512.40 + Total weight: 19684.2 kg + Avg delivery: 4.4 days + ``` +5. (Optional) Run the codec server in a fourth terminal: + ``` + go run ./external-storage/codec-server + ``` + In the Temporal Web UI (http://localhost:8233), open Settings → Data Encoder + and set the Remote Codec Endpoint to `http://localhost:8081`. Reload the + workflow page; the inline compressed payloads will be shown as readable + JSON, and externally-stored payloads can be downloaded to fetch their + actual content from the mock S3. + + The Web UI sends the namespace as the `X-Namespace` header on each request, + so multi-namespace setups can dispatch by reading that header. + + | Endpoint | Behavior | + | --- | --- | + | `POST /encode` | Compress the payload, then offload to S3 if it exceeds the threshold. | + | `POST /decode` | Retrieve any external storage references from S3, then decompress. Pass `?preserveStorageRefs=true` to leave references as-is. | + | `POST /download` | All inputs must be storage references. Retrieves them from S3 and decompresses. | +6. Run `temporal workflow show` to see how payloads are stored: + ``` + temporal workflow show --workflow-id external-storage- + ``` + The workflow's input (`OrderBatchRequest`) and result (`BatchSummary`) are + zlib-encoded and stored inline in Temporal — small enough to compress to a + few hundred bytes. The two activity payloads carrying the order list — the + output of `FetchOrders` and the input to `ProcessOrders` — exceed 256 KiB + even after compression, so they appear as external-storage references, + confirming the SDK offloaded them to S3. + +## How it works + +The client's `DataConverter` is wrapped with the SDK's zlib codec, and +`client.Options.ExternalStorage` is set with the SDK's S3 driver +(`go.temporal.io/sdk/contrib/aws/s3driver`): + +```go +driver, _ := s3driver.NewDriver(s3driver.Options{ + Client: awssdkv2.NewClient(s3Client), + Bucket: s3driver.StaticBucket(externalstorage.S3Bucket), +}) +client.Dial(client.Options{ + DataConverter: converter.NewCodecDataConverter( + converter.GetDefaultDataConverter(), + converter.NewZlibCodec(converter.ZlibCodecOptions{AlwaysEncode: true}), + ), + ExternalStorage: converter.ExternalStorage{ + Drivers: []converter.StorageDriver{driver}, + }, +}) +``` + +On the encode path the SDK: + +1. Serializes the Go value to a `Payload`. +2. Runs the zlib codec to compress the payload bytes. +3. Checks the compressed size against `PayloadSizeThreshold` (default: 256 KiB). +4. If still above the threshold, stores the compressed bytes in S3 via + the SDK's `s3driver` and replaces the inline payload with a claim-check + reference. + +On the decode path the SDK reverses these steps, transparently retrieving from +S3 and decompressing as needed. + +The worker, the starter, and the codec server must use the **same** codec and +external storage configuration so each side can read what the other wrote. In +this sample the shared wiring lives in +[data_converter.go](./data_converter.go) for the worker and starter, and is +mirrored in [codec-server/main.go](./codec-server/main.go) for the codec +server. + +## Codec server + +The codec server is built directly on top of the SDK's +`converter.NewPayloadHTTPHandler`, which implements the `/encode`, `/decode`, +and `/download` endpoints with full external storage support. The sample adds +two thin layers around it: + +- A **namespace dispatcher** that picks a per-namespace handler by inspecting + the `X-Namespace` header sent by the Temporal Web UI and CLI. Only `"default"` + is configured here, but the same map can host other namespaces with their own + codec chains and storage backends. +- A **CORS middleware** that allows the Web UI origin to call the codec + server. diff --git a/external-storage/codec-server/main.go b/external-storage/codec-server/main.go new file mode 100644 index 00000000..784cfc8c --- /dev/null +++ b/external-storage/codec-server/main.go @@ -0,0 +1,141 @@ +// codec-server hosts the payload HTTP handler from the Temporal Go SDK so the +// Web UI and CLI can transform external-storage payloads on demand. +// +// Deliberately left out for sample simplicity: authentication (slot a +// middleware between the CORS handler and the dispatcher) and configurable +// listen address. For an example of enabling authentication in a codec +// server, look at ../../codec-server. +package main + +import ( + "context" + "flag" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "strconv" + "time" + + externalstorage "github.com/temporalio/samples-go/external-storage" + "go.temporal.io/sdk/converter" +) + +const webUIOrigin = "http://localhost:8233" + +// newPayloadNamespacesHTTPHandler returns an http.Handler that dispatches each +// request to a per-namespace handler chosen by the X-Namespace header. The +// Temporal Web UI and CLI send that header on every codec server request, so one +// process can host different codec/storage configurations per namespace +// without per-namespace URL prefixes. +func newPayloadNamespacesHTTPHandler(handlers map[string]http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + namespace := r.Header.Get("X-Namespace") + h, ok := handlers[namespace] + if !ok { + http.NotFound(w, r) + return + } + h.ServeHTTP(w, r) + }) +} + +// statusRecorder captures the response status code so the logging middleware +// can include it. WriteHeader is only called once per request by the SDK's +// payload handler; subsequent writes go through ResponseWriter directly. +type statusRecorder struct { + http.ResponseWriter + status int +} + +func (s *statusRecorder) WriteHeader(code int) { + s.status = code + s.ResponseWriter.WriteHeader(code) +} + +// newLoggingHTTPHandler prints a one-line summary of each request: method, +// path, namespace, response status, and how long the handler took. +func newLoggingHTTPHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} + next.ServeHTTP(rec, r) + log.Printf("%s %s namespace=%q status=%d duration=%s", + r.Method, r.URL.Path, r.Header.Get("X-Namespace"), rec.status, time.Since(start)) + }) +} + +// newCORSHTTPHandler lets the Temporal Web UI call the codec server from its own +// origin. The X-Namespace header is allowlisted so the dispatcher can read it. +func newCORSHTTPHandler(origin string, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Origin") == origin { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Access-Control-Allow-Methods", "POST,OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type,X-Namespace") + } + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + next.ServeHTTP(w, r) + }) +} + +func main() { + var port int + flag.IntVar(&port, "port", 8081, "Port to listen on") + flag.Parse() + + ctx := context.Background() + driver, err := externalstorage.NewS3Driver(ctx) + if err != nil { + log.Fatalf("new s3 driver: %v", err) + } + + // Build the payload handler with the same codec + external storage that + // the worker and starter use. PreStorageCodecs runs before storage on + // encode and after retrieval on decode, mirroring what the client-side + // DataConverter does. + defaultNamespaceHandler, err := converter.NewPayloadHTTPHandler(converter.PayloadHTTPHandlerOptions{ + PreStorageCodecs: []converter.PayloadCodec{ + converter.NewZlibCodec(converter.ZlibCodecOptions{AlwaysEncode: true}), + }, + ExternalStorage: converter.ExternalStorage{ + Drivers: []converter.StorageDriver{driver}, + }, + }) + if err != nil { + log.Fatalf("new payload handler: %v", err) + } + + // Per-namespace map: extend this to host additional namespaces with their + // own codec chain and/or storage backend. + handler := newPayloadNamespacesHTTPHandler(map[string]http.Handler{ + "default": defaultNamespaceHandler, + }) + handler = newCORSHTTPHandler(webUIOrigin, handler) + handler = newLoggingHTTPHandler(handler) + + srv := &http.Server{ + Addr: "localhost:" + strconv.Itoa(port), + Handler: handler, + } + + errCh := make(chan error, 1) + go func() { errCh <- srv.ListenAndServe() }() + + fmt.Printf("Codec server running at http://%s, ctrl+c to exit\n", srv.Addr) + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt) + select { + case <-sigCh: + _ = srv.Close() + case err := <-errCh: + if err != nil && err != http.ErrServerClosed { + log.Fatal(err) + } + } +} diff --git a/external-storage/data_converter.go b/external-storage/data_converter.go new file mode 100644 index 00000000..75d6ca72 --- /dev/null +++ b/external-storage/data_converter.go @@ -0,0 +1,38 @@ +package externalstorage + +import ( + "context" + "fmt" + + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/converter" +) + +// NewClient dials Temporal with the data converter and external storage +// configuration shared by every process in this sample (worker, starter, +// codec server). They must all agree on: +// +// - the codec chain that wraps each payload (zlib here), and +// - the external storage driver and threshold that decides when a payload +// is offloaded instead of stored inline. +// +// If any one of them diverges, payloads written by one side will not be +// readable by the other. +func NewClient(ctx context.Context, options client.Options) (client.Client, error) { + driver, err := NewS3Driver(ctx) + if err != nil { + return nil, fmt.Errorf("new s3 driver: %w", err) + } + + if options.DataConverter == nil { + options.DataConverter = converter.NewCodecDataConverter( + converter.GetDefaultDataConverter(), + converter.NewZlibCodec(converter.ZlibCodecOptions{AlwaysEncode: true}), + ) + } + options.ExternalStorage = converter.ExternalStorage{ + Drivers: []converter.StorageDriver{driver}, + } + + return client.Dial(options) +} diff --git a/external-storage/s3-mock/main.go b/external-storage/s3-mock/main.go new file mode 100644 index 00000000..71a882b4 --- /dev/null +++ b/external-storage/s3-mock/main.go @@ -0,0 +1,76 @@ +// s3-mock runs an in-memory S3-compatible HTTP server backed by gofakes3, so +// this sample can run locally without an AWS account or Docker. +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "strconv" + "time" + + externalstorage "github.com/temporalio/samples-go/external-storage" + "github.com/johannesboyne/gofakes3" + "github.com/johannesboyne/gofakes3/backend/s3mem" +) + +// statusRecorder captures the response status code so the logging middleware +// can include it. +type statusRecorder struct { + http.ResponseWriter + status int +} + +func (s *statusRecorder) WriteHeader(code int) { + s.status = code + s.ResponseWriter.WriteHeader(code) +} + +// newLoggingHTTPHandler prints a one-line summary of each request: method, +// path, response status, and how long the handler took. +func newLoggingHTTPHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} + next.ServeHTTP(rec, r) + log.Printf("%s %s status=%d duration=%s", + r.Method, r.URL.Path, rec.status, time.Since(start)) + }) +} + +func main() { + var port int + flag.IntVar(&port, "port", 5000, "Port to listen on") + flag.Parse() + + backend := s3mem.New() + if err := backend.CreateBucket(externalstorage.S3Bucket); err != nil { + log.Fatalf("create bucket: %v", err) + } + faker := gofakes3.New(backend) + + srv := &http.Server{ + Addr: "localhost:" + strconv.Itoa(port), + Handler: newLoggingHTTPHandler(faker.Server()), + } + + errCh := make(chan error, 1) + go func() { errCh <- srv.ListenAndServe() }() + + fmt.Printf("Mock S3 server running at http://%s\n", srv.Addr) + fmt.Printf("Bucket %q created. Press ctrl+c to stop.\n", externalstorage.S3Bucket) + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt) + select { + case <-sigCh: + _ = srv.Close() + case err := <-errCh: + if err != nil && err != http.ErrServerClosed { + log.Fatal(err) + } + } +} diff --git a/external-storage/s3.go b/external-storage/s3.go new file mode 100644 index 00000000..0bf7ea71 --- /dev/null +++ b/external-storage/s3.go @@ -0,0 +1,57 @@ +package externalstorage + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + "go.temporal.io/sdk/contrib/aws/s3driver" + "go.temporal.io/sdk/contrib/aws/s3driver/awssdkv2" + "go.temporal.io/sdk/converter" +) + +// S3 endpoint and credentials used by every process in this sample (worker, +// starter, codec server, s3-mock). They point at the mock S3 server started by +// s3-mock/main.go. +const ( + S3Endpoint = "http://localhost:5000" + S3Bucket = "temporal-payloads" + S3AccessKey = "test" + S3SecretKey = "test" + S3Region = "us-east-1" +) + +// NewS3Client returns an aws-sdk-go-v2 S3 client configured for the local mock +// server. Path-style addressing (http://host/bucket/key) is used in place of +// the SDK's default virtual-hosted style (http://bucket.host/key) so the +// client doesn't have to resolve bucket.localhost — that resolution depends on +// the OS's handling of *.localhost (RFC 6761) and isn't guaranteed everywhere. +func NewS3Client(ctx context.Context) (*s3.Client, error) { + cfg, err := config.LoadDefaultConfig(ctx, + config.WithRegion(S3Region), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(S3AccessKey, S3SecretKey, "")), + ) + if err != nil { + return nil, fmt.Errorf("load aws config: %w", err) + } + return s3.NewFromConfig(cfg, func(o *s3.Options) { + o.BaseEndpoint = aws.String(S3Endpoint) + o.UsePathStyle = true + }), nil +} + +// NewS3Driver builds the SDK's S3 StorageDriver, wired to the mock S3 client +// via the SDK's aws-sdk-go-v2 client adapter. +func NewS3Driver(ctx context.Context) (converter.StorageDriver, error) { + s3Client, err := NewS3Client(ctx) + if err != nil { + return nil, err + } + return s3driver.NewDriver(s3driver.Options{ + Client: awssdkv2.NewClient(s3Client), + Bucket: s3driver.StaticBucket(S3Bucket), + }) +} diff --git a/external-storage/sample_data.go b/external-storage/sample_data.go new file mode 100644 index 00000000..c5f71774 --- /dev/null +++ b/external-storage/sample_data.go @@ -0,0 +1,94 @@ +package externalstorage + +import ( + "fmt" + "hash/fnv" + "math/rand" +) + +// Produce payloads large enough to exceed the default 256 KiB ExternalStorage +// threshold without hand-crafted catalogs. Each order is padded with random +// filler in its item descriptions and shipping notes. Calibrated so 100 orders +// serialize to roughly 300 KiB of JSON. + +const ( + itemsPerOrder = 5 + itemDescriptionChars = 500 + shippingNotesChars = 200 +) + +var cities = []struct { + city, state string +}{ + {"Houston", "TX"}, + {"Dallas", "TX"}, + {"Los Angeles", "CA"}, + {"San Francisco", "CA"}, + {"Denver", "CO"}, + {"Miami", "FL"}, + {"Chicago", "IL"}, + {"New York", "NY"}, + {"Seattle", "WA"}, + {"Atlanta", "GA"}, +} + +const fillerAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ " + +func filler(rng *rand.Rand, n int) string { + b := make([]byte, n) + for i := range b { + b[i] = fillerAlphabet[rng.Intn(len(fillerAlphabet))] + } + return string(b) +} + +func generateOrders(batchID string, count int) []Order { + orders := make([]Order, count) + for i := 0; i < count; i++ { + orders[i] = generateOrder(batchID, i+1) + } + return orders +} + +func generateOrder(batchID string, index int) Order { + h := fnv.New64a() + _, _ = fmt.Fprintf(h, "%s-%d", batchID, index) + rng := rand.New(rand.NewSource(int64(h.Sum64()))) + loc := cities[rng.Intn(len(cities))] + + items := make([]OrderItem, itemsPerOrder) + var totalWeight float64 + for i := range items { + qty := rng.Intn(10) + 1 + weight := round2(0.5 + rng.Float64()*49.5) + items[i] = OrderItem{ + SKU: fmt.Sprintf("SKU-%05d", 10000+rng.Intn(90000)), + Name: fmt.Sprintf("Product %d", 1+rng.Intn(999)), + Description: filler(rng, itemDescriptionChars), + Quantity: qty, + UnitPriceUSD: round2(10.0 + rng.Float64()*990.0), + WeightKg: weight, + } + totalWeight += weight * float64(qty) + } + + return Order{ + ID: fmt.Sprintf("ORD-%06d", index), + Customer: Customer{ + ID: fmt.Sprintf("CUST-%06d", index), + Name: fmt.Sprintf("Customer %d", index), + Email: fmt.Sprintf("customer%d@example.com", index), + Address: Address{ + Street: fmt.Sprintf("%d Main Street", 100+rng.Intn(9900)), + City: loc.city, + State: loc.state, + ZipCode: fmt.Sprintf("%05d", 10000+rng.Intn(90000)), + Country: "US", + }, + }, + Items: items, + TotalWeightKg: round2(totalWeight), + ShippingNotes: filler(rng, shippingNotesChars), + } +} + diff --git a/external-storage/starter/main.go b/external-storage/starter/main.go new file mode 100644 index 00000000..8e60ffc5 --- /dev/null +++ b/external-storage/starter/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "time" + + externalstorage "github.com/temporalio/samples-go/external-storage" + "go.temporal.io/sdk/client" +) + +func main() { + var orderCount int + flag.IntVar(&orderCount, "orders", 200, "Number of orders in the batch") + flag.Parse() + + c, err := externalstorage.NewClient(context.Background(), client.Options{}) + if err != nil { + log.Fatalln("Unable to create client", err) + } + defer c.Close() + + runID := time.Now().Format("20060102-150405") + workflowID := "external-storage-" + runID + request := externalstorage.OrderBatchRequest{ + BatchID: "BATCH-" + runID, + OrderCount: orderCount, + } + + fmt.Printf("Starting workflow %s (batch_id=%s, order_count=%d)\n", workflowID, request.BatchID, request.OrderCount) + + we, err := c.ExecuteWorkflow(context.Background(), + client.StartWorkflowOptions{ + ID: workflowID, + TaskQueue: externalstorage.TaskQueue, + }, + externalstorage.ProcessOrderBatchWorkflow, + request, + ) + if err != nil { + log.Fatalln("Unable to execute workflow", err) + } + + var summary externalstorage.BatchSummary + if err := we.Get(context.Background(), &summary); err != nil { + log.Fatalln("Unable to get workflow result", err) + } + + fmt.Printf("\nBatch %s: %d orders processed\n", summary.BatchID, summary.OrderCount) + fmt.Printf(" Total shipping cost: $%.2f\n", summary.TotalShippingCostUSD) + fmt.Printf(" Total weight: %.1f kg\n", summary.TotalWeightKg) + fmt.Printf(" Avg delivery: %.1f days\n", summary.AvgDeliveryDays) +} diff --git a/external-storage/worker/main.go b/external-storage/worker/main.go new file mode 100644 index 00000000..8abdc355 --- /dev/null +++ b/external-storage/worker/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "log" + + externalstorage "github.com/temporalio/samples-go/external-storage" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/worker" +) + +func main() { + c, err := externalstorage.NewClient(context.Background(), client.Options{}) + if err != nil { + log.Fatalln("Unable to create client", err) + } + defer c.Close() + + w := worker.New(c, externalstorage.TaskQueue, worker.Options{}) + w.RegisterWorkflow(externalstorage.ProcessOrderBatchWorkflow) + w.RegisterActivity(externalstorage.FetchOrders) + w.RegisterActivity(externalstorage.ProcessOrders) + + if err := w.Run(worker.InterruptCh()); err != nil { + log.Fatalln("Unable to start worker", err) + } +} diff --git a/external-storage/workflows.go b/external-storage/workflows.go new file mode 100644 index 00000000..9e6cc9f6 --- /dev/null +++ b/external-storage/workflows.go @@ -0,0 +1,143 @@ +package externalstorage + +import ( + "context" + "time" + + "go.temporal.io/sdk/workflow" +) + +const ( + // TaskQueue is the task queue used by the external-storage worker and starter. + TaskQueue = "external-storage-task-queue" + + // warehouseState is the fulfillment center state used to estimate delivery. + warehouseState = "TX" +) + +type Address struct { + Street string `json:"street"` + City string `json:"city"` + State string `json:"state"` + ZipCode string `json:"zip_code"` + Country string `json:"country"` +} + +type Customer struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Address Address `json:"address"` +} + +type OrderItem struct { + SKU string `json:"sku"` + Name string `json:"name"` + Description string `json:"description"` + Quantity int `json:"quantity"` + UnitPriceUSD float64 `json:"unit_price_usd"` + WeightKg float64 `json:"weight_kg"` +} + +type Order struct { + ID string `json:"id"` + Customer Customer `json:"customer"` + Items []OrderItem `json:"items"` + TotalWeightKg float64 `json:"total_weight_kg"` + ShippingNotes string `json:"shipping_notes"` +} + +type ProcessedOrder struct { + ID string `json:"id"` + CustomerID string `json:"customer_id"` + DestinationCity string `json:"destination_city"` + DestinationState string `json:"destination_state"` + TotalWeightKg float64 `json:"total_weight_kg"` + ShippingCostUSD float64 `json:"shipping_cost_usd"` + EstimatedDeliveryDays int `json:"estimated_delivery_days"` +} + +type OrderBatchRequest struct { + BatchID string `json:"batch_id"` + OrderCount int `json:"order_count"` +} + +type BatchSummary struct { + BatchID string `json:"batch_id"` + OrderCount int `json:"order_count"` + TotalShippingCostUSD float64 `json:"total_shipping_cost_usd"` + TotalWeightKg float64 `json:"total_weight_kg"` + AvgDeliveryDays float64 `json:"avg_delivery_days"` +} + +// FetchOrders returns the orders for a batch. The slice is intentionally large +// enough that, even after zlib compression, it exceeds the default +// ExternalStorage threshold and gets offloaded to S3. +func FetchOrders(_ context.Context, request OrderBatchRequest) ([]Order, error) { + return generateOrders(request.BatchID, request.OrderCount), nil +} + +// ProcessOrders computes a shipping cost and an estimated delivery day count +// for each order, and returns the per-order results. +func ProcessOrders(_ context.Context, orders []Order) ([]ProcessedOrder, error) { + results := make([]ProcessedOrder, len(orders)) + for i, order := range orders { + days := 5 + if order.Customer.Address.State == warehouseState { + days = 2 + } + results[i] = ProcessedOrder{ + ID: order.ID, + CustomerID: order.Customer.ID, + DestinationCity: order.Customer.Address.City, + DestinationState: order.Customer.Address.State, + TotalWeightKg: order.TotalWeightKg, + ShippingCostUSD: round2(2.50 + 1.20*order.TotalWeightKg), + EstimatedDeliveryDays: days, + } + } + return results, nil +} + +// ProcessOrderBatchWorkflow fetches a batch of orders and processes them. +// The workflow input and result are small. The intermediate order list (the +// output of FetchOrders and the input of ProcessOrders) is the payload that +// exceeds the size threshold and gets offloaded to external storage. +func ProcessOrderBatchWorkflow(ctx workflow.Context, request OrderBatchRequest) (BatchSummary, error) { + ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + StartToCloseTimeout: 5 * time.Minute, + }) + + var orders []Order + if err := workflow.ExecuteActivity(ctx, FetchOrders, request).Get(ctx, &orders); err != nil { + return BatchSummary{}, err + } + + var processed []ProcessedOrder + if err := workflow.ExecuteActivity(ctx, ProcessOrders, orders).Get(ctx, &processed); err != nil { + return BatchSummary{}, err + } + + var totalCost, totalWeight float64 + var totalDays int + for _, p := range processed { + totalCost += p.ShippingCostUSD + totalWeight += p.TotalWeightKg + totalDays += p.EstimatedDeliveryDays + } + var avgDays float64 + if len(processed) > 0 { + avgDays = float64(totalDays) / float64(len(processed)) + } + + return BatchSummary{ + BatchID: request.BatchID, + OrderCount: len(processed), + TotalShippingCostUSD: round2(totalCost), + TotalWeightKg: round2(totalWeight), + AvgDeliveryDays: round1(avgDays), + }, nil +} + +func round1(v float64) float64 { return float64(int(v*10+0.5)) / 10 } +func round2(v float64) float64 { return float64(int(v*100+0.5)) / 100 } diff --git a/go.mod b/go.mod index 5c4204ac..d057ba9b 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,14 @@ go 1.25.0 replace github.com/cactus/go-statsd-client => github.com/cactus/go-statsd-client/v5 v5.0.0 require ( + github.com/aws/aws-sdk-go-v2 v1.41.7 + github.com/aws/aws-sdk-go-v2/config v1.32.17 + github.com/aws/aws-sdk-go-v2/credentials v1.19.16 + github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 github.com/golang/mock v1.7.0-rc.1 github.com/golang/snappy v0.0.4 github.com/google/uuid v1.6.0 + github.com/johannesboyne/gofakes3 v0.0.0-20260208201424-4c385a1f6a73 github.com/nexus-rpc/sdk-go v0.6.0 github.com/opentracing/opentracing-go v1.2.0 github.com/pborman/uuid v1.2.1 @@ -23,6 +28,8 @@ require ( go.temporal.io/sdk v1.43.0 go.temporal.io/sdk/contrib/aws/lambdaworker v0.1.1 go.temporal.io/sdk/contrib/aws/lambdaworker/otel v0.1.1 + go.temporal.io/sdk/contrib/aws/s3driver v0.2.0 + go.temporal.io/sdk/contrib/aws/s3driver/awssdkv2 v0.2.0 go.temporal.io/sdk/contrib/datadog v0.5.0 go.temporal.io/sdk/contrib/envconfig v1.0.1 go.temporal.io/sdk/contrib/opentelemetry v0.7.0 @@ -61,6 +68,20 @@ require ( github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aws/aws-lambda-go v1.47.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect + github.com/aws/smithy-go v1.25.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -99,6 +120,7 @@ require ( github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/robfig/cron v1.2.0 // indirect + github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect github.com/shirou/gopsutil/v4 v4.25.8-0.20250809033336-ffcdc2b7662f // indirect github.com/stretchr/objx v0.5.2 // indirect @@ -123,6 +145,7 @@ require ( go.opentelemetry.io/otel/metric v1.42.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect @@ -135,6 +158,7 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect diff --git a/go.sum b/go.sum index 63aafd76..38941913 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,44 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI= github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= +github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= +github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY= +github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= +github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU= +github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.75 h1:S61/E3N01oral6B3y9hZ2E1iFDqCZPPOBoBQretCnBI= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.75/go.mod h1:bDMQbkI1vJbNjnvJYpPTSNYBkI/VIv18ngWb/K84tkk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= +github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= +github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -73,6 +111,8 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc= +github.com/cevatbarisyilmaz/ara v0.0.4/go.mod h1:BfFOxnUd6Mj6xmcvRxHN3Sr21Z1T3U2MYkYOmoQe4Ts= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -186,6 +226,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/johannesboyne/gofakes3 v0.0.0-20260208201424-4c385a1f6a73 h1:0xkWp+RMC2ImuKacheMHEAtrbOTMOa0kYkxyzM1Z/II= +github.com/johannesboyne/gofakes3 v0.0.0-20260208201424-4c385a1f6a73/go.mod h1:S4S9jGBVlLri0OeqrSSbCGG5vsI6he06UJyuz1WT1EE= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -289,6 +331,8 @@ github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfm github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= +github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc= github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= github.com/shirou/gopsutil/v4 v4.25.8-0.20250809033336-ffcdc2b7662f h1:S+PHRM3lk96X0/cGEGUukqltzkX/ekUx0F9DoCGK1G0= @@ -300,6 +344,8 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M= +github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -345,6 +391,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/collector/component v1.39.0 h1:GJw80zXURBG4h0sh97bPLEn2Ra+NAWUpskaooA0wru4= @@ -414,6 +462,8 @@ go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.0.1 h1:T go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.0.1/go.mod h1:riqUmAOJFDFuIAzZu/3V6cOrTyfWzpgNJnG5UwrapCk= go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.0.1 h1:z/oMlrCv3Kopwh/dtdRagJy+qsRRPA86/Ux3g7+zFXM= go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.0.1/go.mod h1:C7EHYSIiaALi9RnNORCVaPCQDuJgJEn/XxkctaTez1E= +go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8= +go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d/go.mod h1:92Uoe3l++MlthCm+koNi0tcUCX3anayogF0Pa/sp24k= go.temporal.io/api v1.5.0/go.mod h1:BqKxEJJYdxb5dqf0ODfzfMxh8UEQ5L3zKS51FiIYYkA= go.temporal.io/api v1.62.11 h1:MWDaooDvOJCIRb1atqeZX2ErDPNTsNc3/mMEVEvvaVU= go.temporal.io/api v1.62.11/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= @@ -424,6 +474,10 @@ go.temporal.io/sdk/contrib/aws/lambdaworker v0.1.1 h1:AQBa7CN+EOWhZaf4vr46TfxTZM go.temporal.io/sdk/contrib/aws/lambdaworker v0.1.1/go.mod h1:Rgn/tlb4MDNAAjnXKNgHui4IY+MogMCk4Y4c2YA6Dcc= go.temporal.io/sdk/contrib/aws/lambdaworker/otel v0.1.1 h1:sMTtpD5jsb4FeJadYkoOOzb84oWYK2/g0keb3IRO6xY= go.temporal.io/sdk/contrib/aws/lambdaworker/otel v0.1.1/go.mod h1:QtqQsUTPtylgShFZi4ce1d12WqE35byx+YO1Cnn/0cg= +go.temporal.io/sdk/contrib/aws/s3driver v0.2.0 h1:vQ0LvD/chtVWq7ZA6Z8j8KlZtr7YX4iHs7V4/JksYJg= +go.temporal.io/sdk/contrib/aws/s3driver v0.2.0/go.mod h1:5sdBFkgbHgi9VCJGcbV4ZCChIHzukpKSLFwxKdcqy9E= +go.temporal.io/sdk/contrib/aws/s3driver/awssdkv2 v0.2.0 h1:fNhUuib4JSm6W/OohiKW1e0WiWZk2sH7fXcHxqZYV3Y= +go.temporal.io/sdk/contrib/aws/s3driver/awssdkv2 v0.2.0/go.mod h1:S/ZaYSksU6K8l/aK0nI6W7DTBD5YmT5ye0D2Nh6vSn8= go.temporal.io/sdk/contrib/datadog v0.5.0 h1:GfiDiqWNzkHqxO00H3Inxv66H+KLwS8ifz/PITv4OXg= go.temporal.io/sdk/contrib/datadog v0.5.0/go.mod h1:MeHebmCM0ujSoNk2P4b+inyPmcR4IOZqygCVgpekAls= go.temporal.io/sdk/contrib/envconfig v1.0.1 h1:HZCcS6vNPJiUxJrkc5Wdeen+056LWmYe2dI0D6UuB5g= @@ -577,6 +631,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -640,6 +696,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/validator.v2 v2.0.0-20200605151824-2b28d334fa05/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= From de18a6772f7b992e0fc10699c07d30d68313de01 Mon Sep 17 00:00:00 2001 From: jmaeagle99 <44687433+jmaeagle99@users.noreply.github.com> Date: Wed, 20 May 2026 09:53:55 -0700 Subject: [PATCH 2/2] Fix spelling --- external-storage/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-storage/README.md b/external-storage/README.md index 8e14f510..a140f41d 100644 --- a/external-storage/README.md +++ b/external-storage/README.md @@ -26,7 +26,7 @@ on demand for the Temporal Web UI. ## Steps to run this sample 1. Run a [Temporal service](https://github.com/temporalio/samples-go/tree/main/#how-to-use). -2. In a separte terminal, run the mock S3 server. It listens on `:5000` and +2. In a separate terminal, run the mock S3 server. It listens on `:5000` and creates the `temporal-payloads` bucket. Leave it running. ``` go run ./external-storage/s3-mock