github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/core/raftlease/fsm.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package raftlease 5 6 import ( 7 "io" 8 "sync" 9 "time" 10 11 "github.com/hashicorp/raft" 12 "github.com/juju/errors" 13 "github.com/juju/utils/set" 14 yaml "gopkg.in/yaml.v2" 15 16 "github.com/juju/juju/core/globalclock" 17 "github.com/juju/juju/core/lease" 18 ) 19 20 const ( 21 // CommandVersion is the current version of the command format. If 22 // this changes then we need to be sure that reading and applying 23 // commands for previous versions still works. 24 CommandVersion = 1 25 26 // SnapshotVersion is the current version of the snapshot 27 // format. Similarly, changes to the snapshot representation need 28 // to be backward-compatible. 29 SnapshotVersion = 1 30 31 // OperationClaim denotes claiming a new lease. 32 OperationClaim = "claim" 33 34 // OperationExtend denotes extending an already-held lease. 35 OperationExtend = "extend" 36 37 // OperationSetTime denotes updating stored global time (which 38 // will also remove any expired leases). 39 OperationSetTime = "setTime" 40 41 // OperationPin pins a lease, preventing it from expiring 42 // until it is unpinned. 43 OperationPin = "pin" 44 45 // OperationUnpin unpins a lease, restoring normal 46 // lease expiry behaviour. 47 OperationUnpin = "unpin" 48 ) 49 50 // FSMResponse defines what will be available on the return value from 51 // FSM apply calls. 52 type FSMResponse interface { 53 // Error is a lease error (rather than anything to do with the 54 // raft machinery). 55 Error() error 56 57 // Notify tells the target what changes occurred because of the 58 // applied command. 59 Notify(NotifyTarget) 60 } 61 62 // NewFSM returns a new FSM to store lease information. 63 func NewFSM() *FSM { 64 return &FSM{ 65 entries: make(map[lease.Key]*entry), 66 pinned: make(map[lease.Key]set.Strings), 67 } 68 } 69 70 // FSM stores the state of leases in the system. 71 type FSM struct { 72 mu sync.Mutex 73 globalTime time.Time 74 entries map[lease.Key]*entry 75 76 // Pinned leases are denoted by having a non-empty collection of tags 77 // representing the applications requiring pinned behaviour, 78 // against their key. 79 // This allows different Juju concerns to pin leases, but remove only 80 // their own pins. It is done to avoid restoring normal expiration 81 // to a lease pinned by another concern operating under under the 82 // assumption that the lease holder will not change. 83 pinned map[lease.Key]set.Strings 84 } 85 86 func (f *FSM) claim(key lease.Key, holder string, duration time.Duration) *response { 87 if _, found := f.entries[key]; found { 88 return invalidResponse() 89 } 90 f.entries[key] = &entry{ 91 holder: holder, 92 start: f.globalTime, 93 duration: duration, 94 } 95 return &response{claimed: key, claimer: holder} 96 } 97 98 func (f *FSM) extend(key lease.Key, holder string, duration time.Duration) *response { 99 entry, found := f.entries[key] 100 if !found { 101 return invalidResponse() 102 } 103 if entry.holder != holder { 104 return invalidResponse() 105 } 106 expiry := f.globalTime.Add(duration) 107 if !expiry.After(entry.start.Add(entry.duration)) { 108 // No extension needed - the lease already expires after the 109 // new time. 110 return &response{} 111 } 112 // entry is a pointer back into the f.entries map, so this update 113 // isn't lost. 114 entry.start = f.globalTime 115 entry.duration = duration 116 return &response{} 117 } 118 119 func (f *FSM) pin(key lease.Key, entity string) *response { 120 if f.pinned[key] == nil { 121 f.pinned[key] = set.NewStrings() 122 } 123 f.pinned[key].Add(entity) 124 return &response{} 125 } 126 127 func (f *FSM) unpin(key lease.Key, entity string) *response { 128 if f.pinned[key] != nil { 129 f.pinned[key].Remove(entity) 130 } 131 return &response{} 132 } 133 134 func (f *FSM) setTime(oldTime, newTime time.Time) *response { 135 if f.globalTime != oldTime { 136 return &response{err: globalclock.ErrConcurrentUpdate} 137 } 138 f.globalTime = newTime 139 return &response{expired: f.removeExpired(newTime)} 140 } 141 142 // expired returns a collection of keys for leases that have expired. 143 // Any pinned leases are not included in the return. 144 func (f *FSM) removeExpired(newTime time.Time) []lease.Key { 145 var expired []lease.Key 146 for key, entry := range f.entries { 147 expiry := entry.start.Add(entry.duration) 148 if expiry.Before(newTime) && !f.isPinned(key) { 149 delete(f.entries, key) 150 expired = append(expired, key) 151 } 152 } 153 return expired 154 } 155 156 // GlobalTime returns the FSM's internal time. 157 func (f *FSM) GlobalTime() time.Time { 158 return f.globalTime 159 } 160 161 // Leases gets information about all of the leases in the system, 162 // optionally filtered by the input lease keys. 163 func (f *FSM) Leases(getLocalTime func() time.Time, keys ...lease.Key) map[lease.Key]lease.Info { 164 if len(keys) > 0 { 165 return f.filteredLeases(getLocalTime, keys) 166 } 167 return f.allLeases(getLocalTime) 168 } 169 170 // filteredLeases is an optimisation for anticipated usage. 171 // There will usually be a single key for filtering, so iterating over the 172 // filter list and retrieving from entries will be fastest by far. 173 func (f *FSM) filteredLeases(getLocalTime func() time.Time, keys []lease.Key) map[lease.Key]lease.Info { 174 results := make(map[lease.Key]lease.Info) 175 f.mu.Lock() 176 localTime := getLocalTime() 177 for _, key := range keys { 178 if entry, ok := f.entries[key]; ok { 179 results[key] = f.infoFromEntry(localTime, key, entry) 180 } 181 } 182 f.mu.Unlock() 183 return results 184 } 185 186 func (f *FSM) allLeases(getLocalTime func() time.Time) map[lease.Key]lease.Info { 187 results := make(map[lease.Key]lease.Info) 188 f.mu.Lock() 189 localTime := getLocalTime() 190 for key, entry := range f.entries { 191 results[key] = f.infoFromEntry(localTime, key, entry) 192 } 193 f.mu.Unlock() 194 return results 195 } 196 197 func (f *FSM) infoFromEntry(localTime time.Time, key lease.Key, entry *entry) lease.Info { 198 globalExpiry := entry.start.Add(entry.duration) 199 200 // Pinned leases are always represented as having an expiry in the future. 201 // This prevents the lease manager from waking up thinking it has some 202 // expiry events to handle. 203 remaining := globalExpiry.Sub(f.globalTime) 204 if f.isPinned(key) { 205 remaining = 30 * time.Second 206 } 207 localExpiry := localTime.Add(remaining) 208 209 return lease.Info{ 210 Holder: entry.holder, 211 Expiry: localExpiry, 212 } 213 } 214 215 // Pinned returns all of the currently known lease pins and applications 216 // requiring the pinned behaviour. 217 func (f *FSM) Pinned() map[lease.Key][]string { 218 f.mu.Lock() 219 pinned := make(map[lease.Key][]string) 220 for key, entities := range f.pinned { 221 if !entities.IsEmpty() { 222 pinned[key] = entities.SortedValues() 223 } 224 } 225 f.mu.Unlock() 226 return pinned 227 } 228 229 func (f *FSM) isPinned(key lease.Key) bool { 230 return !f.pinned[key].IsEmpty() 231 } 232 233 // entry holds the details of a lease. 234 type entry struct { 235 // holder identifies the current holder of the lease. 236 holder string 237 238 // start is the global time at which the lease started. 239 start time.Time 240 241 // duration is the duration for which the lease is valid, 242 // from the start time. 243 duration time.Duration 244 } 245 246 var _ FSMResponse = (*response)(nil) 247 248 // response stores what happened as a result of applying a command. 249 type response struct { 250 err error 251 claimer string 252 claimed lease.Key 253 expired []lease.Key 254 } 255 256 // Error is part of FSMResponse. 257 func (r *response) Error() error { 258 return r.err 259 } 260 261 // Notify is part of FSMResponse. 262 func (r *response) Notify(target NotifyTarget) { 263 // This response is either for a claim (in which case claimer will 264 // be set) or a set-time (so it will have zero or more expiries). 265 if r.claimer != "" { 266 target.Claimed(r.claimed, r.claimer) 267 } 268 for _, expiredKey := range r.expired { 269 target.Expired(expiredKey) 270 } 271 } 272 273 func invalidResponse() *response { 274 return &response{err: lease.ErrInvalid} 275 } 276 277 // Apply is part of raft.FSM. 278 func (f *FSM) Apply(log *raft.Log) interface{} { 279 var command Command 280 err := yaml.Unmarshal(log.Data, &command) 281 if err != nil { 282 return &response{err: errors.Trace(err)} 283 } 284 if err := command.Validate(); err != nil { 285 return &response{err: errors.Trace(err)} 286 } 287 288 f.mu.Lock() 289 defer f.mu.Unlock() 290 291 switch command.Operation { 292 case OperationClaim: 293 return f.claim(command.LeaseKey(), command.Holder, command.Duration) 294 case OperationExtend: 295 return f.extend(command.LeaseKey(), command.Holder, command.Duration) 296 case OperationPin: 297 return f.pin(command.LeaseKey(), command.PinEntity) 298 case OperationUnpin: 299 return f.unpin(command.LeaseKey(), command.PinEntity) 300 case OperationSetTime: 301 return f.setTime(command.OldTime, command.NewTime) 302 default: 303 return &response{err: errors.NotValidf("operation %q", command.Operation)} 304 } 305 } 306 307 // Snapshot is part of raft.FSM. 308 func (f *FSM) Snapshot() (raft.FSMSnapshot, error) { 309 f.mu.Lock() 310 311 entries := make(map[SnapshotKey]SnapshotEntry, len(f.entries)) 312 for key, entry := range f.entries { 313 entries[SnapshotKey{ 314 Namespace: key.Namespace, 315 ModelUUID: key.ModelUUID, 316 Lease: key.Lease, 317 }] = SnapshotEntry{ 318 Holder: entry.holder, 319 Start: entry.start, 320 Duration: entry.duration, 321 } 322 } 323 324 pinned := make(map[SnapshotKey][]string) 325 for key, entities := range f.pinned { 326 if entities.IsEmpty() { 327 continue 328 } 329 pinned[SnapshotKey{ 330 Namespace: key.Namespace, 331 ModelUUID: key.ModelUUID, 332 Lease: key.Lease, 333 }] = entities.SortedValues() 334 } 335 336 f.mu.Unlock() 337 338 return &Snapshot{ 339 Version: SnapshotVersion, 340 Entries: entries, 341 Pinned: pinned, 342 GlobalTime: f.globalTime, 343 }, nil 344 } 345 346 // Restore is part of raft.FSM. 347 func (f *FSM) Restore(reader io.ReadCloser) error { 348 defer reader.Close() 349 350 var snapshot Snapshot 351 decoder := yaml.NewDecoder(reader) 352 if err := decoder.Decode(&snapshot); err != nil { 353 return errors.Trace(err) 354 } 355 if snapshot.Version != SnapshotVersion { 356 return errors.NotValidf("snapshot version %d", snapshot.Version) 357 } 358 if snapshot.Entries == nil { 359 return errors.NotValidf("nil entries") 360 } 361 362 newEntries := make(map[lease.Key]*entry, len(snapshot.Entries)) 363 for key, ssEntry := range snapshot.Entries { 364 newEntries[lease.Key{ 365 Namespace: key.Namespace, 366 ModelUUID: key.ModelUUID, 367 Lease: key.Lease, 368 }] = &entry{ 369 holder: ssEntry.Holder, 370 start: ssEntry.Start, 371 duration: ssEntry.Duration, 372 } 373 } 374 375 newPinned := make(map[lease.Key]set.Strings, len(snapshot.Pinned)) 376 for key, entities := range snapshot.Pinned { 377 newPinned[lease.Key{ 378 Namespace: key.Namespace, 379 ModelUUID: key.ModelUUID, 380 Lease: key.Lease, 381 }] = set.NewStrings(entities...) 382 } 383 384 f.mu.Lock() 385 f.globalTime = snapshot.GlobalTime 386 f.entries = newEntries 387 f.pinned = newPinned 388 f.mu.Unlock() 389 390 return nil 391 } 392 393 // Snapshot defines the format of the FSM snapshot. 394 type Snapshot struct { 395 Version int `yaml:"version"` 396 Entries map[SnapshotKey]SnapshotEntry `yaml:"entries"` 397 Pinned map[SnapshotKey][]string `yaml:"pinned"` 398 GlobalTime time.Time `yaml:"global-time"` 399 } 400 401 // Persist is part of raft.FSMSnapshot. 402 func (s *Snapshot) Persist(sink raft.SnapshotSink) (err error) { 403 defer func() { 404 if err != nil { 405 sink.Cancel() 406 } 407 }() 408 409 encoder := yaml.NewEncoder(sink) 410 if err := encoder.Encode(s); err != nil { 411 return errors.Trace(err) 412 } 413 if err := encoder.Close(); err != nil { 414 return errors.Trace(err) 415 } 416 return sink.Close() 417 } 418 419 // Release is part of raft.FSMSnapshot. 420 func (s *Snapshot) Release() {} 421 422 // SnapshotKey defines the format of a lease key in a snapshot. 423 type SnapshotKey struct { 424 Namespace string `yaml:"namespace"` 425 ModelUUID string `yaml:"model-uuid"` 426 Lease string `yaml:"lease"` 427 } 428 429 // SnapshotEntry defines the format of a lease entry in a snapshot. 430 type SnapshotEntry struct { 431 Holder string `yaml:"holder"` 432 Start time.Time `yaml:"start"` 433 Duration time.Duration `yaml:"duration"` 434 } 435 436 // Command captures the details of an operation to be run on the FSM. 437 type Command struct { 438 // Version of the command format, in case it changes and we need 439 // to handle multiple formats. 440 Version int `yaml:"version"` 441 442 // Operation is one of claim, extend, expire or setTime. 443 Operation string `yaml:"operation"` 444 445 // Namespace is the kind of lease. 446 Namespace string `yaml:"namespace,omitempty"` 447 448 // ModelUUID identifies the model the lease belongs to. 449 ModelUUID string `yaml:"model-uuid,omitempty"` 450 451 // Lease is the name of the lease the command affects. 452 Lease string `yaml:"lease,omitempty"` 453 454 // Holder is the name of the party claiming or extending the 455 // lease. 456 Holder string `yaml:"holder,omitempty"` 457 458 // Duration is how long the lease should last. 459 Duration time.Duration `yaml:"duration,omitempty"` 460 461 // OldTime is the previous time for time updates (to avoid 462 // applying stale ones). 463 OldTime time.Time `yaml:"old-time,omitempty"` 464 465 // NewTime is the time to store as the global time. 466 NewTime time.Time `yaml:"new-time,omitempty"` 467 468 // PinEntity is a tag representing an entity concerned 469 // with a pin or unpin operation. 470 PinEntity string `yaml:"pin-entity,omitempty"` 471 } 472 473 // Validate checks that the command describes a valid state change. 474 func (c *Command) Validate() error { 475 // For now there's only version 1. 476 if c.Version != 1 { 477 return errors.NotValidf("version %d", c.Version) 478 } 479 switch c.Operation { 480 case OperationClaim, OperationExtend: 481 if err := c.validateLeaseKey(); err != nil { 482 return err 483 } 484 if err := c.validateNoTime(); err != nil { 485 return err 486 } 487 if c.Holder == "" { 488 return errors.NotValidf("%s with empty holder", c.Operation) 489 } 490 if c.Duration == 0 { 491 return errors.NotValidf("%s with zero duration", c.Operation) 492 } 493 if c.PinEntity != "" { 494 return errors.NotValidf("%s with pin entity", c.Operation) 495 } 496 case OperationPin, OperationUnpin: 497 if err := c.validateLeaseKey(); err != nil { 498 return err 499 } 500 if err := c.validateNoTime(); err != nil { 501 return err 502 } 503 if c.Duration != 0 { 504 return errors.NotValidf("%s with duration", c.Operation) 505 } 506 if c.PinEntity == "" { 507 return errors.NotValidf("%s with empty pin entity", c.Operation) 508 } 509 case OperationSetTime: 510 // An old time of 0 is valid when starting up. 511 var zeroTime time.Time 512 if c.NewTime == zeroTime { 513 return errors.NotValidf("setTime with zero new time") 514 } 515 if c.Holder != "" { 516 return errors.NotValidf("setTime with holder") 517 } 518 if c.Duration != 0 { 519 return errors.NotValidf("setTime with duration") 520 } 521 if c.Namespace != "" { 522 return errors.NotValidf("setTime with namespace") 523 } 524 if c.ModelUUID != "" { 525 return errors.NotValidf("setTime with model UUID") 526 } 527 if c.Lease != "" { 528 if c.Holder == "" { 529 return errors.NotValidf("%s with empty holder", c.Operation) 530 } 531 return errors.NotValidf("setTime with lease") 532 } 533 if c.PinEntity != "" { 534 return errors.NotValidf("setTime with pin entity") 535 } 536 default: 537 return errors.NotValidf("operation %q", c.Operation) 538 } 539 return nil 540 } 541 542 func (c *Command) validateLeaseKey() error { 543 if c.Namespace == "" { 544 return errors.NotValidf("%s with empty namespace", c.Operation) 545 } 546 if c.ModelUUID == "" { 547 return errors.NotValidf("%s with empty model UUID", c.Operation) 548 } 549 if c.Lease == "" { 550 return errors.NotValidf("%s with empty lease", c.Operation) 551 } 552 return nil 553 } 554 555 func (c *Command) validateNoTime() error { 556 var zeroTime time.Time 557 if c.OldTime != zeroTime { 558 return errors.NotValidf("%s with old time", c.Operation) 559 } 560 if c.NewTime != zeroTime { 561 return errors.NotValidf("%s with new time", c.Operation) 562 } 563 return nil 564 } 565 566 // LeaseKey makes a lease key from the fields in the command. 567 func (c *Command) LeaseKey() lease.Key { 568 return lease.Key{ 569 Namespace: c.Namespace, 570 ModelUUID: c.ModelUUID, 571 Lease: c.Lease, 572 } 573 } 574 575 // Marshal converts this command to a byte slice. 576 func (c *Command) Marshal() ([]byte, error) { 577 return yaml.Marshal(c) 578 } 579 580 // UnmarshalCommand converts a marshalled command []byte into a 581 // command. 582 func UnmarshalCommand(data []byte) (*Command, error) { 583 var result Command 584 err := yaml.Unmarshal(data, &result) 585 return &result, err 586 }