From 0d5c48ea8dcb3ebe6f4b734e1a774b8d3cd8a27c Mon Sep 17 00:00:00 2001 From: Joe Wright Date: Wed, 25 Feb 2026 23:11:34 +0000 Subject: [PATCH 1/2] backend/s3: optimize StateMgr workspace existence check Replace O(n) ListObjectsV2 pagination with O(1) HeadObject call when checking if a workspace state file exists in StateMgr. Previously, StateMgr called Workspaces() which listed ALL objects matching the workspace prefix, then linearly searched the results. With 15,000 workspaces this meant 15 API calls and 15,000 string comparisons just to check if one workspace exists. Now we call HeadObject directly on the specific state file path. Benchmark with 15,000 workspace objects: - Before: 14.4s avg (15 paginated ListObjectsV2 calls) - After: 10.9s avg (1 HeadObject call) - Improvement: 24-33% faster --- .../backend/remote-state/s3/backend_state.go | 16 +++--------- internal/backend/remote-state/s3/client.go | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/internal/backend/remote-state/s3/backend_state.go b/internal/backend/remote-state/s3/backend_state.go index 35d1481e72d3..a180781e465b 100644 --- a/internal/backend/remote-state/s3/backend_state.go +++ b/internal/backend/remote-state/s3/backend_state.go @@ -193,18 +193,10 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, tfdiags.Diagnostics) { // If we need to force-unlock, but for some reason the state no longer // exists, the user will have to use aws tools to manually fix the // situation. - existing, wDiags := b.Workspaces() - diags = diags.Append(wDiags) - if wDiags.HasErrors() { - return nil, diags - } - - exists := false - for _, s := range existing { - if s == name { - exists = true - break - } + ctx := context.TODO() + exists, err := client.Exists(ctx) + if err != nil { + return nil, diags.Append(err) } // We need to create the object so it's listed by States. diff --git a/internal/backend/remote-state/s3/client.go b/internal/backend/remote-state/s3/client.go index 66a9db6fe246..88266e9d8038 100644 --- a/internal/backend/remote-state/s3/client.go +++ b/internal/backend/remote-state/s3/client.go @@ -187,6 +187,31 @@ func (c *RemoteClient) get(ctx context.Context) (*remote.Payload, error) { return payload, nil } +func (c *RemoteClient) Exists(ctx context.Context) (bool, error) { + headInput := &s3.HeadObjectInput{ + Bucket: aws.String(c.bucketName), + Key: aws.String(c.path), + } + if c.serverSideEncryption && c.customerEncryptionKey != nil { + headInput.SSECustomerKey = aws.String(base64.StdEncoding.EncodeToString(c.customerEncryptionKey)) + headInput.SSECustomerAlgorithm = aws.String(s3EncryptionAlgorithm) + headInput.SSECustomerKeyMD5 = aws.String(c.getSSECustomerKeyMD5()) + } + + _, err := c.s3Client.HeadObject(ctx, headInput) + if err != nil { + switch { + case IsA[*s3types.NoSuchBucket](err): + return false, fmt.Errorf(errS3NoSuchBucket, c.bucketName, err) + case IsA[*s3types.NotFound](err): + return false, nil + } + return false, fmt.Errorf("Unable to access object %q in S3 bucket %q: %w", c.path, c.bucketName, err) + } + + return true, nil +} + func (c *RemoteClient) Put(data []byte) tfdiags.Diagnostics { var diags tfdiags.Diagnostics return diags.Append(c.put(data)) From 587fa3e631291d22ba486d1d265c1fe250bd382b Mon Sep 17 00:00:00 2001 From: Joe Wright Date: Thu, 26 Feb 2026 10:21:33 +0000 Subject: [PATCH 2/2] Include ChangeLog --- .changes/v1.15/ENHANCEMENTS-20260226-102105.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/v1.15/ENHANCEMENTS-20260226-102105.yaml diff --git a/.changes/v1.15/ENHANCEMENTS-20260226-102105.yaml b/.changes/v1.15/ENHANCEMENTS-20260226-102105.yaml new file mode 100644 index 000000000000..93936a066266 --- /dev/null +++ b/.changes/v1.15/ENHANCEMENTS-20260226-102105.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: 'backend/s3: Improved performance when using workspaces by replacing workspace listing with a direct state file check' +time: 2026-02-26T10:21:05.578904Z +custom: + Issue: "33137"