v2 / vlib / context / cause.v
202 lines · 187 sloc · 6.58 KB · e39b793809e5ee2606d9613bbc3785aa08988cee
Raw
1// This module defines the Context type, which carries deadlines, cancellation signals,
2// and other request-scoped values across API boundaries and between processes.
3// Based on: https://github.com/golang/go/tree/master/src/context
4// Last commit: https://github.com/golang/go/commit/52bf14e0e8bdcd73f1ddfb0c4a1d0200097d3ba2
5module context
6
7import rand
8import time
9
10// cause_context_key is the value-context key used to store the cause in a CauseContext.
11const cause_context_key = Key('context.CauseContext')
12
13// CancelCauseFunc is a cancel function that accepts an optional cause error.
14// Calling it with a non-none cause records that cause; calling it with none
15// is equivalent to calling a plain CancelFn.
16pub type CancelCauseFunc = fn (cause IError)
17
18// A CauseContext wraps a CancelContext and records a cause error set via
19// CancelCauseFunc. It implements the Context interface by delegating to the
20// embedded CancelContext.
21@[heap]
22struct CauseContext {
23 id string
24 deadline time.Time
25mut:
26 cancel_ctx CancelContext
27 cause IError = none
28}
29
30// with_cancel_cause behaves like with_cancel but returns a CancelCauseFunc
31// instead of a CancelFn. Calling the returned function with a non-none error
32// records that error as the cause, retrievable via cause(ctx). Calling it with
33// none is identical to a plain cancel.
34//
35// Example:
36// mut bg := context.background()
37// mut ctx, cancel := context.with_cancel_cause(mut bg)
38// defer { cancel(none) }
39// cancel(my_err)
40// assert context.cause(ctx).str() == my_err.str()
41pub fn with_cancel_cause(mut parent Context) (Context, CancelCauseFunc) {
42 id := rand.uuid_v4()
43 inner := new_cancel_context(parent)
44 mut ctx := &CauseContext{
45 id: id
46 cancel_ctx: inner
47 }
48 propagate_cancel(mut parent, mut ctx)
49 cancel_fn := fn [mut ctx] (cause IError) {
50 if cause !is none {
51 ctx.cancel_ctx.mutex.lock()
52 if ctx.cause is none {
53 ctx.cause = cause
54 }
55 ctx.cancel_ctx.mutex.unlock()
56 }
57 ctx.cancel_ctx.cancel(true, canceled)
58 }
59 return Context(ctx), CancelCauseFunc(cancel_fn)
60}
61
62// with_deadline_cause behaves like with_deadline but accepts a cause error that
63// is recorded when the deadline fires (instead of the generic deadline_exceeded).
64// If cancel is called before the deadline, that cancellation takes effect instead.
65pub fn with_deadline_cause(mut parent Context, d time.Time, deadline_cause IError) (Context, CancelFn) {
66 if cur := parent.deadline() {
67 if cur < d {
68 return with_cancel(mut parent)
69 }
70 }
71 inner := new_cancel_context(parent)
72 id := rand.uuid_v4()
73 mut ctx := &CauseContext{
74 id: id
75 cancel_ctx: inner
76 deadline: d
77 }
78 propagate_cancel(mut parent, mut ctx)
79 dur := d - time.now()
80 if dur.nanoseconds() <= 0 {
81 // deadline already passed
82 ctx.cause = deadline_cause
83 ctx.cancel_ctx.cancel(false, deadline_exceeded)
84 cancel_fn := fn [mut ctx] () {
85 ctx.cancel_ctx.cancel(true, canceled)
86 }
87 return Context(ctx), CancelFn(cancel_fn)
88 }
89 spawn fn (mut ctx CauseContext, dur time.Duration, deadline_cause IError) {
90 time.sleep(dur)
91 ctx.cancel_ctx.mutex.lock()
92 // Only record deadline cause if the context wasn't already canceled,
93 // so that the first cancellation's cause (manual or from parent) wins.
94 if ctx.cancel_ctx.err is none {
95 ctx.cause = deadline_cause
96 }
97 ctx.cancel_ctx.mutex.unlock()
98 ctx.cancel_ctx.cancel(true, deadline_exceeded)
99 }(mut ctx, dur, deadline_cause)
100 cancel_fn := fn [mut ctx] () {
101 ctx.cancel_ctx.cancel(true, canceled)
102 }
103 return Context(ctx), CancelFn(cancel_fn)
104}
105
106// with_timeout_cause is a convenience wrapper around with_deadline_cause using
107// a relative duration instead of an absolute time.
108pub fn with_timeout_cause(mut parent Context, timeout time.Duration, deadline_cause IError) (Context, CancelFn) {
109 return with_deadline_cause(mut parent, time.now().add(timeout), deadline_cause)
110}
111
112// cause returns the cause of the context's cancellation.
113// If ctx was canceled via CancelCauseFunc with a non-none cause, that cause is
114// returned. Otherwise, cause returns ctx.err() (i.e. canceled or
115// deadline_exceeded).
116pub fn cause(ctx Context) IError {
117 mut mctx := ctx
118 if val := mctx.value(cause_context_key) {
119 match val {
120 CauseContext {
121 val.cancel_ctx.mutex.lock()
122 c := val.cause
123 val.cancel_ctx.mutex.unlock()
124 if c !is none {
125 return c
126 }
127 }
128 else {}
129 }
130 }
131 return mctx.err()
132}
133
134// --- CauseContext implements Context and Canceler ---
135
136// deadline returns the deadline for the context, or none if no deadline is set.
137pub fn (ctx &CauseContext) deadline() ?time.Time {
138 if ctx.deadline != time.Time{} {
139 return ctx.deadline
140 }
141 return none
142}
143
144// done returns a channel that is closed when the context is canceled.
145pub fn (mut ctx CauseContext) done() chan int {
146 return ctx.cancel_ctx.done()
147}
148
149// err returns the error that canceled the context, or none if not canceled.
150pub fn (mut ctx CauseContext) err() IError {
151 return ctx.cancel_ctx.err()
152}
153
154// value returns the cause context itself when the key matches cause_context_key,
155// otherwise delegates to the embedded cancel context.
156pub fn (ctx &CauseContext) value(key Key) ?Any {
157 if key == cause_context_key {
158 return ctx
159 }
160 return ctx.cancel_ctx.value(key)
161}
162
163// str returns a string representation of the context (e.g. 'EmptyContext.with_cancel_cause').
164pub fn (ctx &CauseContext) str() string {
165 return context_name(ctx.cancel_ctx.context) + '.with_cancel_cause'
166}
167
168// cancel cancels the context. If remove_from_parent is true, it also removes
169// this context from its parent's children list. The err is recorded as the cause.
170pub fn (mut ctx CauseContext) cancel(remove_from_parent bool, err IError) {
171 // If being canceled with the generic 'context canceled' error and we
172 // have a parent, propagate the parent's cause so children can retrieve it.
173 // We walk the value chain directly instead of calling cause(): cause()
174 // would fall through to err() on a parent CancelContext, which would
175 // deadlock here because this cancel is typically invoked from within a
176 // parent's cancel() while that parent's mutex is held. The cause field
177 // on a CauseContext is always assigned before its cancel propagates, so
178 // reading it without locking is safe along this downward path.
179 if err.str() == 'context canceled' {
180 match ctx.cancel_ctx.context {
181 EmptyContext {}
182 else {
183 mut parent := ctx.cancel_ctx.context
184 if val := parent.value(cause_context_key) {
185 match val {
186 CauseContext {
187 if val.cause !is none {
188 ctx.cause = val.cause
189 }
190 }
191 else {}
192 }
193 }
194 }
195 }
196 }
197 ctx.cancel_ctx.cancel(false, err)
198 if remove_from_parent {
199 mut cc := &ctx.cancel_ctx.context
200 remove_child(mut cc, ctx)
201 }
202}
203