master
go 755 lines 19.3 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package dyncfg
4
5 import (
6 "context"
7 "errors"
8 "sync"
9 "time"
10
11 "github.com/netdata/netdata/go/plugins/logger"
12 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
13 )
14
15 // Callbacks defines component-specific operations for the handler.
16 type Callbacks[C Config] interface {
17 // ExtractKey parses dyncfg function ID into cache key + job name.
18 ExtractKey(fn Function) (key, name string, ok bool)
19
20 // ParseAndValidate parses payload into a config with dyncfg metadata set.
21 // Includes all validation (including heavy checks like module instantiation).
22 ParseAndValidate(fn Function, name string) (C, error)
23
24 // ValidateJobName enforces the domain's job-name policy. Called before
25 // ParseAndValidate so cheap name-format rejections happen without parsing payload.
26 ValidateJobName(name string) error
27
28 // Start creates a work unit and starts it. Owns the full start lifecycle
29 // including pre-start cleanup and post-fail retry scheduling.
30 // Return CodedError to override EnableFailCode.
31 // Used by CmdEnable and CmdUpdate (conversion only).
32 Start(cfg C) error
33
34 // Update handles non-conversion config updates (dyncfg->dyncfg).
35 // Called after caches are already updated. SD uses mgr.Restart
36 // for graceful transition; jobmgr uses Stop+Start.
37 Update(oldCfg, newCfg C) error
38
39 // Stop stops all work and cleans up all component state for a config.
40 // Safe to call for non-running configs (all ops are no-ops).
41 Stop(cfg C)
42
43 // OnStatusChange is called after status transitions in enable/disable/update.
44 // Not called in CmdAdd or CmdRemove.
45 OnStatusChange(entry *Entry[C], oldStatus Status, fn Function)
46
47 // ConfigID returns the dyncfg wire protocol ID for a config.
48 ConfigID(cfg C) string
49 }
50
51 // CodedError allows callbacks to override the default response code.
52 type CodedError interface {
53 error
54 Code() int
55 }
56
57 // CommandMessageSource optionally provides a success/warning message
58 // for the command that just completed.
59 type CommandMessageSource interface {
60 TakeCommandMessage() string
61 }
62
63 // HandlerOpts configures the handler with component-specific settings.
64 type HandlerOpts[C Config] struct {
65 Logger *logger.Logger
66 API *Responder
67 Seen *SeenCache[C]
68 Exposed *ExposedCache[C]
69 Callbacks Callbacks[C]
70 WaitKey func(cfg C) string // optional key used to gate config processing until enable/disable
71 WaitTimeout time.Duration // optional timeout for decision wait; zero keeps wait open until matching command
72
73 Path string // dyncfg path (e.g. "/collectors/go.d/Jobs")
74 EnableFailCode int // response code for enable failure (jobmgr: 200, SD: 422)
75 RemoveStockOnEnableFail bool // remove stock config from exposed on enable failure
76 JobCommands []Command // base commands for jobs; CommandRemove is added implicitly for dyncfg configs
77 }
78
79 // Handler implements the shared dyncfg command state machine.
80 // It manages two caches (seen/exposed) borrowed from the component,
81 // and delegates domain-specific work to Callbacks.
82 type Handler[C Config] struct {
83 *logger.Logger
84 api *Responder
85 seen *SeenCache[C]
86 exposed *ExposedCache[C]
87 cb Callbacks[C]
88 path string
89 enableFailCode int
90 removeStockOnEnableFail bool
91 jobCommands []Command
92 waitGate *waitGate[C]
93 }
94
95 func takeCommandMessage[C Config](cb Callbacks[C]) string {
96 msgSrc, ok := any(cb).(CommandMessageSource)
97 if !ok {
98 return ""
99 }
100 return msgSrc.TakeCommandMessage()
101 }
102
103 // WaitTimeoutEvent describes a wait gate timeout transition.
104 type WaitTimeoutEvent struct {
105 Key string
106 Elapsed time.Duration
107 Threshold time.Duration
108 }
109
110 // WaitDecisionStep is one serialized wait-loop transition.
111 type WaitDecisionStep struct {
112 Command Function
113 HasCommand bool
114 Timeout WaitTimeoutEvent
115 TimedOut bool
116 }
117
118 // waitGate encapsulates wait-for-decision state and timing orchestration.
119 type waitGate[C Config] struct {
120 keyFn func(cfg C) string
121 timeout time.Duration
122 key string
123 since time.Time
124 deadline time.Time
125 mu sync.RWMutex
126 now func() time.Time
127 }
128
129 func newWaitGate[C Config](keyFn func(cfg C) string, timeout time.Duration) *waitGate[C] {
130 return &waitGate[C]{
131 keyFn: keyFn,
132 timeout: timeout,
133 now: time.Now,
134 }
135 }
136
137 func (wg *waitGate[C]) waitForDecision(cfg C) {
138 if wg.keyFn == nil {
139 return
140 }
141 key := wg.keyFn(cfg)
142 if key == "" {
143 return
144 }
145
146 wg.mu.Lock()
147 wg.key = key
148 wg.since = time.Time{}
149 wg.deadline = time.Time{}
150 if wg.timeout > 0 {
151 now := wg.nowTime()
152 wg.since = now
153 wg.deadline = now.Add(wg.timeout)
154 }
155 wg.mu.Unlock()
156 }
157
158 func (wg *waitGate[C]) waitingForDecision() bool {
159 wg.mu.RLock()
160 defer wg.mu.RUnlock()
161 return wg.key != ""
162 }
163
164 func (wg *waitGate[C]) decisionRemaining() (time.Duration, bool) {
165 wg.mu.RLock()
166 defer wg.mu.RUnlock()
167
168 if wg.timeout <= 0 || wg.key == "" || wg.deadline.IsZero() {
169 return 0, false
170 }
171 now := wg.nowTime()
172 if now.After(wg.deadline) || now.Equal(wg.deadline) {
173 return 0, true
174 }
175 return wg.deadline.Sub(now), true
176 }
177
178 func (wg *waitGate[C]) nextStep(ctx context.Context, dyncfgCh <-chan Function) (WaitDecisionStep, bool) {
179 var step WaitDecisionStep
180
181 waitFor, hasTimeout := wg.decisionRemaining()
182 if !hasTimeout {
183 select {
184 case <-ctx.Done():
185 return step, false
186 case fn := <-dyncfgCh:
187 step.Command = fn
188 step.HasCommand = true
189 return step, true
190 }
191 }
192
193 timer := time.NewTimer(waitFor)
194 defer func() {
195 if !timer.Stop() {
196 select {
197 case <-timer.C:
198 default:
199 }
200 }
201 }()
202
203 select {
204 case <-ctx.Done():
205 return step, false
206 case fn := <-dyncfgCh:
207 step.Command = fn
208 step.HasCommand = true
209 return step, true
210 case <-timer.C:
211 step.Timeout, step.TimedOut = wg.expireDecision()
212 return step, true
213 }
214 }
215
216 func (wg *waitGate[C]) expireDecision() (WaitTimeoutEvent, bool) {
217 var event WaitTimeoutEvent
218
219 if wg.timeout <= 0 {
220 return event, false
221 }
222 now := wg.nowTime()
223
224 wg.mu.Lock()
225 defer wg.mu.Unlock()
226
227 if wg.key == "" || wg.deadline.IsZero() || now.Before(wg.deadline) {
228 return event, false
229 }
230
231 event.Key = wg.key
232 event.Threshold = wg.timeout
233 if !wg.since.IsZero() && now.After(wg.since) {
234 event.Elapsed = now.Sub(wg.since)
235 } else {
236 event.Elapsed = wg.timeout
237 }
238
239 wg.clearLocked()
240 return event, true
241 }
242
243 func (wg *waitGate[C]) currentKey() string {
244 wg.mu.RLock()
245 defer wg.mu.RUnlock()
246 return wg.key
247 }
248
249 func (wg *waitGate[C]) keyFor(cfg C) string {
250 if wg.keyFn == nil {
251 return ""
252 }
253 return wg.keyFn(cfg)
254 }
255
256 func (wg *waitGate[C]) clearIfMatch(key string) {
257 wg.mu.Lock()
258 defer wg.mu.Unlock()
259 if wg.key == key {
260 wg.clearLocked()
261 }
262 }
263
264 func (wg *waitGate[C]) clearLocked() {
265 wg.key = ""
266 wg.since = time.Time{}
267 wg.deadline = time.Time{}
268 }
269
270 func (wg *waitGate[C]) nowTime() time.Time {
271 if wg.now != nil {
272 return wg.now()
273 }
274 return time.Now()
275 }
276
277 func (wg *waitGate[C]) setNow(now func() time.Time) {
278 wg.mu.Lock()
279 wg.now = now
280 wg.mu.Unlock()
281 }
282
283 func NewHandler[C Config](opts HandlerOpts[C]) *Handler[C] {
284 return &Handler[C]{
285 Logger: opts.Logger,
286 api: opts.API,
287 seen: opts.Seen,
288 exposed: opts.Exposed,
289 cb: opts.Callbacks,
290 path: opts.Path,
291 enableFailCode: opts.EnableFailCode,
292 removeStockOnEnableFail: opts.RemoveStockOnEnableFail,
293 jobCommands: opts.JobCommands,
294 waitGate: newWaitGate(opts.WaitKey, opts.WaitTimeout),
295 }
296 }
297
298 func (h *Handler[C]) Seen() *SeenCache[C] { return h.seen }
299 func (h *Handler[C]) Exposed() *ExposedCache[C] { return h.exposed }
300
301 // SetAPI replaces the responder (e.g. to silence output in CLI mode).
302 func (h *Handler[C]) SetAPI(api *Responder) { h.api = api }
303
304 // RememberDiscoveredConfig ensures a discovered config is present in Seen cache.
305 func (h *Handler[C]) RememberDiscoveredConfig(cfg C) {
306 if _, ok := h.seen.Lookup(cfg); ok {
307 return
308 }
309 h.seen.Add(cfg)
310 }
311
312 // AddDiscoveredConfig upserts a discovered config into Seen and Exposed caches.
313 func (h *Handler[C]) AddDiscoveredConfig(cfg C, status Status) *Entry[C] {
314 h.RememberDiscoveredConfig(cfg)
315 entry := &Entry[C]{Cfg: cfg, Status: status}
316 h.exposed.Add(entry)
317 return entry
318 }
319
320 // RemoveDiscoveredConfig removes a discovered config from Seen and Exposed caches.
321 // Returns the removed Exposed entry when the removed seen config was also exposed.
322 func (h *Handler[C]) RemoveDiscoveredConfig(cfg C) (*Entry[C], bool) {
323 if _, ok := h.seen.Lookup(cfg); !ok {
324 return nil, false
325 }
326 h.seen.Remove(cfg)
327
328 entry, ok := h.exposed.LookupByKey(cfg.ExposedKey())
329 if !ok || entry.Cfg.UID() != cfg.UID() {
330 return nil, false
331 }
332
333 h.exposed.Remove(cfg)
334 return entry, true
335 }
336
337 // WaitForDecision blocks non-dyncfg config processing until a matching
338 // enable/disable command is observed for the provided config.
339 func (h *Handler[C]) WaitForDecision(cfg C) {
340 h.waitGate.waitForDecision(cfg)
341 }
342
343 // WaitingForDecision reports whether config processing should currently wait
344 // for a matching enable/disable command.
345 func (h *Handler[C]) WaitingForDecision() bool {
346 return h.waitGate.waitingForDecision()
347 }
348
349 // WaitDecisionRemaining returns time until wait gate timeout.
350 func (h *Handler[C]) WaitDecisionRemaining() (time.Duration, bool) {
351 return h.waitGate.decisionRemaining()
352 }
353
354 // NextWaitDecisionStep blocks until either a dyncfg command arrives, wait timeout fires, or context is canceled.
355 // It centralizes wait-loop orchestration so caller logic stays minimal.
356 func (h *Handler[C]) NextWaitDecisionStep(ctx context.Context, dyncfgCh <-chan Function) (WaitDecisionStep, bool) {
357 return h.waitGate.nextStep(ctx, dyncfgCh)
358 }
359
360 // ExpireWaitDecision clears the current wait gate when it exceeds configured timeout.
361 func (h *Handler[C]) ExpireWaitDecision() (WaitTimeoutEvent, bool) {
362 return h.waitGate.expireDecision()
363 }
364
365 // SyncDecision updates wait-state based on the incoming command.
366 // Only a matching enable/disable command clears the current wait key.
367 func (h *Handler[C]) SyncDecision(fn Function) {
368 cmd := fn.Command()
369 if cmd != CommandEnable && cmd != CommandDisable {
370 return
371 }
372
373 waitKey := h.waitGate.currentKey()
374 if waitKey == "" {
375 return
376 }
377
378 key, _, ok := h.cb.ExtractKey(fn)
379 if !ok {
380 return
381 }
382 entry, ok := h.exposed.LookupByKey(key)
383 if !ok {
384 return
385 }
386 if h.waitGate.keyFor(entry.Cfg) != waitKey {
387 return
388 }
389
390 h.waitGate.clearIfMatch(waitKey)
391 }
392
393 // NotifyJobCreate registers/updates a config in the dyncfg API (upsert).
394 func (h *Handler[C]) NotifyJobCreate(cfg C, status Status) {
395 isDyncfg := cfg.SourceType() == "dyncfg"
396 h.api.ConfigCreate(netdataapi.ConfigOpts{
397 ID: h.cb.ConfigID(cfg),
398 Status: status.String(),
399 ConfigType: ConfigTypeJob.String(),
400 Path: h.path,
401 SourceType: cfg.SourceType(),
402 Source: cfg.Source(),
403 SupportedCommands: h.jobSupportedCommands(isDyncfg),
404 })
405 }
406
407 // NotifyJobStatus sends a status update for a config.
408 func (h *Handler[C]) NotifyJobStatus(cfg C, status Status) {
409 h.api.ConfigStatus(h.cb.ConfigID(cfg), status)
410 }
411
412 // NotifyJobRemove removes a config from the dyncfg API.
413 func (h *Handler[C]) NotifyJobRemove(cfg C) {
414 h.api.ConfigDelete(h.cb.ConfigID(cfg))
415 }
416
417 func (h *Handler[C]) jobSupportedCommands(isDyncfg bool) string {
418 cmds := make([]Command, len(h.jobCommands))
419 copy(cmds, h.jobCommands)
420 if isDyncfg {
421 cmds = append(cmds, CommandRemove)
422 }
423 return JoinCommands(cmds...)
424 }
425
426 // CmdAdd handles the "add" command.
427 func (h *Handler[C]) CmdAdd(fn Function) {
428 if err := fn.ValidateArgs(3); err != nil {
429 h.api.SendCodef(fn, 400, "%v", err)
430 return
431 }
432
433 key, name, ok := h.cb.ExtractKey(fn)
434 if !ok {
435 h.api.SendCodef(fn, 400, "invalid job ID format.")
436 return
437 }
438
439 if err := fn.ValidateHasPayload(); err != nil {
440 h.api.SendCodef(fn, 400, "%v", err)
441 return
442 }
443
444 if err := h.cb.ValidateJobName(name); err != nil {
445 h.api.SendCodef(fn, 400, "invalid job name '%s': %v.", name, err)
446 return
447 }
448
449 newCfg, err := h.cb.ParseAndValidate(fn, name)
450 if err != nil {
451 h.api.SendCodef(fn, 400, "%v", err)
452 return
453 }
454
455 // Replace existing config at the same key, if any.
456 if existing, ok := h.exposed.LookupByKey(key); ok {
457 if _, found := h.seen.Lookup(existing.Cfg); found && existing.Cfg.SourceType() == "dyncfg" {
458 h.seen.Remove(existing.Cfg)
459 }
460 h.exposed.Remove(existing.Cfg)
461 h.cb.Stop(existing.Cfg)
462 }
463
464 h.seen.Add(newCfg)
465 newEntry := &Entry[C]{Cfg: newCfg, Status: StatusAccepted}
466 h.exposed.Add(newEntry)
467
468 h.api.SendCodef(fn, 202, "")
469 h.NotifyJobCreate(newCfg, StatusAccepted)
470 }
471
472 // CmdEnable handles the "enable" command.
473 func (h *Handler[C]) CmdEnable(fn Function) {
474 key, _, ok := h.cb.ExtractKey(fn)
475 if !ok {
476 h.api.SendCodef(fn, 400, "invalid job ID format.")
477 return
478 }
479
480 entry, ok := h.exposed.LookupByKey(key)
481 if !ok {
482 h.api.SendCodef(fn, 404, "job not found.")
483 return
484 }
485
486 oldStatus := entry.Status
487
488 switch entry.Status {
489 case StatusRunning:
490 h.api.SendCodef(fn, 200, "")
491 h.NotifyJobStatus(entry.Cfg, StatusRunning)
492 return
493 case StatusAccepted, StatusDisabled, StatusFailed:
494 // proceed to start
495 default:
496 h.api.SendCodef(fn, 405, "enabling is not allowed in '%s' state.", entry.Status)
497 h.NotifyJobStatus(entry.Cfg, entry.Status)
498 return
499 }
500
501 err := h.cb.Start(entry.Cfg)
502
503 if err != nil {
504 entry.Status = StatusFailed
505
506 code := h.enableFailCode
507 var ce CodedError
508 if errors.As(err, &ce) {
509 code = ce.Code()
510 }
511 h.api.SendCodef(fn, code, "%v", err)
512
513 // Stock removal only for non-CodedError failures (runtime detection failures).
514 // CodedError = validation error (e.g. createCollectorJob → 400, no stock removal).
515 if h.removeStockOnEnableFail && !isCodedError(err) && entry.Cfg.SourceType() == "stock" {
516 h.exposed.Remove(entry.Cfg)
517 h.NotifyJobRemove(entry.Cfg)
518 } else {
519 h.NotifyJobStatus(entry.Cfg, StatusFailed)
520 }
521
522 h.cb.OnStatusChange(entry, oldStatus, fn)
523 return
524 }
525
526 entry.Status = StatusRunning
527 h.api.SendCodef(fn, 200, "%s", takeCommandMessage(h.cb))
528 h.NotifyJobStatus(entry.Cfg, StatusRunning)
529 h.cb.OnStatusChange(entry, oldStatus, fn)
530 }
531
532 // CmdDisable handles the "disable" command.
533 func (h *Handler[C]) CmdDisable(fn Function) {
534 key, _, ok := h.cb.ExtractKey(fn)
535 if !ok {
536 h.api.SendCodef(fn, 400, "invalid job ID format.")
537 return
538 }
539
540 entry, ok := h.exposed.LookupByKey(key)
541 if !ok {
542 h.api.SendCodef(fn, 404, "job not found.")
543 return
544 }
545
546 oldStatus := entry.Status
547
548 if entry.Status == StatusDisabled {
549 h.api.SendCodef(fn, 200, "")
550 h.NotifyJobStatus(entry.Cfg, StatusDisabled)
551 return
552 }
553
554 // Unconditional for all non-Disabled statuses.
555 h.cb.Stop(entry.Cfg)
556
557 entry.Status = StatusDisabled
558 h.api.SendCodef(fn, 200, "%s", takeCommandMessage(h.cb))
559 h.NotifyJobStatus(entry.Cfg, StatusDisabled)
560 h.cb.OnStatusChange(entry, oldStatus, fn)
561 }
562
563 // CmdRemove handles the "remove" command.
564 func (h *Handler[C]) CmdRemove(fn Function) {
565 key, _, ok := h.cb.ExtractKey(fn)
566 if !ok {
567 h.api.SendCodef(fn, 400, "invalid job ID format.")
568 return
569 }
570
571 entry, ok := h.exposed.LookupByKey(key)
572 if !ok {
573 h.api.SendCodef(fn, 404, "job not found.")
574 return
575 }
576
577 if entry.Cfg.SourceType() != "dyncfg" {
578 h.api.SendCodef(fn, 405, "removing jobs of type '%s' is not supported, only 'dyncfg' jobs can be removed.", entry.Cfg.SourceType())
579 return
580 }
581
582 h.seen.Remove(entry.Cfg)
583 h.exposed.Remove(entry.Cfg)
584 h.cb.Stop(entry.Cfg)
585
586 h.api.SendCodef(fn, 200, "%s", takeCommandMessage(h.cb))
587 h.NotifyJobRemove(entry.Cfg)
588 }
589
590 // CmdUpdate handles the "update" command.
591 func (h *Handler[C]) CmdUpdate(fn Function) {
592 key, name, ok := h.cb.ExtractKey(fn)
593 if !ok {
594 h.api.SendCodef(fn, 400, "invalid job ID format.")
595 return
596 }
597
598 entry, ok := h.exposed.LookupByKey(key)
599 if !ok {
600 h.api.SendCodef(fn, 404, "job not found.")
601 return
602 }
603
604 if err := fn.ValidateHasPayload(); err != nil {
605 h.api.SendCodef(fn, 400, "%v", err)
606 return
607 }
608
609 newCfg, err := h.cb.ParseAndValidate(fn, name)
610 if err != nil {
611 h.api.SendCodef(fn, 400, "%v", err)
612 h.NotifyJobStatus(entry.Cfg, entry.Status)
613 return
614 }
615
616 isConversion := entry.Cfg.SourceType() != "dyncfg"
617
618 // No-op: running dyncfg config with same hash.
619 if !isConversion && entry.Status == StatusRunning && entry.Cfg.Hash() == newCfg.Hash() {
620 h.api.SendCodef(fn, 200, "")
621 h.NotifyJobStatus(entry.Cfg, StatusRunning)
622 return
623 }
624
625 if entry.Status == StatusAccepted {
626 h.api.SendCodef(fn, 403, "updating is not allowed in '%s' state.", entry.Status)
627 h.NotifyJobStatus(entry.Cfg, StatusAccepted)
628 return
629 }
630
631 oldStatus := entry.Status
632 oldCfg := entry.Cfg
633
634 // For conversion: stop old before cache update (matching jobmgr line 681).
635 if isConversion {
636 h.cb.Stop(oldCfg)
637 }
638
639 // Update caches.
640 if !isConversion {
641 h.seen.Remove(oldCfg)
642 }
643 h.seen.Add(newCfg)
644 newEntry := &Entry[C]{Cfg: newCfg, Status: StatusAccepted}
645 h.exposed.Add(newEntry)
646
647 // Preserve Disabled status.
648 if oldStatus == StatusDisabled {
649 newEntry.Status = StatusDisabled
650 if isConversion {
651 h.NotifyJobCreate(newCfg, StatusDisabled)
652 }
653 h.api.SendCodef(fn, 200, "%s", takeCommandMessage(h.cb))
654 h.NotifyJobStatus(newCfg, StatusDisabled)
655 h.cb.OnStatusChange(newEntry, oldStatus, fn)
656 return
657 }
658
659 // Start or update.
660 if isConversion {
661 err = h.cb.Start(newCfg)
662 } else {
663 err = h.cb.Update(oldCfg, newCfg)
664 }
665
666 if err != nil {
667 if !isConversion && errors.Is(err, ErrNonDisruptiveUpdate) {
668 // Update failed before runtime disruption; rollback to old cache state.
669 h.seen.Remove(newCfg)
670 h.seen.Add(oldCfg)
671 h.exposed.Add(entry)
672
673 h.api.SendCodef(fn, 200, "%v", err)
674 h.NotifyJobStatus(oldCfg, oldStatus)
675 // No OnStatusChange call here: effective state did not change.
676 return
677 }
678
679 newEntry.Status = StatusFailed
680 if isConversion {
681 h.NotifyJobCreate(newCfg, StatusFailed)
682 }
683 h.api.SendCodef(fn, 200, "%v", err)
684 h.NotifyJobStatus(newCfg, StatusFailed)
685 h.cb.OnStatusChange(newEntry, oldStatus, fn)
686 return
687 }
688
689 newEntry.Status = StatusRunning
690 if isConversion {
691 h.NotifyJobCreate(newCfg, StatusRunning)
692 }
693 h.api.SendCodef(fn, 200, "%s", takeCommandMessage(h.cb))
694 h.NotifyJobStatus(newCfg, StatusRunning)
695 h.cb.OnStatusChange(newEntry, oldStatus, fn)
696 }
697
698 // CmdRestart handles the "restart" command.
699 // Stops the existing work and starts the same config again.
700 // Only allowed for Running/Failed configs (rejects Accepted/Disabled).
701 func (h *Handler[C]) CmdRestart(fn Function) {
702 key, _, ok := h.cb.ExtractKey(fn)
703 if !ok {
704 h.api.SendCodef(fn, 400, "invalid job ID format.")
705 return
706 }
707
708 entry, ok := h.exposed.LookupByKey(key)
709 if !ok {
710 h.api.SendCodef(fn, 404, "job not found.")
711 return
712 }
713
714 switch entry.Status {
715 case StatusAccepted, StatusDisabled:
716 h.api.SendCodef(fn, 405, "restarting is not allowed in '%s' state.", entry.Status)
717 h.NotifyJobStatus(entry.Cfg, entry.Status)
718 return
719 case StatusRunning, StatusFailed:
720 // proceed
721 default:
722 h.api.SendCodef(fn, 405, "restarting is not allowed in '%s' state.", entry.Status)
723 h.NotifyJobStatus(entry.Cfg, entry.Status)
724 return
725 }
726
727 oldStatus := entry.Status
728
729 h.cb.Stop(entry.Cfg)
730
731 err := h.cb.Start(entry.Cfg)
732
733 if err != nil {
734 entry.Status = StatusFailed
735 code := 422
736 var ce CodedError
737 if errors.As(err, &ce) {
738 code = ce.Code()
739 }
740 h.api.SendCodef(fn, code, "job restart failed: %v", err)
741 h.NotifyJobStatus(entry.Cfg, StatusFailed)
742 h.cb.OnStatusChange(entry, oldStatus, fn)
743 return
744 }
745
746 entry.Status = StatusRunning
747 h.api.SendCodef(fn, 200, "")
748 h.NotifyJobStatus(entry.Cfg, StatusRunning)
749 h.cb.OnStatusChange(entry, oldStatus, fn)
750 }
751
752 func isCodedError(err error) bool {
753 var ce CodedError
754 return errors.As(err, &ce)
755 }