| 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 |
| 5 | module context |
| 6 | |
| 7 | import rand |
| 8 | import time |
| 9 | |
| 10 | // cause_context_key is the value-context key used to store the cause in a CauseContext. |
| 11 | const 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. |
| 16 | pub 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] |
| 22 | struct CauseContext { |
| 23 | id string |
| 24 | deadline time.Time |
| 25 | mut: |
| 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() |
| 41 | pub 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. |
| 65 | pub 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. |
| 108 | pub 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). |
| 116 | pub 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. |
| 137 | pub 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. |
| 145 | pub 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. |
| 150 | pub 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. |
| 156 | pub 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'). |
| 164 | pub 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. |
| 170 | pub 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 | |