forked from posener/context
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontext.go
216 lines (179 loc) · 5.54 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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
/*
Package context is a proof of concept implementation of **scoped context**,
proposed in (this blog post) https://posener.github.io/goroutine-scoped-context.
This library should not be used for production code.
Usage
The context package should be imported from `github.com/posener/context`.
import (
- "context"
+ "github.com/posener/context"
)
Since this implementation does not involve changes to the runtime,
the goroutine context must be initialized.
func main() {
+ context.Init()
// Go code goes here.
}
Functions should not anymore receive the context in the first argument.
They should get it from the goroutine scope.
-func foo(ctx context.Context) {
+func foo() {
+ ctx := context.Get()
// Use context.
}
Applying context to a scope:
unset := context.Set(ctx)
// ctx is applied until unset is called, or a deeper `Set` call.
unset()
Or:
defer context.Set(ctx)()
// ctx is applied until the end of the function or a deeper `Set` call.
Invoking goroutines should be done with `context.Go` or `context.GoCtx`
Running a new goroutine with the current stored context:
-go foo()
+context.Go(foo)
More complected functions:
-go foo(1, "hello")
+context.Go(func() { foo(1, "hello") })
Running a goroutine with a new context:
// `ctx` is the context that we want to have in the invoked goroutine
context.GoCtx(ctx, foo)
`context.TODO` should not be used anymore:
-f(context.TODO())
+f(context.Get())
*/
package context
import (
stdctx "context"
"sync"
"github.com/noke-inc/lib_context/runtime"
)
type (
Context = stdctx.Context
CancelFunc = stdctx.CancelFunc
)
var (
WithCancel = stdctx.WithCancel
WithTimeout = stdctx.WithTimeout
WithDeadline = stdctx.WithDeadline
WithValue = stdctx.WithValue
Background = stdctx.Background
DeadlineExceeded = stdctx.DeadlineExceeded
Canceled = stdctx.Canceled
)
var (
// storage is used instead of goroutine local storage to
// store goroutine(ID) to Context mapping.
storage map[uint64][]Context
// mutex for locking the storage map.
mu sync.RWMutex
)
func init() {
storage = make(map[uint64][]Context)
Init()
}
// peek simulates fetching of context from goroutine local storage
// It gets the context from `storage` map according to the current
// goroutine ID.
// If the goroutine ID is not in the map, it panic. This case
// may occur when a user did not use the `context.Go` or `context.GoCtx`
// to invoke a goroutine.
// Note: real goroutine local storage won't need the implemented locking
// exists in this implementation, since the storage won't be accessible from
// different goroutines.
func peek() Context {
id := runtime.GID()
mu.RLock()
defer mu.RUnlock()
stack := storage[id]
if stack == nil {
panic("goroutine ran without using context.Go or context.GoCtx")
}
return stack[len(stack)-1]
}
// push simulates storing of context in the goroutine local storage.
// It gets the context to push to the context stack, and returns a pop function.
// Note: real goroutine local storage won't need the implemented locking
// exists in this implementation, since the storage won't be accessible from
// different goroutines.
func push(ctx Context) func() {
id := runtime.GID()
mu.Lock()
defer mu.Unlock()
storage[id] = append(storage[id], ctx)
size := len(storage[id])
return func() { pop(id, size) }
}
// pop simulates removal of a context from the thread local storage.
// If the stack is emptied, it will be removed from the storage map.
// Note: real goroutine local storage won't need the implemented locking
// exists in this implementation, since the storage won't be accessible from
// different goroutines.
func pop(id uint64, stackSize int) {
mu.Lock()
defer mu.Unlock()
if len(storage[id]) != stackSize {
if len(storage[id]) < stackSize {
panic("multiple call for unset")
}
panic("there are contexts that should be unset before")
}
storage[id] = storage[id][:len(storage[id])-1]
// Remove the stack from the map if it was emptied
if len(storage[id]) == 0 {
delete(storage, id)
}
}
// Init creates the first background context in a program.
// it should be called once, in the beginning of the main
// function or in init() function.
// It returns the created context.
// All following goroutine invocations should be replaced
// by context.Go or context.GoCtx.
//
// Note:
// This function won't be needed in the real implementation.
func Init() Context {
ctx := Background()
push(ctx)
return ctx
}
// Get gets the context of the current goroutine
// It may panic if the current go routine did not ran with
// context.Go or context.GoCtx.
//
// Note:
// This function won't panic in the real implementation.
func Get() Context {
return peek()
}
// Set creates a context scope.
// It returns an "unset" function that should invoked at the
// end of this context scope. In any case, it must be invoked,
// exactly once, and in the right order.
func Set(ctx Context) func() {
return push(ctx)
}
// Go invokes f in a new goroutine and takes care of propagating
// the current context to the created goroutine.
// It may panic if the current goroutine was not invoked with
// context.Go or context.GoCtx.
//
// Note:
// In the real implementation, this should be the behavior
// of the `go` keyword. It will also won't panic.
func Go(f func()) {
GoCtx(peek(), f)
}
// GoCtx invokes f in a new goroutine with the given context.
//
// Note:
// In the real implementation, accepting the context argument
// should be incorporated into the behavior of the `go` keyword.
func GoCtx(ctx Context, f func()) {
go func() {
pop := push(ctx)
defer pop()
f()
}()
}