-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontext.go
186 lines (151 loc) · 4.62 KB
/
context.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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// Package implements a graceful shutdown context tree for your goroutines.
//
// It means that parent context wouldn't close until all its children's worked.
//
// # example:
//
// You are creating a context tree:
//
// root => child1 => child2 => child3
//
// and trying to close the root.
//
// All subchilds will close in reverse order (first - child3, then child2, child1, root).
// This closing order is absolutely essential because the child context could use some parent resources or send some signals to the parent. If a parent closes before the child, it will cause undefined behavior or goroutine locking.
//
// Unfortunately, context from the standard Go library does not guarantee this closing order.
// See issue: https://github.com/golang/go/issues/51075
//
// This module resolves this problem and guarantees a correct closing order.
package context
import (
"sync"
)
// Instances of this interface are sent to your node through the Go() method.
//
// (see [ContextedInstance])
type Context interface {
// creates a new child context, for instance, what implements ContextedInstance interface
NewContextFor(instance ContextedInstance) (ChildContext, error)
// When this channel closes, it means that the child context should exit from the Go function.
Context() chan struct{}
// Close the current context and all children in reverse order.
Close()
}
type contextState int
const (
notStarted contextState = 0
working contextState = 1
freezed contextState = 2
disposing contextState = 3
)
type context struct {
parents map[*context]*context
childs map[*context]*context
instance ContextedInstance
state contextState
isOpened chan struct{}
root *root
}
type root struct {
ready sync.Mutex
contexts map[ContextedInstance]*context
}
func newEmptyContext() *context {
newContext := &context{
parents: map[*context]*context{},
childs: map[*context]*context{},
instance: nil,
state: working,
isOpened: make(chan struct{}),
root: &root{
contexts: make(map[ContextedInstance]*context),
},
}
return newContext
}
// NewContextFor ...
func (parent *context) NewContextFor(instance ContextedInstance) (ChildContext, error) {
parent.root.ready.Lock()
defer parent.root.ready.Unlock()
switch parent.state {
case freezed:
return nil, &ClosingIsInProcessForFreezeError{}
case disposing:
return nil, &ClosingIsInProcessForDisposingError{}
}
return newContextFor(parent, instance)
}
func newContextFor(parent *context, instance ContextedInstance) (*context, error) {
newContext := parent.root.contexts[instance]
// if context not yet added to tree before, create new one
if newContext == nil {
newContext = &context{
parents: map[*context]*context{},
childs: map[*context]*context{},
instance: instance,
state: notStarted,
isOpened: make(chan struct{}),
root: parent.root,
}
}
newContext.parents[parent] = parent
parent.childs[newContext] = newContext
parent.root.contexts[instance] = newContext
if newContext.state == notStarted {
newContext.state = working
// Start new Context
go func(current *context) {
// execure user context select {...}
current.instance.Go(current)
{
current.root.ready.Lock()
if current.state != disposing {
// Goroutine exits without a Cancel() call, just clean it from all children. If a child has no other parents (closing last parent), initiate child closing.
for child := range current.childs {
delete(child.parents, current)
//fmt.Printf("[%v]\n", len(child.parents))
if len(child.parents) == 0 {
child.freezeAllChildsAndSubchilds()
}
}
}
// Remove node from parent childs and if parent is freezed and empty, initiate it disposing
delete(current.root.contexts, instance)
if current.parents != nil {
for parent := range current.parents {
delete(parent.childs, current)
if parent.state == freezed && len(parent.childs) == 0 {
parent.state = disposing
close(parent.isOpened)
}
}
}
current.root.ready.Unlock()
}
}(newContext)
}
return newContext, nil
}
// Context ...
func (context *context) Context() chan struct{} {
return context.isOpened
}
// Close ...
func (current *context) Close() {
current.root.ready.Lock()
defer current.root.ready.Unlock()
current.freezeAllChildsAndSubchilds()
}
func (current *context) freezeAllChildsAndSubchilds() {
if current.state == working {
current.state = freezed
for child := range current.childs {
child.freezeAllChildsAndSubchilds()
}
}
if current.state == freezed && len(current.childs) == 0 {
current.state = disposing
close(current.isOpened)
}
}