github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/worker/uniter/runner/context.go (about) 1 // Copyright 2012-2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package runner 5 6 import ( 7 "fmt" 8 "os" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/juju/errors" 14 "github.com/juju/loggo" 15 "github.com/juju/names" 16 "github.com/juju/utils/proxy" 17 "gopkg.in/juju/charm.v5" 18 19 "github.com/juju/juju/api/uniter" 20 "github.com/juju/juju/apiserver/params" 21 "github.com/juju/juju/network" 22 "github.com/juju/juju/worker/uniter/metrics" 23 "github.com/juju/juju/worker/uniter/runner/jujuc" 24 ) 25 26 var logger = loggo.GetLogger("juju.worker.uniter.context") 27 var mutex = sync.Mutex{} 28 var ErrIsNotLeader = errors.Errorf("this unit is not the leader") 29 30 // meterStatus describes the unit's meter status. 31 type meterStatus struct { 32 code string 33 info string 34 } 35 36 // MetricsRecorder is used to store metrics supplied by the add-metric command. 37 type MetricsRecorder interface { 38 AddMetric(key, value string, created time.Time) error 39 IsDeclaredMetric(key string) bool 40 Close() error 41 } 42 43 // metricsReader is used to read metrics batches stored by the metrics recorder 44 // and remove metrics batches that have been marked as succesfully sent. 45 type metricsReader interface { 46 Open() ([]metrics.MetricBatch, error) 47 Remove(uuid string) error 48 Close() error 49 } 50 51 // HookContext is the implementation of jujuc.Context. 52 type HookContext struct { 53 unit *uniter.Unit 54 55 // state is the handle to the uniter State so that HookContext can make 56 // API calls on the stateservice. 57 // NOTE: We would like to be rid of the fake-remote-Unit and switch 58 // over fully to API calls on State. This adds that ability, but we're 59 // not fully there yet. 60 state *uniter.State 61 62 // LeadershipContext supplies several jujuc.Context methods. 63 LeadershipContext 64 65 // privateAddress is the cached value of the unit's private 66 // address. 67 privateAddress string 68 69 // publicAddress is the cached value of the unit's public 70 // address. 71 publicAddress string 72 73 // availabilityzone is the cached value of the unit's availability zone name. 74 availabilityzone string 75 76 // configSettings holds the service configuration. 77 configSettings charm.Settings 78 79 // id identifies the context. 80 id string 81 82 // actionData contains the values relevant to the run of an Action: 83 // its tag, its parameters, and its results. 84 actionData *ActionData 85 86 // uuid is the universally unique identifier of the environment. 87 uuid string 88 89 // envName is the human friendly name of the environment. 90 envName string 91 92 // unitName is the human friendly name of the local unit. 93 unitName string 94 95 // status is the status of the local unit. 96 status *jujuc.StatusInfo 97 98 // relationId identifies the relation for which a relation hook is 99 // executing. If it is -1, the context is not running a relation hook; 100 // otherwise, its value must be a valid key into the relations map. 101 relationId int 102 103 // remoteUnitName identifies the changing unit of the executing relation 104 // hook. It will be empty if the context is not running a relation hook, 105 // or if it is running a relation-broken hook. 106 remoteUnitName string 107 108 // relations contains the context for every relation the unit is a member 109 // of, keyed on relation id. 110 relations map[int]*ContextRelation 111 112 // apiAddrs contains the API server addresses. 113 apiAddrs []string 114 115 // proxySettings are the current proxy settings that the uniter knows about. 116 proxySettings proxy.Settings 117 118 // metricsRecorder is used to write metrics batches to a storage (usually a file). 119 metricsRecorder MetricsRecorder 120 121 // definedMetrics specifies the metrics the charm has defined in its metrics.yaml file. 122 definedMetrics *charm.Metrics 123 124 // meterStatus is the status of the unit's metering. 125 meterStatus *meterStatus 126 127 // pendingPorts contains a list of port ranges to be opened or 128 // closed when the current hook is committed. 129 pendingPorts map[PortRange]PortRangeInfo 130 131 // machinePorts contains cached information about all opened port 132 // ranges on the unit's assigned machine, mapped to the unit that 133 // opened each range and the relevant relation. 134 machinePorts map[network.PortRange]params.RelationUnit 135 136 // assignedMachineTag contains the tag of the unit's assigned 137 // machine. 138 assignedMachineTag names.MachineTag 139 140 // process is the process of the command that is being run in the local context, 141 // like a juju-run command or a hook 142 process *os.Process 143 144 // rebootPriority tells us when the hook wants to reboot. If rebootPriority is jujuc.RebootNow 145 // the hook will be killed and requeued 146 rebootPriority jujuc.RebootPriority 147 148 // storage provides access to the information about storage attached to the unit. 149 storage StorageContextAccessor 150 151 // storageId is the tag of the storage instance associated with the running hook. 152 storageTag names.StorageTag 153 154 // hasRunSetStatus is true if a call to the status-set was made during the 155 // invocation of a hook. 156 // This attribute is persisted to local uniter state at the end of the hook 157 // execution so that the uniter can ultimately decide if it needs to update 158 // a charm's workload status, or if the charm has already taken care of it. 159 hasRunStatusSet bool 160 161 // storageAddConstraints is a collection of storage constraints 162 // keyed on storage name as specified in the charm. 163 // This collection will be added to the unit on successful 164 // hook run, so the actual add will happen in a flush. 165 storageAddConstraints map[string][]params.StorageConstraints 166 } 167 168 func (ctx *HookContext) RequestReboot(priority jujuc.RebootPriority) error { 169 var err error 170 if priority == jujuc.RebootNow { 171 // At this point, the hook should be running 172 err = ctx.killCharmHook() 173 } 174 175 switch err { 176 case nil, ErrNoProcess: 177 // ErrNoProcess almost certainly means we are running in debug hooks 178 ctx.SetRebootPriority(priority) 179 } 180 return err 181 } 182 183 func (ctx *HookContext) GetRebootPriority() jujuc.RebootPriority { 184 mutex.Lock() 185 defer mutex.Unlock() 186 return ctx.rebootPriority 187 } 188 189 func (ctx *HookContext) SetRebootPriority(priority jujuc.RebootPriority) { 190 mutex.Lock() 191 defer mutex.Unlock() 192 ctx.rebootPriority = priority 193 } 194 195 func (ctx *HookContext) GetProcess() *os.Process { 196 mutex.Lock() 197 defer mutex.Unlock() 198 return ctx.process 199 } 200 201 func (ctx *HookContext) SetProcess(process *os.Process) { 202 mutex.Lock() 203 defer mutex.Unlock() 204 ctx.process = process 205 } 206 207 func (ctx *HookContext) Id() string { 208 return ctx.id 209 } 210 211 func (ctx *HookContext) UnitName() string { 212 return ctx.unitName 213 } 214 215 // UnitStatus will return the status for the current Unit. 216 func (ctx *HookContext) UnitStatus() (*jujuc.StatusInfo, error) { 217 if ctx.status == nil { 218 var err error 219 status, err := ctx.unit.UnitStatus() 220 if err != nil { 221 return nil, err 222 } 223 ctx.status = &jujuc.StatusInfo{ 224 Status: string(status.Status), 225 Info: status.Info, 226 Data: status.Data, 227 } 228 } 229 return ctx.status, nil 230 } 231 232 // ServiceStatus returns the status for the service and all the units on 233 // the service to which this context unit belongs, only if this unit is 234 // the leader. 235 func (ctx *HookContext) ServiceStatus() (jujuc.ServiceStatusInfo, error) { 236 var err error 237 isLeader, err := ctx.IsLeader() 238 if err != nil { 239 return jujuc.ServiceStatusInfo{}, errors.Annotatef(err, "cannot determine leadership") 240 } 241 if !isLeader { 242 return jujuc.ServiceStatusInfo{}, ErrIsNotLeader 243 } 244 service, err := ctx.unit.Service() 245 if err != nil { 246 return jujuc.ServiceStatusInfo{}, errors.Trace(err) 247 } 248 status, err := service.Status(ctx.unit.Name()) 249 if err != nil { 250 return jujuc.ServiceStatusInfo{}, errors.Trace(err) 251 } 252 us := make([]jujuc.StatusInfo, len(status.Units)) 253 i := 0 254 for t, s := range status.Units { 255 us[i] = jujuc.StatusInfo{ 256 Tag: t, 257 Status: string(s.Status), 258 Info: s.Info, 259 Data: s.Data, 260 } 261 i++ 262 } 263 return jujuc.ServiceStatusInfo{ 264 Service: jujuc.StatusInfo{ 265 Tag: service.Tag().String(), 266 Status: string(status.Service.Status), 267 Info: status.Service.Info, 268 Data: status.Service.Data, 269 }, 270 Units: us, 271 }, nil 272 } 273 274 // SetUnitStatus will set the given status for this unit. 275 func (ctx *HookContext) SetUnitStatus(status jujuc.StatusInfo) error { 276 ctx.hasRunStatusSet = true 277 logger.Debugf("[WORKLOAD-STATUS] %s: %s", status.Status, status.Info) 278 return ctx.unit.SetUnitStatus( 279 params.Status(status.Status), 280 status.Info, 281 status.Data, 282 ) 283 } 284 285 // SetServiceStatus will set the given status to the service to which this 286 // unit's belong, only if this unit is the leader. 287 func (ctx *HookContext) SetServiceStatus(status jujuc.StatusInfo) error { 288 logger.Debugf("[SERVICE-STATUS] %s: %s", status.Status, status.Info) 289 isLeader, err := ctx.IsLeader() 290 if err != nil { 291 return errors.Annotatef(err, "cannot determine leadership") 292 } 293 if !isLeader { 294 return ErrIsNotLeader 295 } 296 297 service, err := ctx.unit.Service() 298 if err != nil { 299 return errors.Trace(err) 300 } 301 return service.SetStatus( 302 ctx.unit.Name(), 303 params.Status(status.Status), 304 status.Info, 305 status.Data, 306 ) 307 } 308 309 func (ctx *HookContext) HasExecutionSetUnitStatus() bool { 310 return ctx.hasRunStatusSet 311 } 312 313 func (ctx *HookContext) ResetExecutionSetUnitStatus() { 314 ctx.hasRunStatusSet = false 315 } 316 317 func (ctx *HookContext) PublicAddress() (string, bool) { 318 return ctx.publicAddress, ctx.publicAddress != "" 319 } 320 321 func (ctx *HookContext) PrivateAddress() (string, bool) { 322 return ctx.privateAddress, ctx.privateAddress != "" 323 } 324 325 func (ctx *HookContext) AvailabilityZone() (string, bool) { 326 return ctx.availabilityzone, ctx.availabilityzone != "" 327 } 328 329 func (ctx *HookContext) StorageTags() []names.StorageTag { 330 return ctx.storage.StorageTags() 331 } 332 333 func (ctx *HookContext) HookStorage() (jujuc.ContextStorageAttachment, bool) { 334 return ctx.Storage(ctx.storageTag) 335 } 336 337 func (ctx *HookContext) Storage(tag names.StorageTag) (jujuc.ContextStorageAttachment, bool) { 338 return ctx.storage.Storage(tag) 339 } 340 341 func (ctx *HookContext) AddUnitStorage(cons map[string]params.StorageConstraints) { 342 // All storage constraints are accumulated before context is flushed. 343 if ctx.storageAddConstraints == nil { 344 ctx.storageAddConstraints = make( 345 map[string][]params.StorageConstraints, 346 len(cons)) 347 } 348 for storage, newConstraints := range cons { 349 // Multiple calls for the same storage are accumulated as well. 350 ctx.storageAddConstraints[storage] = append( 351 ctx.storageAddConstraints[storage], 352 newConstraints) 353 } 354 } 355 356 func (ctx *HookContext) OpenPorts(protocol string, fromPort, toPort int) error { 357 return tryOpenPorts( 358 protocol, fromPort, toPort, 359 ctx.unit.Tag(), 360 ctx.machinePorts, ctx.pendingPorts, 361 ) 362 } 363 364 func (ctx *HookContext) ClosePorts(protocol string, fromPort, toPort int) error { 365 return tryClosePorts( 366 protocol, fromPort, toPort, 367 ctx.unit.Tag(), 368 ctx.machinePorts, ctx.pendingPorts, 369 ) 370 } 371 372 func (ctx *HookContext) OpenedPorts() []network.PortRange { 373 var unitRanges []network.PortRange 374 for portRange, relUnit := range ctx.machinePorts { 375 if relUnit.Unit == ctx.unit.Tag().String() { 376 unitRanges = append(unitRanges, portRange) 377 } 378 } 379 network.SortPortRanges(unitRanges) 380 return unitRanges 381 } 382 383 func (ctx *HookContext) ConfigSettings() (charm.Settings, error) { 384 if ctx.configSettings == nil { 385 var err error 386 ctx.configSettings, err = ctx.unit.ConfigSettings() 387 if err != nil { 388 return nil, err 389 } 390 } 391 result := charm.Settings{} 392 for name, value := range ctx.configSettings { 393 result[name] = value 394 } 395 return result, nil 396 } 397 398 // ActionName returns the name of the action. 399 func (ctx *HookContext) ActionName() (string, error) { 400 if ctx.actionData == nil { 401 return "", errors.New("not running an action") 402 } 403 return ctx.actionData.Name, nil 404 } 405 406 // ActionParams simply returns the arguments to the Action. 407 func (ctx *HookContext) ActionParams() (map[string]interface{}, error) { 408 if ctx.actionData == nil { 409 return nil, errors.New("not running an action") 410 } 411 return ctx.actionData.Params, nil 412 } 413 414 // SetActionMessage sets a message for the Action, usually an error message. 415 func (ctx *HookContext) SetActionMessage(message string) error { 416 if ctx.actionData == nil { 417 return errors.New("not running an action") 418 } 419 ctx.actionData.ResultsMessage = message 420 return nil 421 } 422 423 // SetActionFailed sets the fail state of the action. 424 func (ctx *HookContext) SetActionFailed() error { 425 if ctx.actionData == nil { 426 return errors.New("not running an action") 427 } 428 ctx.actionData.Failed = true 429 return nil 430 } 431 432 // UpdateActionResults inserts new values for use with action-set and 433 // action-fail. The results struct will be delivered to the state server 434 // upon completion of the Action. It returns an error if not called on an 435 // Action-containing HookContext. 436 func (ctx *HookContext) UpdateActionResults(keys []string, value string) error { 437 if ctx.actionData == nil { 438 return errors.New("not running an action") 439 } 440 addValueToMap(keys, value, ctx.actionData.ResultsMap) 441 return nil 442 } 443 444 func (ctx *HookContext) HookRelation() (jujuc.ContextRelation, bool) { 445 return ctx.Relation(ctx.relationId) 446 } 447 448 func (ctx *HookContext) RemoteUnitName() (string, bool) { 449 return ctx.remoteUnitName, ctx.remoteUnitName != "" 450 } 451 452 func (ctx *HookContext) Relation(id int) (jujuc.ContextRelation, bool) { 453 r, found := ctx.relations[id] 454 return r, found 455 } 456 457 func (ctx *HookContext) RelationIds() []int { 458 ids := []int{} 459 for id := range ctx.relations { 460 ids = append(ids, id) 461 } 462 return ids 463 } 464 465 // AddMetric adds metrics to the hook context. 466 func (ctx *HookContext) AddMetric(key, value string, created time.Time) error { 467 if ctx.metricsRecorder == nil || ctx.definedMetrics == nil { 468 return errors.New("metrics disabled") 469 } 470 471 err := ctx.definedMetrics.ValidateMetric(key, value) 472 if err != nil { 473 return errors.Annotatef(err, "invalid metric %q", key) 474 } 475 476 err = ctx.metricsRecorder.AddMetric(key, value, created) 477 if err != nil { 478 return errors.Annotate(err, "failed to store metric") 479 } 480 return nil 481 } 482 483 // ActionData returns the context's internal action data. It's meant to be 484 // transitory; it exists to allow uniter and runner code to keep working as 485 // it did; it should be considered deprecated, and not used by new clients. 486 func (c *HookContext) ActionData() (*ActionData, error) { 487 if c.actionData == nil { 488 return nil, errors.New("not running an action") 489 } 490 return c.actionData, nil 491 } 492 493 // HookVars returns an os.Environ-style list of strings necessary to run a hook 494 // such that it can know what environment it's operating in, and can call back 495 // into context. 496 func (context *HookContext) HookVars(paths Paths) []string { 497 vars := context.proxySettings.AsEnvironmentValues() 498 vars = append(vars, 499 "CHARM_DIR="+paths.GetCharmDir(), // legacy, embarrassing 500 "JUJU_CHARM_DIR="+paths.GetCharmDir(), 501 "JUJU_CONTEXT_ID="+context.id, 502 "JUJU_AGENT_SOCKET="+paths.GetJujucSocket(), 503 "JUJU_UNIT_NAME="+context.unitName, 504 "JUJU_ENV_UUID="+context.uuid, 505 "JUJU_ENV_NAME="+context.envName, 506 "JUJU_API_ADDRESSES="+strings.Join(context.apiAddrs, " "), 507 "JUJU_METER_STATUS="+context.meterStatus.code, 508 "JUJU_METER_INFO="+context.meterStatus.info, 509 "JUJU_MACHINE_ID="+context.assignedMachineTag.Id(), 510 "JUJU_AVAILABILITY_ZONE="+context.availabilityzone, 511 ) 512 if r, found := context.HookRelation(); found { 513 vars = append(vars, 514 "JUJU_RELATION="+r.Name(), 515 "JUJU_RELATION_ID="+r.FakeId(), 516 "JUJU_REMOTE_UNIT="+context.remoteUnitName, 517 ) 518 } 519 if context.actionData != nil { 520 vars = append(vars, 521 "JUJU_ACTION_NAME="+context.actionData.Name, 522 "JUJU_ACTION_UUID="+context.actionData.Tag.Id(), 523 "JUJU_ACTION_TAG="+context.actionData.Tag.String(), 524 ) 525 } 526 return append(vars, osDependentEnvVars(paths)...) 527 } 528 529 func (ctx *HookContext) handleReboot(err *error) { 530 logger.Infof("handling reboot") 531 rebootPriority := ctx.GetRebootPriority() 532 switch rebootPriority { 533 case jujuc.RebootSkip: 534 return 535 case jujuc.RebootAfterHook: 536 // Reboot should happen only after hook has finished. 537 if *err != nil { 538 return 539 } 540 *err = ErrReboot 541 case jujuc.RebootNow: 542 *err = ErrRequeueAndReboot 543 } 544 err2 := ctx.unit.SetUnitStatus(params.StatusRebooting, "", nil) 545 if err2 != nil { 546 logger.Errorf("updating agent status: %v", err2) 547 } 548 reqErr := ctx.unit.RequestReboot() 549 if reqErr != nil { 550 *err = reqErr 551 } 552 } 553 554 // addJujuUnitsMetric adds the juju-units built in metric if it 555 // is defined for this context. 556 func (ctx *HookContext) addJujuUnitsMetric() error { 557 if ctx.metricsRecorder.IsDeclaredMetric("juju-units") { 558 err := ctx.metricsRecorder.AddMetric("juju-units", "1", time.Now().UTC()) 559 if err != nil { 560 return errors.Trace(err) 561 } 562 } 563 return nil 564 } 565 566 // Prepare implements the Context interface. 567 func (ctx *HookContext) Prepare() error { 568 if ctx.actionData != nil { 569 err := ctx.state.ActionBegin(ctx.actionData.Tag) 570 if err != nil { 571 return errors.Trace(err) 572 } 573 } 574 return nil 575 } 576 577 // Flush implements the Context interface. 578 func (ctx *HookContext) Flush(process string, ctxErr error) (err error) { 579 // A non-existant metricsRecorder simply means that metrics were disabled 580 // for this hook run. 581 if ctx.metricsRecorder != nil { 582 err := ctx.addJujuUnitsMetric() 583 if err != nil { 584 return errors.Trace(err) 585 } 586 err = ctx.metricsRecorder.Close() 587 if err != nil { 588 return errors.Trace(err) 589 } 590 } 591 592 writeChanges := ctxErr == nil 593 594 // In the case of Actions, handle any errors using finalizeAction. 595 if ctx.actionData != nil { 596 // If we had an error in err at this point, it's part of the 597 // normal behavior of an Action. Errors which happen during 598 // the finalize should be handed back to the uniter. Close 599 // over the existing err, clear it, and only return errors 600 // which occur during the finalize, e.g. API call errors. 601 defer func(ctxErr error) { 602 err = ctx.finalizeAction(ctxErr, err) 603 }(ctxErr) 604 ctxErr = nil 605 } else { 606 // TODO(gsamfira): Just for now, reboot will not be supported in actions. 607 defer ctx.handleReboot(&err) 608 } 609 610 for id, rctx := range ctx.relations { 611 if writeChanges { 612 if e := rctx.WriteSettings(); e != nil { 613 e = errors.Errorf( 614 "could not write settings from %q to relation %d: %v", 615 process, id, e, 616 ) 617 logger.Errorf("%v", e) 618 if ctxErr == nil { 619 ctxErr = e 620 } 621 } 622 } 623 } 624 625 for rangeKey, rangeInfo := range ctx.pendingPorts { 626 if writeChanges { 627 var e error 628 var op string 629 if rangeInfo.ShouldOpen { 630 e = ctx.unit.OpenPorts( 631 rangeKey.Ports.Protocol, 632 rangeKey.Ports.FromPort, 633 rangeKey.Ports.ToPort, 634 ) 635 op = "open" 636 } else { 637 e = ctx.unit.ClosePorts( 638 rangeKey.Ports.Protocol, 639 rangeKey.Ports.FromPort, 640 rangeKey.Ports.ToPort, 641 ) 642 op = "close" 643 } 644 if e != nil { 645 e = errors.Annotatef(e, "cannot %s %v", op, rangeKey.Ports) 646 logger.Errorf("%v", e) 647 if ctxErr == nil { 648 ctxErr = e 649 } 650 } 651 } 652 } 653 654 // add storage to unit dynamically 655 if len(ctx.storageAddConstraints) > 0 && writeChanges { 656 err := ctx.unit.AddStorage(ctx.storageAddConstraints) 657 if err != nil { 658 err = errors.Annotatef(err, "cannot add storage") 659 logger.Errorf("%v", err) 660 if ctxErr == nil { 661 ctxErr = err 662 } 663 } 664 } 665 666 // TODO (tasdomas) 2014 09 03: context finalization needs to modified to apply all 667 // changes in one api call to minimize the risk 668 // of partial failures. 669 670 if !writeChanges { 671 return ctxErr 672 } 673 674 return ctxErr 675 } 676 677 // finalizeAction passes back the final status of an Action hook to state. 678 // It wraps any errors which occurred in normal behavior of the Action run; 679 // only errors passed in unhandledErr will be returned. 680 func (ctx *HookContext) finalizeAction(err, unhandledErr error) error { 681 // TODO (binary132): synchronize with gsamfira's reboot logic 682 message := ctx.actionData.ResultsMessage 683 results := ctx.actionData.ResultsMap 684 tag := ctx.actionData.Tag 685 status := params.ActionCompleted 686 if ctx.actionData.Failed { 687 status = params.ActionFailed 688 } 689 690 // If we had an action error, we'll simply encapsulate it in the response 691 // and discard the error state. Actions should not error the uniter. 692 if err != nil { 693 message = err.Error() 694 if IsMissingHookError(err) { 695 message = fmt.Sprintf("action not implemented on unit %q", ctx.unitName) 696 } 697 status = params.ActionFailed 698 } 699 700 callErr := ctx.state.ActionFinish(tag, status, results, message) 701 if callErr != nil { 702 unhandledErr = errors.Wrap(unhandledErr, callErr) 703 } 704 return unhandledErr 705 } 706 707 // killCharmHook tries to kill the current running charm hook. 708 func (ctx *HookContext) killCharmHook() error { 709 proc := ctx.GetProcess() 710 if proc == nil { 711 // nothing to kill 712 return ErrNoProcess 713 } 714 logger.Infof("trying to kill context process %d", proc.Pid) 715 716 tick := time.After(0) 717 timeout := time.After(30 * time.Second) 718 for { 719 // We repeatedly try to kill the process until we fail; this is 720 // because we don't control the *Process, and our clients expect 721 // to be able to Wait(); so we can't Wait. We could do better, 722 // but not with a single implementation across all platforms. 723 // TODO(gsamfira): come up with a better cross-platform approach. 724 select { 725 case <-tick: 726 err := proc.Kill() 727 if err != nil { 728 logger.Infof("kill returned: %s", err) 729 logger.Infof("assuming already killed") 730 return nil 731 } 732 case <-timeout: 733 return errors.Errorf("failed to kill context process %d", proc.Pid) 734 } 735 logger.Infof("waiting for context process %d to die", proc.Pid) 736 tick = time.After(100 * time.Millisecond) 737 } 738 }