Skip to content

Commit ab8d83b

Browse files
committed
Introduce configuration knobs for determining the staleness of a lock file.
1 parent 6c8b42a commit ab8d83b

File tree

5 files changed

+59
-30
lines changed

5 files changed

+59
-30
lines changed

backend/remote-state/gcs/backend.go

+32
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"os"
1010
"strings"
11+
"time"
1112

1213
"cloud.google.com/go/storage"
1314
"github.com/hashicorp/terraform/backend"
@@ -35,6 +36,13 @@ type Backend struct {
3536

3637
projectID string
3738
region string
39+
40+
// Time between consecutive heartbeats on the lock file.
41+
lockHeartbeatInterval time.Duration
42+
43+
// The mininum duration that must have passed since the youngest
44+
// recorded heartbeat before the lock file is considered stale/orphaned.
45+
lockStaleAfter time.Duration
3846
}
3947

4048
func New() backend.Backend {
@@ -88,6 +96,20 @@ func New() backend.Backend {
8896
Description: "Region / location in which to create the bucket",
8997
Default: "",
9098
},
99+
100+
"lock_heartbeat_interval": {
101+
Type: schema.TypeString,
102+
Optional: true,
103+
Description: "Time between consecutive heartbeats on the lock file as a duration string (cf. https://golang.org/pkg/time/#ParseDuration).",
104+
Default: "1m",
105+
},
106+
107+
"lock_stale_after": {
108+
Type: schema.TypeString,
109+
Optional: true,
110+
Description: "Mininum duration (cf. https://golang.org/pkg/time/#ParseDuration) that must have passed since the youngest recorded heartbeat before the lock file is considered stale/orphaned.",
111+
Default: "15m",
112+
},
91113
},
92114
}
93115

@@ -188,6 +210,16 @@ func (b *Backend) configure(ctx context.Context) error {
188210
b.encryptionKey = k
189211
}
190212

213+
b.lockHeartbeatInterval, err = time.ParseDuration(data.Get("lock_heartbeat_interval").(string))
214+
if err != nil {
215+
return fmt.Errorf("Error parsing lock_heartbeat_interval: %s", err)
216+
}
217+
218+
b.lockStaleAfter, err = time.ParseDuration(data.Get("lock_stale_after").(string))
219+
if err != nil {
220+
return fmt.Errorf("Error parsing lock_stale_after: %s", err)
221+
}
222+
191223
return nil
192224
}
193225

backend/remote-state/gcs/backend_state.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,14 @@ func (b *Backend) client(name string) (*remoteClient, error) {
7575
}
7676

7777
return &remoteClient{
78-
storageContext: b.storageContext,
79-
storageClient: b.storageClient,
80-
bucketName: b.bucketName,
81-
stateFilePath: b.stateFile(name),
82-
lockFilePath: b.lockFile(name),
83-
encryptionKey: b.encryptionKey,
78+
storageContext: b.storageContext,
79+
storageClient: b.storageClient,
80+
bucketName: b.bucketName,
81+
stateFilePath: b.stateFile(name),
82+
lockFilePath: b.lockFile(name),
83+
encryptionKey: b.encryptionKey,
84+
lockHeartbeatInterval: b.lockHeartbeatInterval,
85+
lockStaleAfter: b.lockStaleAfter,
8486
}, nil
8587
}
8688

backend/remote-state/gcs/backend_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -207,15 +207,15 @@ func testStaleLocks(t *testing.T, b1, b2 backend.Backend) {
207207
infoB.Who = "clientB"
208208

209209
// For faster tests, reduce the duration until the lock is considered stale.
210-
heartbeatInterval = 5 * time.Second
211-
minHeartbeatAgeUntilStale = 20 * time.Second
210+
lockerB.(*remote.State).Client.(*remoteClient).lockHeartbeatInterval = 5 * time.Second
211+
lockerB.(*remote.State).Client.(*remoteClient).lockStaleAfter = 20 * time.Second
212212

213213
lockIDA, err := lockerA.Lock(infoA)
214214
if err != nil {
215215
t.Fatal("unable to get initial lock:", err)
216216
}
217217

218-
// Stop heartbeating on the lock file. It will be considered stale after minHeartbeatAgeUntilStale.
218+
// Stop heartbeating on the lock file. It will be considered stale after lockStaleAfter.
219219
lockerA.(*remote.State).Client.(*remoteClient).stopHeartbeatCh <- true
220220

221221
// Lock is still held by A after 10 seconds.

backend/remote-state/gcs/client.go

+14-21
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"time"
1111

1212
"cloud.google.com/go/storage"
13-
multierror "github.com/hashicorp/go-multierror"
13+
"github.com/hashicorp/go-multierror"
1414
"github.com/hashicorp/terraform/state"
1515
"github.com/hashicorp/terraform/state/remote"
1616
"golang.org/x/net/context"
@@ -22,12 +22,14 @@ import (
2222
type remoteClient struct {
2323
mutex sync.Mutex
2424

25-
storageContext context.Context
26-
storageClient *storage.Client
27-
bucketName string
28-
stateFilePath string
29-
lockFilePath string
30-
encryptionKey []byte
25+
storageContext context.Context
26+
storageClient *storage.Client
27+
bucketName string
28+
stateFilePath string
29+
lockFilePath string
30+
encryptionKey []byte
31+
lockHeartbeatInterval time.Duration
32+
lockStaleAfter time.Duration
3133

3234
// The initial generation number of the lock file created by this
3335
// remoteClient.
@@ -42,18 +44,9 @@ type remoteClient struct {
4244
// heartbeats on it. This header facilitates a safe migration from previous
4345
// Terraform versions that do not yet perform any heartbeats on the lock file.
4446
// A lock file will only be considered stale and force-unlocked if its age
45-
// exceeds minHeartbeatAgeUntilStale AND this metadata header is present.
47+
// exceeds lockStaleAfter AND this metadata header is present.
4648
const metadataHeaderHeartbeatEnabled = "x-goog-meta-heartbeating"
4749

48-
var (
49-
// Time between consecutive heartbeats on the lock file.
50-
heartbeatInterval = 1 * time.Minute
51-
52-
// The mininum duration that must have passed since the youngest
53-
// recorded heartbeat before the lock file is considered stale/orphaned.
54-
minHeartbeatAgeUntilStale = 15 * time.Minute
55-
)
56-
5750
func (c *remoteClient) Get() (payload *remote.Payload, err error) {
5851
stateFileReader, err := c.stateFile().NewReader(c.storageContext)
5952
if err != nil {
@@ -153,8 +146,8 @@ func (c *remoteClient) unlockIfStale() bool {
153146
log.Printf("[TRACE] Found existing lock file %s from an older client that does not perform heartbeats", c.lockFileURL())
154147
return false
155148
}
156-
age := time.Now().Sub(attrs.Updated)
157-
if age > minHeartbeatAgeUntilStale {
149+
age := time.Since(attrs.Updated)
150+
if age > c.lockStaleAfter {
158151
log.Printf("[WARN] Existing lock file %s is considered stale, last heartbeat was %s ago", c.lockFileURL(), age)
159152
if err := c.Unlock(strconv.FormatInt(attrs.Generation, 10)); err != nil {
160153
log.Printf("[WARN] Failed to release stale lock: %s", err)
@@ -170,9 +163,9 @@ func (c *remoteClient) unlockIfStale() bool {
170163
// heartbeatLockFile periodically updates the "updated" timestamp of the lock
171164
// file until the lock is released in Unlock().
172165
func (c *remoteClient) heartbeatLockFile() {
173-
log.Printf("[TRACE] Starting heartbeat on lock file %s, interval is %s", c.lockFileURL(), heartbeatInterval)
166+
log.Printf("[TRACE] Starting heartbeat on lock file %s, interval is %s", c.lockFileURL(), c.lockHeartbeatInterval)
174167

175-
ticker := time.NewTicker(heartbeatInterval)
168+
ticker := time.NewTicker(c.lockHeartbeatInterval)
176169
defer ticker.Stop()
177170

178171
defer func() {

website/docs/backends/types/gcs.html.md

+2
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,5 @@ The following configuration options are supported:
6060
* `region` / `GOOGLE_REGION` - (Optional) The region in which a new bucket is created.
6161
For more information, see [Bucket Locations](https://cloud.google.com/storage/docs/bucket-locations).
6262
* `encryption_key` / `GOOGLE_ENCRYPTION_KEY` - (Optional) A 32 byte base64 encoded 'customer supplied encryption key' used to encrypt all state. For more information see [Customer Supplied Encryption Keys](https://cloud.google.com/storage/docs/encryption#customer-supplied).
63+
* `lock_heartbeat_interval` - (Optional) The time between consecutive heartbeats on the lock file as a [duration string](https://golang.org/pkg/time/#ParseDuration). Defaults to "1m".
64+
* `lock_stale_after` - (Optional) The mininum duration (as a [duration string](https://golang.org/pkg/time/#ParseDuration)) that must have passed since the youngest recorded heartbeat before the lock file is considered stale/orphaned. Defaults to "15m".

0 commit comments

Comments
 (0)