-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathHow-to-correctly-use-package-context.slide
348 lines (244 loc) · 9.81 KB
/
How-to-correctly-use-package-context.slide
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
How to correctly use package context
Advice and pitfalls
17 Aug 2017
Jack Lindamood
Software Engineer, Twitch
: https://www.calhoun.io/pitfalls-of-context-values-and-how-to-avoid-or-mitigate-them/
: https://faiface.github.io/post/context-should-go-away-go2/
* About the author
- Writing Go for 4 years
- Currently software engineer at Twitch
- Most backend at Twitch powered by Go
- Hundred(s) of repositories
.link https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39 How to correctly use context.Context in Go 1.7
* Talk Sections
- Why context exists
- Request cancellation
- Request scoped values
* Why context exists
* Problem that created context.Context
- Every long request should have a timeout
- Need to propagate that timeout across the request
- Let's say it's 3 seconds at the start of the request
- How much time is left in the middle of the request?
- Need to store that information somewhere so the middle of the request can stop
* Problem expanded
- What if one request requires multiple RPC calls to resolve
- If one of those RPC calls fails, it may be worth failing the whole request
.image images/normal_request.png
* Good request
.image images/good_request.png
- Everything works, finishes in 40ms
* Failed request
.image images/bad_request.png
- RPC2 failed, still took 40ms
* Failed request end early
.image images/bad_request_dangle.png
- If RPC2 failed, just end early. Dangling RPC3
* Failed request ideal
.image images/bad_request_ideal.png
- If RPC2 fails, tell RPC 3 and 4 to end early
* Solution
- Use object to signal when a request is over
- Includes hints on when the request is expected to end
- Channels naturally report when the request is done
* Let's throw variables in there too
- No concept of thread/goroutine specific variables in Go
- Reasonable, since it becomes tricky when goroutines depend upon other goroutines
- Since context is threaded everywhere, throw variables on it to as a grab bag of information
- Very easy to abuse
* context.Context implementation details
- Tree of immutable context nodes
- Cancellation of a node cancels all sub nodes
- Context values are a node
- Value lookup goes backwards up the tree
* Example context chain
.code code/ctxtree.go
.image images/context_tree.png
* Context tree at 3 sec
.image images/context_tree_3s.png
* Context tree at 5 sec
.image images/context_tree_5s.png
* context.Context API
- 3 functions on when to end your sub-request
- 1 function on request scoped variables
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
* When should you use context.Context?
- Every RPC call should have an ability to time out
- This is just reasonable API design
- Not just _timeout_, since you also need to be able to end an RPC call early if the result is no longer needed.
- context.Context is the Go standard solution
- Any function that can block or take a long time to finish should have a context.Context
* How to create a context
- Use `context.Background()` at the beginning of an RPC request
- If you don't have a context, and need to call a context function, use `context.TODO()`
- Give sub requests their own sub context if they need other timeouts
.image images/context_tree.png
* How to integrate context.Context
- As the first variable of a function call
func (d* Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)
- As an optional value on a request struct
func (r *Request) WithContext(ctx context.Context) *Request
- The variable name should probably be `ctx`
* Where to put a context
- Think of a context flowing through your program, like water in a river
- Ideally, context lives with the call stack
- Do not store the context in a struct
- Only exception is when the struct is a *request* struct (`http.Request`)
- Request structs should end with a request
- Context instances should be unreferenced when the RPC call is finished
- Context dies when the request dies
* Context package caveats
- Create pattern of closing contexts
- Especially timeout contexts
- If a context GCs, and isn't canceled, you probably did something wrong
ctx, cancel := context.WithTimeout(parentCtx, time.Second)
// Uses time.AfterFunc
// Will not garbage collect before timer expires
defer cancel()
// Good pattern to defer cancel() after creation
* Request cancellation
* When to cancel a context early
- When you don't care about spawned logic
- golang.org/x/sync/errgroup as example
- errgroup uses context to create an RPC cancellation abstraction
- Great deep dive into ideal context usage
* Example usage (golang.org/x/sync/errgroup)
.code code/makeeg.go
: Make two requests concurrently
: Time out each at 1 sec
: If either fails or 5XX, end the other early
- Return a context that *Group* can cancel early
* Example usage (golang.org/x/sync/errgroup)
.code code/doeg1.go
- Execute a function concurrently
* Example usage (golang.org/x/sync/errgroup)
.code code/doeg2.go
- If that function fails
* Example usage (golang.org/x/sync/errgroup)
.code code/doeg3.go
- Cancel the returned context once with `sync.Once`
* Example usage (golang.org/x/sync/errgroup)
.code code/doegwait.go
- Notice cleanup `cancel()` after Wait
* Example usage (errgroup)
.code code/egexample.go
: A lot going on here
* Request scoped values
* context.Value: the API duct tape
- Creates `Value` nodes in the context chain
.code code/ctxvalue.go
: Duct tape fixes everything
* Scope your keyspace
- Private type
- Instance of private type
- Get/With that uses private instance
- `WithXYZ` over `SetXYZ`
.code code/ctxstore1.go
: Be wary of vendor directory issues
: Rec WithXYZ over SetXYZ
* Context.Value should be immutable
- context.Context is designed immutable
- Keep this with context.Value
- Do not store a value that, if changed, the change is seen by other contexts
- Remember this when you use context.Value
* What to put in context.Value
- Everything about a context should be request scoped
- Includes context.Value
- What is a request scoped value?
- Derived from request data and goes away when the request is over
* What things are clearly not request scoped
- Objects made outside the request and not changed with the request
- Database connection
- But what if you put the user ID on the connection?
- Global logger
- But what if you put the user ID on the logger?
* What's the problem with context.Value?
- Unfortunately, almost everything is derived from the request
- Why bother having function parameters.
- Just accept a context?
- Think about it: what isn't derived from the request?
// Who needs other function parameters?
func Add(ctx context.Context) int {
return ctx.Value("first").(int) + ctx.Value("second").(int)
}
* Why I dislike context.Value
- Function parameters clearly tell you what a function needs
func IsAdminUser(ctx context.Context) bool {
userID := GetUserID(ctx)
return authSingleton.IsAdmin(userID)
}
* What if we changed the function signature?
func IsAdminUser(ctx context.Context) bool
func IsAdminUser(ctx context.Context, userID string, authenticator auth.Service) bool
- What does this function signature tell us?
- This function can end early
- This function takes a user ID
- This function uses an authenticator on the userID
- What do I need to change to test this function?
- stub out authenticator
- modify the userID
- All of this from the signature
* Which function is easier to refactor?
- If it takes just a context, I need to make sure the userID is there wherever I use it
- If it takes what it needs, then I know what to modify
* So what is ok to put in context.Value?
- context.Value should inform, not control
- Should never be required input for documented results
- If your function can't behave correctly because of what `context.Value` has, you're obscuring your API too heavily
* What things usually don't control
- Request ID
- Often given to each RPC request.
- The logic of the request is almost never gated on what the ID is
- Logging
- The logger itself is not request scoped, so should not sit on the context
- Logging decoration can be request scoped, so can sit on the context
- User ID (if used only for logging)
- Incoming request ID
- The non request scoped logger, can use the context to decorate logs
* Things that clearly control
- Database connection
- Controls logic very heavily
- Explicitly call it out as a parameter
- Authentication
- Obviously controls logic
- Very important to how a function works
- Call it out explicitly
* Creative debugging with context.Value
- net/http/httptrace
.link https://medium.com/@cep21/go-1-7-httptrace-and-context-debug-patterns-608ae887224a Go 1.7 httptrace and context debug patterns
.code code/httptrace.go
: https://medium.com/@cep21/go-1-7-httptrace-and-context-debug-patterns-608ae887224a
* How net/http uses httptrace
- If a trace is attached, execute trace callbacks
- Inform, not control
.code code/httptrace2.go
* Dodgy dependency injections (github.com/golang/oauth2)
- They use `ctx.Value` to locate dependencies
- Recommend not doing this
.code code/oauth.go
* Reasons people abuse context.Value
- Middleware abstractions
- Deep callstacks
- Spaghetti designs
- context.Value doesn't make your API cleaner, it makes it more obscured
: Singletons also make your API smaller
* Summary of context.Value
- Great for debugging information
- Required context.Value parts obscure your API
- Be explicit if possible
- Just try not to use it
* Summary of Context
- All long/blocking operations should take a `Context`
- errgroup is a great abstraction on top of `Context`
- Cancel `Context` when done
- `Context.Value` should inform, not control
- Scope `Context.Value` keys
- `Context` (and `Value`) should be immutable and thread safe
- Context dies with the request