github.com/dkerwin/nomad@v0.3.3-0.20160525181927-74554135514b/nomad/fsm.go (about) 1 package nomad 2 3 import ( 4 "fmt" 5 "io" 6 "log" 7 "time" 8 9 "github.com/armon/go-metrics" 10 "github.com/hashicorp/nomad/nomad/state" 11 "github.com/hashicorp/nomad/nomad/structs" 12 "github.com/hashicorp/raft" 13 "github.com/ugorji/go/codec" 14 ) 15 16 const ( 17 // timeTableGranularity is the granularity of index to time tracking 18 timeTableGranularity = 5 * time.Minute 19 20 // timeTableLimit is the maximum limit of our tracking 21 timeTableLimit = 72 * time.Hour 22 ) 23 24 // SnapshotType is prefixed to a record in the FSM snapshot 25 // so that we can determine the type for restore 26 type SnapshotType byte 27 28 const ( 29 NodeSnapshot SnapshotType = iota 30 JobSnapshot 31 IndexSnapshot 32 EvalSnapshot 33 AllocSnapshot 34 TimeTableSnapshot 35 PeriodicLaunchSnapshot 36 ) 37 38 // nomadFSM implements a finite state machine that is used 39 // along with Raft to provide strong consistency. We implement 40 // this outside the Server to avoid exposing this outside the package. 41 type nomadFSM struct { 42 evalBroker *EvalBroker 43 blockedEvals *BlockedEvals 44 periodicDispatcher *PeriodicDispatch 45 logOutput io.Writer 46 logger *log.Logger 47 state *state.StateStore 48 timetable *TimeTable 49 } 50 51 // nomadSnapshot is used to provide a snapshot of the current 52 // state in a way that can be accessed concurrently with operations 53 // that may modify the live state. 54 type nomadSnapshot struct { 55 snap *state.StateSnapshot 56 timetable *TimeTable 57 } 58 59 // snapshotHeader is the first entry in our snapshot 60 type snapshotHeader struct { 61 } 62 63 // NewFSMPath is used to construct a new FSM with a blank state 64 func NewFSM(evalBroker *EvalBroker, periodic *PeriodicDispatch, 65 blocked *BlockedEvals, logOutput io.Writer) (*nomadFSM, error) { 66 // Create a state store 67 state, err := state.NewStateStore(logOutput) 68 if err != nil { 69 return nil, err 70 } 71 72 fsm := &nomadFSM{ 73 evalBroker: evalBroker, 74 periodicDispatcher: periodic, 75 blockedEvals: blocked, 76 logOutput: logOutput, 77 logger: log.New(logOutput, "", log.LstdFlags), 78 state: state, 79 timetable: NewTimeTable(timeTableGranularity, timeTableLimit), 80 } 81 return fsm, nil 82 } 83 84 // Close is used to cleanup resources associated with the FSM 85 func (n *nomadFSM) Close() error { 86 return nil 87 } 88 89 // State is used to return a handle to the current state 90 func (n *nomadFSM) State() *state.StateStore { 91 return n.state 92 } 93 94 // TimeTable returns the time table of transactions 95 func (n *nomadFSM) TimeTable() *TimeTable { 96 return n.timetable 97 } 98 99 func (n *nomadFSM) Apply(log *raft.Log) interface{} { 100 buf := log.Data 101 msgType := structs.MessageType(buf[0]) 102 103 // Witness this write 104 n.timetable.Witness(log.Index, time.Now().UTC()) 105 106 // Check if this message type should be ignored when unknown. This is 107 // used so that new commands can be added with developer control if older 108 // versions can safely ignore the command, or if they should crash. 109 ignoreUnknown := false 110 if msgType&structs.IgnoreUnknownTypeFlag == structs.IgnoreUnknownTypeFlag { 111 msgType &= ^structs.IgnoreUnknownTypeFlag 112 ignoreUnknown = true 113 } 114 115 switch msgType { 116 case structs.NodeRegisterRequestType: 117 return n.applyUpsertNode(buf[1:], log.Index) 118 case structs.NodeDeregisterRequestType: 119 return n.applyDeregisterNode(buf[1:], log.Index) 120 case structs.NodeUpdateStatusRequestType: 121 return n.applyStatusUpdate(buf[1:], log.Index) 122 case structs.NodeUpdateDrainRequestType: 123 return n.applyDrainUpdate(buf[1:], log.Index) 124 case structs.JobRegisterRequestType: 125 return n.applyUpsertJob(buf[1:], log.Index) 126 case structs.JobDeregisterRequestType: 127 return n.applyDeregisterJob(buf[1:], log.Index) 128 case structs.EvalUpdateRequestType: 129 return n.applyUpdateEval(buf[1:], log.Index) 130 case structs.EvalDeleteRequestType: 131 return n.applyDeleteEval(buf[1:], log.Index) 132 case structs.AllocUpdateRequestType: 133 return n.applyAllocUpdate(buf[1:], log.Index) 134 case structs.AllocClientUpdateRequestType: 135 return n.applyAllocClientUpdate(buf[1:], log.Index) 136 default: 137 if ignoreUnknown { 138 n.logger.Printf("[WARN] nomad.fsm: ignoring unknown message type (%d), upgrade to newer version", msgType) 139 return nil 140 } else { 141 panic(fmt.Errorf("failed to apply request: %#v", buf)) 142 } 143 } 144 } 145 146 func (n *nomadFSM) applyUpsertNode(buf []byte, index uint64) interface{} { 147 defer metrics.MeasureSince([]string{"nomad", "fsm", "register_node"}, time.Now()) 148 var req structs.NodeRegisterRequest 149 if err := structs.Decode(buf, &req); err != nil { 150 panic(fmt.Errorf("failed to decode request: %v", err)) 151 } 152 153 if err := n.state.UpsertNode(index, req.Node); err != nil { 154 n.logger.Printf("[ERR] nomad.fsm: UpsertNode failed: %v", err) 155 return err 156 } 157 158 // Unblock evals for the nodes computed node class if it is in a ready 159 // state. 160 if req.Node.Status == structs.NodeStatusReady { 161 n.blockedEvals.Unblock(req.Node.ComputedClass, index) 162 } 163 164 return nil 165 } 166 167 func (n *nomadFSM) applyDeregisterNode(buf []byte, index uint64) interface{} { 168 defer metrics.MeasureSince([]string{"nomad", "fsm", "deregister_node"}, time.Now()) 169 var req structs.NodeDeregisterRequest 170 if err := structs.Decode(buf, &req); err != nil { 171 panic(fmt.Errorf("failed to decode request: %v", err)) 172 } 173 174 if err := n.state.DeleteNode(index, req.NodeID); err != nil { 175 n.logger.Printf("[ERR] nomad.fsm: DeleteNode failed: %v", err) 176 return err 177 } 178 return nil 179 } 180 181 func (n *nomadFSM) applyStatusUpdate(buf []byte, index uint64) interface{} { 182 defer metrics.MeasureSince([]string{"nomad", "fsm", "node_status_update"}, time.Now()) 183 var req structs.NodeUpdateStatusRequest 184 if err := structs.Decode(buf, &req); err != nil { 185 panic(fmt.Errorf("failed to decode request: %v", err)) 186 } 187 188 if err := n.state.UpdateNodeStatus(index, req.NodeID, req.Status); err != nil { 189 n.logger.Printf("[ERR] nomad.fsm: UpdateNodeStatus failed: %v", err) 190 return err 191 } 192 193 // Unblock evals for the nodes computed node class if it is in a ready 194 // state. 195 if req.Status == structs.NodeStatusReady { 196 node, err := n.state.NodeByID(req.NodeID) 197 if err != nil { 198 n.logger.Printf("[ERR] nomad.fsm: looking up node %q failed: %v", req.NodeID, err) 199 return err 200 201 } 202 n.blockedEvals.Unblock(node.ComputedClass, index) 203 } 204 205 return nil 206 } 207 208 func (n *nomadFSM) applyDrainUpdate(buf []byte, index uint64) interface{} { 209 defer metrics.MeasureSince([]string{"nomad", "fsm", "node_drain_update"}, time.Now()) 210 var req structs.NodeUpdateDrainRequest 211 if err := structs.Decode(buf, &req); err != nil { 212 panic(fmt.Errorf("failed to decode request: %v", err)) 213 } 214 215 if err := n.state.UpdateNodeDrain(index, req.NodeID, req.Drain); err != nil { 216 n.logger.Printf("[ERR] nomad.fsm: UpdateNodeDrain failed: %v", err) 217 return err 218 } 219 return nil 220 } 221 222 func (n *nomadFSM) applyUpsertJob(buf []byte, index uint64) interface{} { 223 defer metrics.MeasureSince([]string{"nomad", "fsm", "register_job"}, time.Now()) 224 var req structs.JobRegisterRequest 225 if err := structs.Decode(buf, &req); err != nil { 226 panic(fmt.Errorf("failed to decode request: %v", err)) 227 } 228 229 if err := n.state.UpsertJob(index, req.Job); err != nil { 230 n.logger.Printf("[ERR] nomad.fsm: UpsertJob failed: %v", err) 231 return err 232 } 233 234 // We always add the job to the periodic dispatcher because there is the 235 // possibility that the periodic spec was removed and then we should stop 236 // tracking it. 237 if err := n.periodicDispatcher.Add(req.Job); err != nil { 238 n.logger.Printf("[ERR] nomad.fsm: periodicDispatcher.Add failed: %v", err) 239 return err 240 } 241 242 // If it is periodic, record the time it was inserted. This is necessary for 243 // recovering during leader election. It is possible that from the time it 244 // is added to when it was suppose to launch, leader election occurs and the 245 // job was not launched. In this case, we use the insertion time to 246 // determine if a launch was missed. 247 if req.Job.IsPeriodic() { 248 prevLaunch, err := n.state.PeriodicLaunchByID(req.Job.ID) 249 if err != nil { 250 n.logger.Printf("[ERR] nomad.fsm: PeriodicLaunchByID failed: %v", err) 251 return err 252 } 253 254 // Record the insertion time as a launch. We overload the launch table 255 // such that the first entry is the insertion time. 256 if prevLaunch == nil { 257 launch := &structs.PeriodicLaunch{ID: req.Job.ID, Launch: time.Now()} 258 if err := n.state.UpsertPeriodicLaunch(index, launch); err != nil { 259 n.logger.Printf("[ERR] nomad.fsm: UpsertPeriodicLaunch failed: %v", err) 260 return err 261 } 262 } 263 } 264 265 // Check if the parent job is periodic and mark the launch time. 266 parentID := req.Job.ParentID 267 if parentID != "" { 268 parent, err := n.state.JobByID(parentID) 269 if err != nil { 270 n.logger.Printf("[ERR] nomad.fsm: JobByID(%v) lookup for parent failed: %v", parentID, err) 271 return err 272 } else if parent == nil { 273 // The parent has been deregistered. 274 return nil 275 } 276 277 if parent.IsPeriodic() { 278 t, err := n.periodicDispatcher.LaunchTime(req.Job.ID) 279 if err != nil { 280 n.logger.Printf("[ERR] nomad.fsm: LaunchTime(%v) failed: %v", req.Job.ID, err) 281 return err 282 } 283 284 launch := &structs.PeriodicLaunch{ID: parentID, Launch: t} 285 if err := n.state.UpsertPeriodicLaunch(index, launch); err != nil { 286 n.logger.Printf("[ERR] nomad.fsm: UpsertPeriodicLaunch failed: %v", err) 287 return err 288 } 289 } 290 } 291 292 return nil 293 } 294 295 func (n *nomadFSM) applyDeregisterJob(buf []byte, index uint64) interface{} { 296 defer metrics.MeasureSince([]string{"nomad", "fsm", "deregister_job"}, time.Now()) 297 var req structs.JobDeregisterRequest 298 if err := structs.Decode(buf, &req); err != nil { 299 panic(fmt.Errorf("failed to decode request: %v", err)) 300 } 301 302 if err := n.state.DeleteJob(index, req.JobID); err != nil { 303 n.logger.Printf("[ERR] nomad.fsm: DeleteJob failed: %v", err) 304 return err 305 } 306 307 if err := n.periodicDispatcher.Remove(req.JobID); err != nil { 308 n.logger.Printf("[ERR] nomad.fsm: periodicDispatcher.Remove failed: %v", err) 309 return err 310 } 311 312 // We always delete from the periodic launch table because it is possible that 313 // the job was updated to be non-perioidic, thus checking if it is periodic 314 // doesn't ensure we clean it up properly. 315 n.state.DeletePeriodicLaunch(index, req.JobID) 316 317 return nil 318 } 319 320 func (n *nomadFSM) applyUpdateEval(buf []byte, index uint64) interface{} { 321 defer metrics.MeasureSince([]string{"nomad", "fsm", "update_eval"}, time.Now()) 322 var req structs.EvalUpdateRequest 323 if err := structs.Decode(buf, &req); err != nil { 324 panic(fmt.Errorf("failed to decode request: %v", err)) 325 } 326 327 if err := n.state.UpsertEvals(index, req.Evals); err != nil { 328 n.logger.Printf("[ERR] nomad.fsm: UpsertEvals failed: %v", err) 329 return err 330 } 331 332 for _, eval := range req.Evals { 333 if eval.ShouldEnqueue() { 334 n.evalBroker.Enqueue(eval) 335 } else if eval.ShouldBlock() { 336 n.blockedEvals.Block(eval) 337 } 338 } 339 return nil 340 } 341 342 func (n *nomadFSM) applyDeleteEval(buf []byte, index uint64) interface{} { 343 defer metrics.MeasureSince([]string{"nomad", "fsm", "delete_eval"}, time.Now()) 344 var req structs.EvalDeleteRequest 345 if err := structs.Decode(buf, &req); err != nil { 346 panic(fmt.Errorf("failed to decode request: %v", err)) 347 } 348 349 if err := n.state.DeleteEval(index, req.Evals, req.Allocs); err != nil { 350 n.logger.Printf("[ERR] nomad.fsm: DeleteEval failed: %v", err) 351 return err 352 } 353 return nil 354 } 355 356 func (n *nomadFSM) applyAllocUpdate(buf []byte, index uint64) interface{} { 357 defer metrics.MeasureSince([]string{"nomad", "fsm", "alloc_update"}, time.Now()) 358 var req structs.AllocUpdateRequest 359 if err := structs.Decode(buf, &req); err != nil { 360 panic(fmt.Errorf("failed to decode request: %v", err)) 361 } 362 363 // Attach the job to all the allocations. It is pulled out in the 364 // payload to avoid the redundancy of encoding, but should be denormalized 365 // prior to being inserted into MemDB. 366 if j := req.Job; j != nil { 367 for _, alloc := range req.Alloc { 368 if alloc.Job == nil { 369 alloc.Job = j 370 } 371 } 372 } 373 374 // Calculate the total resources of allocations. It is pulled out in the 375 // payload to avoid encoding something that can be computed, but should be 376 // denormalized prior to being inserted into MemDB. 377 for _, alloc := range req.Alloc { 378 if alloc.Resources != nil { 379 continue 380 } 381 382 alloc.Resources = new(structs.Resources) 383 for _, task := range alloc.TaskResources { 384 alloc.Resources.Add(task) 385 } 386 } 387 388 if err := n.state.UpsertAllocs(index, req.Alloc); err != nil { 389 n.logger.Printf("[ERR] nomad.fsm: UpsertAllocs failed: %v", err) 390 return err 391 } 392 return nil 393 } 394 395 func (n *nomadFSM) applyAllocClientUpdate(buf []byte, index uint64) interface{} { 396 defer metrics.MeasureSince([]string{"nomad", "fsm", "alloc_client_update"}, time.Now()) 397 var req structs.AllocUpdateRequest 398 if err := structs.Decode(buf, &req); err != nil { 399 panic(fmt.Errorf("failed to decode request: %v", err)) 400 } 401 if len(req.Alloc) == 0 { 402 return nil 403 } 404 405 // Update all the client allocations 406 if err := n.state.UpdateAllocsFromClient(index, req.Alloc); err != nil { 407 n.logger.Printf("[ERR] nomad.fsm: UpdateAllocFromClient failed: %v", err) 408 return err 409 } 410 411 // Unblock evals for the nodes computed node class if the client has 412 // finished running an allocation. 413 for _, alloc := range req.Alloc { 414 if alloc.ClientStatus == structs.AllocClientStatusComplete || 415 alloc.ClientStatus == structs.AllocClientStatusFailed { 416 nodeID := alloc.NodeID 417 node, err := n.state.NodeByID(nodeID) 418 if err != nil || node == nil { 419 n.logger.Printf("[ERR] nomad.fsm: looking up node %q failed: %v", nodeID, err) 420 return err 421 422 } 423 n.blockedEvals.Unblock(node.ComputedClass, index) 424 } 425 } 426 427 return nil 428 } 429 430 func (n *nomadFSM) Snapshot() (raft.FSMSnapshot, error) { 431 // Create a new snapshot 432 snap, err := n.state.Snapshot() 433 if err != nil { 434 return nil, err 435 } 436 437 ns := &nomadSnapshot{ 438 snap: snap, 439 timetable: n.timetable, 440 } 441 return ns, nil 442 } 443 444 func (n *nomadFSM) Restore(old io.ReadCloser) error { 445 defer old.Close() 446 447 // Create a new state store 448 newState, err := state.NewStateStore(n.logOutput) 449 if err != nil { 450 return err 451 } 452 n.state = newState 453 454 // Start the state restore 455 restore, err := newState.Restore() 456 if err != nil { 457 return err 458 } 459 defer restore.Abort() 460 461 // Create a decoder 462 dec := codec.NewDecoder(old, structs.MsgpackHandle) 463 464 // Read in the header 465 var header snapshotHeader 466 if err := dec.Decode(&header); err != nil { 467 return err 468 } 469 470 // Populate the new state 471 msgType := make([]byte, 1) 472 for { 473 // Read the message type 474 _, err := old.Read(msgType) 475 if err == io.EOF { 476 break 477 } else if err != nil { 478 return err 479 } 480 481 // Decode 482 switch SnapshotType(msgType[0]) { 483 case TimeTableSnapshot: 484 if err := n.timetable.Deserialize(dec); err != nil { 485 return fmt.Errorf("time table deserialize failed: %v", err) 486 } 487 488 case NodeSnapshot: 489 node := new(structs.Node) 490 if err := dec.Decode(node); err != nil { 491 return err 492 } 493 if err := restore.NodeRestore(node); err != nil { 494 return err 495 } 496 497 case JobSnapshot: 498 job := new(structs.Job) 499 if err := dec.Decode(job); err != nil { 500 return err 501 } 502 if err := restore.JobRestore(job); err != nil { 503 return err 504 } 505 506 case EvalSnapshot: 507 eval := new(structs.Evaluation) 508 if err := dec.Decode(eval); err != nil { 509 return err 510 } 511 if err := restore.EvalRestore(eval); err != nil { 512 return err 513 } 514 515 case AllocSnapshot: 516 alloc := new(structs.Allocation) 517 if err := dec.Decode(alloc); err != nil { 518 return err 519 } 520 if err := restore.AllocRestore(alloc); err != nil { 521 return err 522 } 523 524 case IndexSnapshot: 525 idx := new(state.IndexEntry) 526 if err := dec.Decode(idx); err != nil { 527 return err 528 } 529 if err := restore.IndexRestore(idx); err != nil { 530 return err 531 } 532 533 case PeriodicLaunchSnapshot: 534 launch := new(structs.PeriodicLaunch) 535 if err := dec.Decode(launch); err != nil { 536 return err 537 } 538 if err := restore.PeriodicLaunchRestore(launch); err != nil { 539 return err 540 } 541 542 default: 543 return fmt.Errorf("Unrecognized snapshot type: %v", msgType) 544 } 545 } 546 547 // Commit the state restore 548 restore.Commit() 549 return nil 550 } 551 552 func (s *nomadSnapshot) Persist(sink raft.SnapshotSink) error { 553 defer metrics.MeasureSince([]string{"nomad", "fsm", "persist"}, time.Now()) 554 // Register the nodes 555 encoder := codec.NewEncoder(sink, structs.MsgpackHandle) 556 557 // Write the header 558 header := snapshotHeader{} 559 if err := encoder.Encode(&header); err != nil { 560 sink.Cancel() 561 return err 562 } 563 564 // Write the time table 565 sink.Write([]byte{byte(TimeTableSnapshot)}) 566 if err := s.timetable.Serialize(encoder); err != nil { 567 sink.Cancel() 568 return err 569 } 570 571 // Write all the data out 572 if err := s.persistIndexes(sink, encoder); err != nil { 573 sink.Cancel() 574 return err 575 } 576 if err := s.persistNodes(sink, encoder); err != nil { 577 sink.Cancel() 578 return err 579 } 580 if err := s.persistJobs(sink, encoder); err != nil { 581 sink.Cancel() 582 return err 583 } 584 if err := s.persistEvals(sink, encoder); err != nil { 585 sink.Cancel() 586 return err 587 } 588 if err := s.persistAllocs(sink, encoder); err != nil { 589 sink.Cancel() 590 return err 591 } 592 if err := s.persistPeriodicLaunches(sink, encoder); err != nil { 593 sink.Cancel() 594 return err 595 } 596 return nil 597 } 598 599 func (s *nomadSnapshot) persistIndexes(sink raft.SnapshotSink, 600 encoder *codec.Encoder) error { 601 // Get all the indexes 602 iter, err := s.snap.Indexes() 603 if err != nil { 604 return err 605 } 606 607 for { 608 // Get the next item 609 raw := iter.Next() 610 if raw == nil { 611 break 612 } 613 614 // Prepare the request struct 615 idx := raw.(*state.IndexEntry) 616 617 // Write out a node registration 618 sink.Write([]byte{byte(IndexSnapshot)}) 619 if err := encoder.Encode(idx); err != nil { 620 return err 621 } 622 } 623 return nil 624 } 625 626 func (s *nomadSnapshot) persistNodes(sink raft.SnapshotSink, 627 encoder *codec.Encoder) error { 628 // Get all the nodes 629 nodes, err := s.snap.Nodes() 630 if err != nil { 631 return err 632 } 633 634 for { 635 // Get the next item 636 raw := nodes.Next() 637 if raw == nil { 638 break 639 } 640 641 // Prepare the request struct 642 node := raw.(*structs.Node) 643 644 // Write out a node registration 645 sink.Write([]byte{byte(NodeSnapshot)}) 646 if err := encoder.Encode(node); err != nil { 647 return err 648 } 649 } 650 return nil 651 } 652 653 func (s *nomadSnapshot) persistJobs(sink raft.SnapshotSink, 654 encoder *codec.Encoder) error { 655 // Get all the jobs 656 jobs, err := s.snap.Jobs() 657 if err != nil { 658 return err 659 } 660 661 for { 662 // Get the next item 663 raw := jobs.Next() 664 if raw == nil { 665 break 666 } 667 668 // Prepare the request struct 669 job := raw.(*structs.Job) 670 671 // Write out a job registration 672 sink.Write([]byte{byte(JobSnapshot)}) 673 if err := encoder.Encode(job); err != nil { 674 return err 675 } 676 } 677 return nil 678 } 679 680 func (s *nomadSnapshot) persistEvals(sink raft.SnapshotSink, 681 encoder *codec.Encoder) error { 682 // Get all the evaluations 683 evals, err := s.snap.Evals() 684 if err != nil { 685 return err 686 } 687 688 for { 689 // Get the next item 690 raw := evals.Next() 691 if raw == nil { 692 break 693 } 694 695 // Prepare the request struct 696 eval := raw.(*structs.Evaluation) 697 698 // Write out the evaluation 699 sink.Write([]byte{byte(EvalSnapshot)}) 700 if err := encoder.Encode(eval); err != nil { 701 return err 702 } 703 } 704 return nil 705 } 706 707 func (s *nomadSnapshot) persistAllocs(sink raft.SnapshotSink, 708 encoder *codec.Encoder) error { 709 // Get all the allocations 710 allocs, err := s.snap.Allocs() 711 if err != nil { 712 return err 713 } 714 715 for { 716 // Get the next item 717 raw := allocs.Next() 718 if raw == nil { 719 break 720 } 721 722 // Prepare the request struct 723 alloc := raw.(*structs.Allocation) 724 725 // Write out the evaluation 726 sink.Write([]byte{byte(AllocSnapshot)}) 727 if err := encoder.Encode(alloc); err != nil { 728 return err 729 } 730 } 731 return nil 732 } 733 734 func (s *nomadSnapshot) persistPeriodicLaunches(sink raft.SnapshotSink, 735 encoder *codec.Encoder) error { 736 // Get all the jobs 737 launches, err := s.snap.PeriodicLaunches() 738 if err != nil { 739 return err 740 } 741 742 for { 743 // Get the next item 744 raw := launches.Next() 745 if raw == nil { 746 break 747 } 748 749 // Prepare the request struct 750 launch := raw.(*structs.PeriodicLaunch) 751 752 // Write out a job registration 753 sink.Write([]byte{byte(PeriodicLaunchSnapshot)}) 754 if err := encoder.Encode(launch); err != nil { 755 return err 756 } 757 } 758 return nil 759 } 760 761 // Release is a no-op, as we just need to GC the pointer 762 // to the state store snapshot. There is nothing to explicitly 763 // cleanup. 764 func (s *nomadSnapshot) Release() {}