Skip to content

Commit d00e89d

Browse files
committed
server: Require either cluster version v3.6 or --experimental-enable-lease-checkpoint-persist to persist lease remainingTTL
To avoid inconsistant behavior during cluster upgrade we are feature gating persistance behind cluster version. This should ensure that all cluster members are upgraded to v3.6 before changing behavior. To allow backporting this fix to v3.5 we are also introducing flag --experimental-enable-lease-checkpoint-persist that will allow for smooth upgrade in v3.5 clusters with this feature enabled.
1 parent eddfb42 commit d00e89d

File tree

12 files changed

+233
-31
lines changed

12 files changed

+233
-31
lines changed

server/config/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,12 @@ type ServerConfig struct {
147147

148148
ForceNewCluster bool
149149

150-
// EnableLeaseCheckpoint enables primary lessor to persist lease remainingTTL to prevent indefinite auto-renewal of long lived leases.
150+
// EnableLeaseCheckpoint enables leader to send regular checkpoints to other members to prevent reset of remaining TTL on leader change.
151151
EnableLeaseCheckpoint bool
152152
// LeaseCheckpointInterval time.Duration is the wait duration between lease checkpoints.
153153
LeaseCheckpointInterval time.Duration
154+
// LeaseCheckpointPersist enables persisting remainingTTL to prevent indefinite auto-renewal of long lived leases. Always enabled in v3.6. Should be used to ensure smooth upgrade from v3.5 clusters with this feature enabled.
155+
LeaseCheckpointPersist bool
154156

155157
EnableGRPCGateway bool
156158

server/embed/config.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,10 +314,15 @@ type Config struct {
314314
// Deprecated in v3.5.
315315
// TODO: Delete in v3.6 (https://github.com/etcd-io/etcd/issues/12913)
316316
ExperimentalEnableV2V3 string `json:"experimental-enable-v2v3"`
317-
// ExperimentalEnableLeaseCheckpoint enables primary lessor to persist lease remainingTTL to prevent indefinite auto-renewal of long lived leases.
318-
ExperimentalEnableLeaseCheckpoint bool `json:"experimental-enable-lease-checkpoint"`
319-
ExperimentalCompactionBatchLimit int `json:"experimental-compaction-batch-limit"`
320-
ExperimentalWatchProgressNotifyInterval time.Duration `json:"experimental-watch-progress-notify-interval"`
317+
// ExperimentalEnableLeaseCheckpoint enables leader to send regular checkpoints to other members to prevent reset of remaining TTL on leader change.
318+
ExperimentalEnableLeaseCheckpoint bool `json:"experimental-enable-lease-checkpoint"`
319+
// ExperimentalEnableLeaseCheckpointPersist enables persisting remainingTTL to prevent indefinite auto-renewal of long lived leases. Always enabled in v3.6. Should be used to ensure smooth upgrade from v3.5 clusters with this feature enabled.
320+
// Requires experimental-enable-lease-checkpoint to be enabled.
321+
// Deprecated in v3.6.
322+
// TODO: Delete in v3.7
323+
ExperimentalEnableLeaseCheckpointPersist bool `json:"experimental-enable-lease-checkpoint-persist"`
324+
ExperimentalCompactionBatchLimit int `json:"experimental-compaction-batch-limit"`
325+
ExperimentalWatchProgressNotifyInterval time.Duration `json:"experimental-watch-progress-notify-interval"`
321326
// ExperimentalWarningApplyDuration is the time duration after which a warning is generated if applying request
322327
// takes more time than this value.
323328
ExperimentalWarningApplyDuration time.Duration `json:"experimental-warning-apply-duration"`
@@ -678,6 +683,14 @@ func (cfg *Config) Validate() error {
678683
return fmt.Errorf("unknown auto-compaction-mode %q", cfg.AutoCompactionMode)
679684
}
680685

686+
if !cfg.ExperimentalEnableLeaseCheckpointPersist && cfg.ExperimentalEnableLeaseCheckpoint {
687+
cfg.logger.Warn("Detected that checkpointing is enabled without persistence. Consider enabling experimental-enable-lease-checkpoint-persist")
688+
}
689+
690+
if cfg.ExperimentalEnableLeaseCheckpointPersist && !cfg.ExperimentalEnableLeaseCheckpoint {
691+
return fmt.Errorf("setting experimental-enable-lease-checkpoint-persist requires experimental-enable-lease-checkpoint")
692+
}
693+
681694
return nil
682695
}
683696

server/embed/config_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,56 @@ func TestPeerURLsMapAndTokenFromSRV(t *testing.T) {
291291
}
292292
}
293293

294+
func TestLeaseCheckpointValidate(t *testing.T) {
295+
tcs := []struct {
296+
name string
297+
configFunc func() Config
298+
expectError bool
299+
}{
300+
{
301+
name: "Default config should pass",
302+
configFunc: func() Config {
303+
return *NewConfig()
304+
},
305+
},
306+
{
307+
name: "Enabling checkpoint leases should pass",
308+
configFunc: func() Config {
309+
cfg := *NewConfig()
310+
cfg.ExperimentalEnableLeaseCheckpoint = true
311+
return cfg
312+
},
313+
},
314+
{
315+
name: "Enabling checkpoint leases and persist should pass",
316+
configFunc: func() Config {
317+
cfg := *NewConfig()
318+
cfg.ExperimentalEnableLeaseCheckpoint = true
319+
cfg.ExperimentalEnableLeaseCheckpointPersist = true
320+
return cfg
321+
},
322+
},
323+
{
324+
name: "Enabling checkpoint leases persist without checkpointing itself should fail",
325+
configFunc: func() Config {
326+
cfg := *NewConfig()
327+
cfg.ExperimentalEnableLeaseCheckpointPersist = true
328+
return cfg
329+
},
330+
expectError: true,
331+
},
332+
}
333+
for _, tc := range tcs {
334+
t.Run(tc.name, func(t *testing.T) {
335+
cfg := tc.configFunc()
336+
err := cfg.Validate()
337+
if (err != nil) != tc.expectError {
338+
t.Errorf("config.Validate() = %q, expected error: %v", err, tc.expectError)
339+
}
340+
})
341+
}
342+
}
343+
294344
func TestLogRotation(t *testing.T) {
295345
tests := []struct {
296346
name string

server/embed/etcd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ func StartEtcd(inCfg *Config) (e *Etcd, err error) {
216216
ExperimentalEnableDistributedTracing: cfg.ExperimentalEnableDistributedTracing,
217217
UnsafeNoFsync: cfg.UnsafeNoFsync,
218218
EnableLeaseCheckpoint: cfg.ExperimentalEnableLeaseCheckpoint,
219+
LeaseCheckpointPersist: cfg.ExperimentalEnableLeaseCheckpointPersist,
219220
CompactionBatchLimit: cfg.ExperimentalCompactionBatchLimit,
220221
WatchProgressNotifyInterval: cfg.ExperimentalWatchProgressNotifyInterval,
221222
DowngradeCheckTime: cfg.ExperimentalDowngradeCheckTime,

server/etcdmain/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,9 @@ func newConfig() *config {
280280
fs.BoolVar(&cfg.ec.ExperimentalInitialCorruptCheck, "experimental-initial-corrupt-check", cfg.ec.ExperimentalInitialCorruptCheck, "Enable to check data corruption before serving any client/peer traffic.")
281281
fs.DurationVar(&cfg.ec.ExperimentalCorruptCheckTime, "experimental-corrupt-check-time", cfg.ec.ExperimentalCorruptCheckTime, "Duration of time between cluster corruption check passes.")
282282

283-
fs.BoolVar(&cfg.ec.ExperimentalEnableLeaseCheckpoint, "experimental-enable-lease-checkpoint", false, "Enable to persist lease remaining TTL to prevent indefinite auto-renewal of long lived leases.")
283+
fs.BoolVar(&cfg.ec.ExperimentalEnableLeaseCheckpoint, "experimental-enable-lease-checkpoint", false, "Enable leader to send regular checkpoints to other members to prevent reset of remaining TTL on leader change.")
284+
// TODO: delete in v3.7
285+
fs.BoolVar(&cfg.ec.ExperimentalEnableLeaseCheckpointPersist, "experimental-enable-lease-checkpoint-persist", false, "Enable persisting remainingTTL to prevent indefinite auto-renewal of long lived leases. Always enabled in v3.6. Should be used to ensure smooth upgrade from v3.5 clusters with this feature enabled. Requires experimental-enable-lease-checkpoint to be enabled.")
284286
fs.IntVar(&cfg.ec.ExperimentalCompactionBatchLimit, "experimental-compaction-batch-limit", cfg.ec.ExperimentalCompactionBatchLimit, "Sets the maximum revisions deleted in each compaction batch.")
285287
fs.DurationVar(&cfg.ec.ExperimentalWatchProgressNotifyInterval, "experimental-watch-progress-notify-interval", cfg.ec.ExperimentalWatchProgressNotifyInterval, "Duration of periodic watch progress notifications.")
286288
fs.DurationVar(&cfg.ec.ExperimentalDowngradeCheckTime, "experimental-downgrade-check-time", cfg.ec.ExperimentalDowngradeCheckTime, "Duration of time between two downgrade status check.")

server/etcdserver/server.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,9 +592,10 @@ func NewServer(cfg config.ServerConfig) (srv *EtcdServer, err error) {
592592

593593
// always recover lessor before kv. When we recover the mvcc.KV it will reattach keys to its leases.
594594
// If we recover mvcc.KV first, it will attach the keys to the wrong lessor before it recovers.
595-
srv.lessor = lease.NewLessor(srv.Logger(), srv.be, lease.LessorConfig{
595+
srv.lessor = lease.NewLessor(srv.Logger(), srv.be, srv.cluster, lease.LessorConfig{
596596
MinLeaseTTL: int64(math.Ceil(minTTL.Seconds())),
597597
CheckpointInterval: cfg.LeaseCheckpointInterval,
598+
CheckpointPersist: cfg.LeaseCheckpointPersist,
598599
ExpiredLeasesRetryInterval: srv.Cfg.ReqTimeout(),
599600
})
600601

server/lease/leasehttp/http_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestRenewHTTP(t *testing.T) {
3131
be, _ := betesting.NewTmpBackend(t, time.Hour, 10000)
3232
defer betesting.Close(t, be)
3333

34-
le := lease.NewLessor(lg, be, lease.LessorConfig{MinLeaseTTL: int64(5)})
34+
le := lease.NewLessor(lg, be, nil, lease.LessorConfig{MinLeaseTTL: int64(5)})
3535
le.Promote(time.Second)
3636
l, err := le.Grant(1, int64(5))
3737
if err != nil {
@@ -55,7 +55,7 @@ func TestTimeToLiveHTTP(t *testing.T) {
5555
be, _ := betesting.NewTmpBackend(t, time.Hour, 10000)
5656
defer betesting.Close(t, be)
5757

58-
le := lease.NewLessor(lg, be, lease.LessorConfig{MinLeaseTTL: int64(5)})
58+
le := lease.NewLessor(lg, be, nil, lease.LessorConfig{MinLeaseTTL: int64(5)})
5959
le.Promote(time.Second)
6060
l, err := le.Grant(1, int64(5))
6161
if err != nil {
@@ -96,7 +96,7 @@ func testApplyTimeout(t *testing.T, f func(*lease.Lease, string) error) {
9696
be, _ := betesting.NewTmpBackend(t, time.Hour, 10000)
9797
defer betesting.Close(t, be)
9898

99-
le := lease.NewLessor(lg, be, lease.LessorConfig{MinLeaseTTL: int64(5)})
99+
le := lease.NewLessor(lg, be, nil, lease.LessorConfig{MinLeaseTTL: int64(5)})
100100
le.Promote(time.Second)
101101
l, err := le.Grant(1, int64(5))
102102
if err != nil {

server/lease/lessor.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"sync"
2525
"time"
2626

27+
"github.com/coreos/go-semver/semver"
2728
pb "go.etcd.io/etcd/api/v3/etcdserverpb"
2829
"go.etcd.io/etcd/server/v3/lease/leasepb"
2930
"go.etcd.io/etcd/server/v3/mvcc/backend"
@@ -37,6 +38,8 @@ const NoLease = LeaseID(0)
3738
// MaxLeaseTTL is the maximum lease TTL value
3839
const MaxLeaseTTL = 9000000000
3940

41+
var v3_6 = semver.Version{Major: 3, Minor: 6}
42+
4043
var (
4144
forever = time.Time{}
4245

@@ -180,19 +183,29 @@ type lessor struct {
180183
checkpointInterval time.Duration
181184
// the interval to check if the expired lease is revoked
182185
expiredLeaseRetryInterval time.Duration
186+
// whether lessor should always persist remaining TTL (always enabled in v3.6).
187+
checkpointPersist bool
188+
// cluster is used to adapt lessor logic based on cluster version
189+
cluster cluster
190+
}
191+
192+
type cluster interface {
193+
// Version is the cluster-wide minimum major.minor version.
194+
Version() *semver.Version
183195
}
184196

185197
type LessorConfig struct {
186198
MinLeaseTTL int64
187199
CheckpointInterval time.Duration
188200
ExpiredLeasesRetryInterval time.Duration
201+
CheckpointPersist bool
189202
}
190203

191-
func NewLessor(lg *zap.Logger, b backend.Backend, cfg LessorConfig) Lessor {
192-
return newLessor(lg, b, cfg)
204+
func NewLessor(lg *zap.Logger, b backend.Backend, cluster cluster, cfg LessorConfig) Lessor {
205+
return newLessor(lg, b, cluster, cfg)
193206
}
194207

195-
func newLessor(lg *zap.Logger, b backend.Backend, cfg LessorConfig) *lessor {
208+
func newLessor(lg *zap.Logger, b backend.Backend, cluster cluster, cfg LessorConfig) *lessor {
196209
checkpointInterval := cfg.CheckpointInterval
197210
expiredLeaseRetryInterval := cfg.ExpiredLeasesRetryInterval
198211
if checkpointInterval == 0 {
@@ -210,11 +223,13 @@ func newLessor(lg *zap.Logger, b backend.Backend, cfg LessorConfig) *lessor {
210223
minLeaseTTL: cfg.MinLeaseTTL,
211224
checkpointInterval: checkpointInterval,
212225
expiredLeaseRetryInterval: expiredLeaseRetryInterval,
226+
checkpointPersist: cfg.CheckpointPersist,
213227
// expiredC is a small buffered chan to avoid unnecessary blocking.
214228
expiredC: make(chan []*Lease, 16),
215229
stopC: make(chan struct{}),
216230
doneC: make(chan struct{}),
217231
lg: lg,
232+
cluster: cluster,
218233
}
219234
l.initAndRecover()
220235

@@ -351,7 +366,9 @@ func (le *lessor) Checkpoint(id LeaseID, remainingTTL int64) error {
351366
if l, ok := le.leaseMap[id]; ok {
352367
// when checkpointing, we only update the remainingTTL, Promote is responsible for applying this to lease expiry
353368
l.remainingTTL = remainingTTL
354-
l.persistTo(le.b)
369+
if le.shouldPersistCheckpoints() {
370+
l.persistTo(le.b)
371+
}
355372
if le.isPrimary() {
356373
// schedule the next checkpoint as needed
357374
le.scheduleCheckpointIfNeeded(l)
@@ -360,6 +377,15 @@ func (le *lessor) Checkpoint(id LeaseID, remainingTTL int64) error {
360377
return nil
361378
}
362379

380+
func (le *lessor) shouldPersistCheckpoints() bool {
381+
cv := le.cluster.Version()
382+
return le.checkpointPersist || (cv != nil && greaterOrEqual(*cv, v3_6))
383+
}
384+
385+
func greaterOrEqual(first, second semver.Version) bool {
386+
return !first.LessThan(second)
387+
}
388+
363389
// Renew renews an existing lease. If the given lease does not exist or
364390
// has expired, an error will be returned.
365391
func (le *lessor) Renew(id LeaseID) (int64, error) {

server/lease/lessor_bench_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func setUp(t testing.TB) (le *lessor, tearDown func()) {
6868
be, _ := betesting.NewDefaultTmpBackend(t)
6969
// MinLeaseTTL is negative, so we can grant expired lease in benchmark.
7070
// ExpiredLeasesRetryInterval should small, so benchmark of findExpired will recheck expired lease.
71-
le = newLessor(lg, be, LessorConfig{MinLeaseTTL: -1000, ExpiredLeasesRetryInterval: 10 * time.Microsecond})
71+
le = newLessor(lg, be, nil, LessorConfig{MinLeaseTTL: -1000, ExpiredLeasesRetryInterval: 10 * time.Microsecond})
7272
le.SetRangeDeleter(func() TxnDelete {
7373
ftd := &FakeTxnDelete{be.BatchTx()}
7474
ftd.Lock()

0 commit comments

Comments
 (0)