-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathruntime.go
122 lines (102 loc) · 2.6 KB
/
runtime.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package lemon
import (
"context"
"fmt"
"time"
)
// HookRuntime handles Hook lifecycle for the Engine.
//
// Under the hood it use flag and channel to communicate Hook's state. Using this with goroutine for either Start()
// or Stop() protect the Engine from a lock with a Hook blocking forever with either Start() and/or Stop().
//
// Unfortunately, this design increase the use of goroutine.
// However, it offer a great warranty of removing any blocking or deadlock issue while a shutdown is required.
type HookRuntime struct {
// chan used by "stop" goroutine.
c0 chan error
// chan used by "start" goroutine.
c1 chan error
// wait flag for c0.
w0 bool
// wait flag for c1.
w1 bool
}
func (hr *HookRuntime) start(ctx context.Context, h Hook) {
go func() {
defer func() {
err := recover()
if err != nil {
hr.c1 <- fmt.Errorf("lemon startup failed: %s", err)
}
}()
hr.c1 <- h.Start(ctx)
}()
}
func (hr *HookRuntime) stop(ctx context.Context, h Hook) {
go func() {
defer func() {
err := recover()
if err != nil {
hr.c0 <- fmt.Errorf("lemon shutdown failed: %s", err)
}
}()
hr.c0 <- h.Stop(ctx)
}()
}
func (hr *HookRuntime) init() {
if hr.c0 == nil {
hr.c0 = make(chan error, 1)
}
if hr.c1 == nil {
hr.c1 = make(chan error, 1)
}
hr.w0 = true
hr.w1 = true
}
// WaitForEvent will block until a shutdown of the given Hook is required.
// Also, if an error is returned, the Engine will shutdown every Hook.
func (hr *HookRuntime) WaitForEvent(ctx context.Context, h Hook) error {
hr.init()
hr.start(ctx, h)
// Either context was cancelled, or an error has occurred during Hook startup.
select {
case <-ctx.Done():
// Engine's context was cancelled.
hr.stop(ctx, h)
return nil
case err := <-hr.c1:
// Forward that c1 has stopped on shutdown.
hr.c1 <- nil
// If an error has occurred during Hook startup, we have to ignore Hook shutdown.
if err != nil {
hr.w0 = false
}
return err
}
}
// Shutdown will gracefully shutdown the given hook, or kill it after timeout.
// It will also synchronise that Start() and Stop() have finished.
func (hr *HookRuntime) Shutdown(timeout time.Duration) []error {
t := time.Now()
failures := []error{}
// Wait for previous hook to gracefully shutdown, or kill it after timeout.
for {
select {
case err := <-hr.c1:
if err != nil {
failures = append(failures, err)
}
hr.w1 = false
case err := <-hr.c0:
if err != nil {
failures = append(failures, err)
}
hr.w0 = false
case <-time.After(timeout - time.Since(t)):
return failures
}
if !hr.w1 && !hr.w0 {
return failures
}
}
}