From 07a6f4e445e763c8580942811b9adb8066e1fb49 Mon Sep 17 00:00:00 2001 From: Ulises Jeremias Cornejo Fandos Date: Mon, 12 Apr 2021 13:32:51 -0300 Subject: [PATCH] context: add a new `context` module, based on Golang's context, intended to be used in webservers (#9563) --- cmd/tools/vtest-self.v | 5 +- vlib/context/README.md | 188 ++++++++++++++++++++++++++++ vlib/context/_context.v | 77 ++++++++++++ vlib/context/cancel.v | 243 +++++++++++++++++++++++++++++++++++++ vlib/context/cancel_test.v | 39 ++++++ vlib/context/deadline.v | 92 ++++++++++++++ vlib/context/empty.v | 37 ++++++ vlib/context/empty_test.v | 19 +++ vlib/context/value.v | 56 +++++++++ vlib/context/value_test.v | 23 ++++ 10 files changed, 778 insertions(+), 1 deletion(-) create mode 100644 vlib/context/README.md create mode 100644 vlib/context/_context.v create mode 100644 vlib/context/cancel.v create mode 100644 vlib/context/cancel_test.v create mode 100644 vlib/context/deadline.v create mode 100644 vlib/context/empty.v create mode 100644 vlib/context/empty_test.v create mode 100644 vlib/context/value.v create mode 100644 vlib/context/value_test.v diff --git a/cmd/tools/vtest-self.v b/cmd/tools/vtest-self.v index 7849b850aa..7b7295cba2 100644 --- a/cmd/tools/vtest-self.v +++ b/cmd/tools/vtest-self.v @@ -30,7 +30,6 @@ const ( ] skip_with_fsanitize_undefined = []string{} skip_with_werror = [ - 'vlib/sync/array_rlock_test.v', 'vlib/clipboard/clipboard_test.v', 'vlib/eventbus/eventbus_test.v', 'vlib/gx/color_test.v', @@ -56,6 +55,10 @@ const ( 'vlib/strconv/atof_test.v', 'vlib/strconv/f32_f64_to_string_test.v', 'vlib/strconv/number_to_base_test.v', + 'vlib/context/value_test.v' /* the following tests need C casts in `sync` and/or thirdparty/stdatomic */, + 'vlib/context/empty_test.v', + 'vlib/context/cancel_test.v', + 'vlib/sync/array_rlock_test.v', 'vlib/sync/atomic2/atomic_test.v', 'vlib/sync/channel_2_test.v', 'vlib/sync/channel_1_test.v', diff --git a/vlib/context/README.md b/vlib/context/README.md new file mode 100644 index 0000000000..54eedb517f --- /dev/null +++ b/vlib/context/README.md @@ -0,0 +1,188 @@ +# Context + +This module defines the Context type, which carries deadlines, cancellation signals, +and other request-scoped values across API boundaries and between processes. + +Incoming requests to a server should create a Context, and outgoing calls to servers +should accept a Context. The chain of function calls between them must propagate the +Context, optionally replacing it with a derived Context created using with_cancel, +with_deadline, with_timeout, or with_value. When a Context is canceled, all Contexts +derived from it are also canceled. + +The with_cancel, with_deadline, and with_timeout functions take a Context (the parent) +and return a derived Context (the child) and a CancelFunc. Calling the CancelFunc +cancels the child and its children, removes the parent's reference to the child, +and stops any associated timers. Failing to call the CancelFunc leaks the child +and its children until the parent is canceled or the timer fires. + +Programs that use Contexts should follow these rules to keep interfaces consistent +across different modules. + +Do not store Contexts inside a struct type; instead, pass a Context explicitly +to each function that needs it. The Context should be the first parameter, +typically named ctx, just to make it more consistent. + +## Examples + +In this section you can see some usage examples for this module + +### Context With Cancellation + +```v +import context + +// This example demonstrates the use of a cancelable context to prevent a +// routine leak. By the end of the example function, the routine started +// by gen will return without leaking. +fn example_with_cancel() { + // gen generates integers in a separate routine and + // sends them to the returned channel. + // The callers of gen need to cancel the context once + // they are done consuming generated integers not to leak + // the internal routine started by gen. + gen := fn (mut ctx context.CancelerContext) chan int { + dst := chan int{} + go fn (mut ctx context.CancelerContext, dst chan int) { + ch := ctx.done() + loop: for i in 0 .. 5 { + select { + _ := <-ch { + // returning not to leak the routine + break loop + } + dst <- i {} + } + } + }(mut ctx, dst) + return dst + } + + mut ctx := context.with_cancel(context.background()) + defer { + context.cancel(mut ctx) + } + + ch := gen(mut ctx) + for i in 0 .. 5 { + v := <-ch + assert i == v + } +} +``` + +### Context With Deadline + +```v +import context +import time + +const ( + // a reasonable duration to block in an example + short_duration = 1 * time.millisecond +) + +fn after(dur time.Duration) chan int { + dst := chan int{} + go fn (dur time.Duration, dst chan int) { + time.sleep(dur) + dst <- 0 + }(dur, dst) + return dst +} + +// This example passes a context with an arbitrary deadline to tell a blocking +// function that it should abandon its work as soon as it gets to it. +fn example_with_deadline() { + dur := time.now().add(short_duration) + mut ctx := context.with_deadline(context.background(), dur) + + defer { + // Even though ctx will be expired, it is good practice to call its + // cancellation function in any case. Failure to do so may keep the + // context and its parent alive longer than necessary. + context.cancel(mut ctx) + } + + after_ch := after(1 * time.second) + ctx_ch := ctx.done() + select { + _ := <-after_ch { + assert false + } + _ := <-ctx_ch { + assert true + } + } +} +``` + +### Context With Timeout + +```v +import context +import time + +const ( + // a reasonable duration to block in an example + short_duration = 1 * time.millisecond +) + +fn after(dur time.Duration) chan int { + dst := chan int{} + go fn (dur time.Duration, dst chan int) { + time.sleep(dur) + dst <- 0 + }(dur, dst) + return dst +} + +// This example passes a context with a timeout to tell a blocking function that +// it should abandon its work after the timeout elapses. +fn example_with_timeout() { + // Pass a context with a timeout to tell a blocking function that it + // should abandon its work after the timeout elapses. + mut ctx := context.with_timeout(context.background(), short_duration) + defer { + context.cancel(mut ctx) + } + + after_ch := after(1 * time.second) + ctx_ch := ctx.done() + select { + _ := <-after_ch { + assert false + } + _ := <-ctx_ch { + assert true + } + } +} +``` + +### Context With Value + +```v +import context + +type ValueContextKey = string + +// This example demonstrates how a value can be passed to the context +// and also how to retrieve it if it exists. +fn example_with_value() { + f := fn (ctx context.ValueContext, key ValueContextKey) string { + if value := ctx.value(key) { + if !isnil(value) { + return *(&string(value)) + } + } + return 'key not found' + } + + key := ValueContextKey('language') + value := 'VAL' + ctx := context.with_value(context.background(), key, &value) + + assert value == f(ctx, key) + assert 'key not found' == f(ctx, ValueContextKey('color')) +} +``` diff --git a/vlib/context/_context.v b/vlib/context/_context.v new file mode 100644 index 0000000000..7675114f4d --- /dev/null +++ b/vlib/context/_context.v @@ -0,0 +1,77 @@ +module context + +import time + +pub const ( + background = EmptyContext(0) + todo = EmptyContext(1) + + cancel_context_key = 'context.CancelContext' + + // canceled is the error returned by Context.err when the context is canceled. + canceled = 'context canceled' + + // deadline_exceeded is the error returned by Context.err when the context's + // deadline passes. + deadline_exceeded = 'context deadline exceeded' +) + +pub interface Context { + // deadline returns the time when work done on behalf of this context + // should be canceled. deadline returns none when no deadline is + // set. Successive calls to deadline return the same results. + deadline() ?time.Time + // done returns a channel that's closed when work done on behalf of this + // context should be canceled. done may return nil if this context can + // never be canceled. Successive calls to done return the same value. + // The close of the done channel may happen asynchronously, + // after the cancel function returns. + // + // with_cancel arranges for done to be closed when cancel is called; + // with_deadline arranges for done to be closed when the deadline + // expires; with_timeout arranges for done to be closed when the timeout + // elapses. + done() chan int + // If done is not yet closed, err returns nil. + // If done is closed, err returns a non-nil error explaining why: + // canceled if the context was canceled + // or deadline_exceeded if the context's deadline passed. + // After err returns a non-nil error, successive calls to err return the same error. + err() string + // Value returns the value associated with this context for key, or nil + // if no value is associated with key. Successive calls to Value with + // the same key returns the same result. + // + // Use context values only for request-scoped data that transits + // processes and API boundaries, not for passing optional parameters to + // functions. + // + // A key identifies a specific value in a Context. Functions that wish + // to store values in Context typically allocate a key in a global + // variable then use that key as the argument to context.with_value and + // Context.Value. A key can be any type that supports equality; + // packages should define keys as an unexported type to avoid + // collisions. + value(key string) ?voidptr + str() string +} + +// background returns an empty Context. It is never canceled, has no +// values, and has no deadline. It is typically used by the main function, +// initialization, and tests, and as the top-level Context for incoming +// requests. +pub fn background() Context { + return Context(context.background) +} + +// todo returns an empty Context. Code should use todo when +// it's unclear which Context to use or it is not yet available (because the +// surrounding function has not yet been extended to accept a Context +// parameter). +pub fn todo() Context { + return Context(context.todo) +} + +fn context_name(ctx Context) string { + return typeof(ctx) +} diff --git a/vlib/context/cancel.v b/vlib/context/cancel.v new file mode 100644 index 0000000000..7a467054ea --- /dev/null +++ b/vlib/context/cancel.v @@ -0,0 +1,243 @@ +module context + +import rand +import sync +import time + +pub interface Canceler { + id string + cancel(remove_from_parent bool, err string) + done() chan int + str() string +} + +pub fn cancel(mut ctx CancelerContext) { + match mut ctx { + CancelContext { + ctx.cancel(true, canceled) + } + TimerContext { + ctx.cancel(true, canceled) + } + } +} + +// CancelerContext implements the Canceler intarface for both +// struct types: CancelContext and TimerContext +pub type CancelerContext = CancelContext | TimerContext + +pub fn (mut ctx CancelerContext) done() chan int { + match mut ctx { + CancelContext { + return ctx.done() + } + TimerContext { + return ctx.done() + } + } +} + +pub fn (mut ctx CancelerContext) err() string { + match mut ctx { + CancelContext { + return ctx.err() + } + TimerContext { + return ctx.err() + } + } +} + +pub fn (ctx CancelerContext) value(key string) ?voidptr { + match ctx { + CancelContext { + return ctx.value(key) + } + TimerContext { + return ctx.value(key) + } + } +} + +pub fn (mut ctx CancelerContext) cancel(remove_from_parent bool, err string) { + match mut ctx { + CancelContext { + ctx.cancel(remove_from_parent, err) + } + TimerContext { + ctx.cancel(remove_from_parent, err) + } + } +} + +pub fn (ctx CancelerContext) str() string { + match ctx { + CancelContext { + return ctx.str() + } + TimerContext { + return ctx.str() + } + } +} + +// A CancelContext can be canceled. When canceled, it also cancels any children +// that implement Canceler. +pub struct CancelContext { + id string +mut: + context Context + mutex &sync.Mutex + done chan int + children map[string]Canceler + err string +} + +// A CancelFunc tells an operation to abandon its work. +// A CancelFunc does not wait for the work to stop. +// A CancelFunc may be called by multiple goroutines simultaneously. +// After the first call, subsequent calls to a CancelFunc do nothing. +// pub type CancelFunc = fn (c Canceler) + +// with_cancel returns a copy of parent with a new done channel. The returned +// context's done channel is closed when the returned cancel function is called +// or when the parent context's done channel is closed, whichever happens first. +// +// Canceling this context releases resources associated with it, so code should +// call cancel as soon as the operations running in this Context complete. +pub fn with_cancel(parent Context) &CancelerContext { + mut c := new_cancel_context(parent) + propagate_cancel(parent, mut c) + return c +} + +// new_cancel_context returns an initialized CancelContext. +fn new_cancel_context(parent Context) &CancelContext { + return &CancelContext{ + id: rand.uuid_v4() + context: parent + mutex: sync.new_mutex() + } +} + +pub fn (ctx CancelContext) deadline() ?time.Time { + return none +} + +pub fn (mut ctx CancelContext) done() chan int { + ctx.mutex.@lock() + done := ctx.done + ctx.mutex.unlock() + return done +} + +pub fn (mut ctx CancelContext) err() string { + ctx.mutex.@lock() + err := ctx.err + ctx.mutex.unlock() + return err +} + +pub fn (ctx CancelContext) value(key string) ?voidptr { + if key == cancel_context_key { + return voidptr(&ctx) + } + return ctx.context.value(key) +} + +pub fn (ctx CancelContext) str() string { + return context_name(ctx.context) + '.with_cancel' +} + +fn (mut ctx CancelContext) cancel(remove_from_parent bool, err string) { + if err == '' { + panic('context: internal error: missing cancel error') + } + + ctx.mutex.@lock() + if ctx.err != '' { + ctx.mutex.unlock() + // already canceled + return + } + + ctx.err = err + + if !ctx.done.closed { + ctx.done.close() + } + + for _, child in ctx.children { + // NOTE: acquiring the child's lock while holding parent's lock. + child.cancel(false, err) + } + + ctx.children = map[string]Canceler{} + ctx.mutex.unlock() + + if remove_from_parent { + remove_child(ctx.context, ctx) + } +} + +fn propagate_cancel(parent Context, mut child Canceler) { + done := parent.done() + select { + _ := <-done { + // parent is already canceled + child.cancel(false, parent.err()) + return + } + else {} + } + mut p := parent_cancel_context(parent) or { + go fn (parent Context, mut child Canceler) { + pdone := parent.done() + cdone := child.done() + select { + _ := <-pdone { + child.cancel(false, parent.err()) + } + _ := <-cdone {} + else {} + } + }(parent, mut child) + return + } + + if p.err != '' { + // parent has already been canceled + child.cancel(false, p.err) + } else { + p.children[child.id] = child + } +} + +// parent_cancel_context returns the underlying CancelContext for parent. +// It does this by looking up parent.value(&cancel_context_key) to find +// the innermost enclosing CancelContext and then checking whether +// parent.done() matches that CancelContext. (If not, the CancelContext +// has been wrapped in a custom implementation providing a +// different done channel, in which case we should not bypass it.) +fn parent_cancel_context(parent Context) ?CancelContext { + done := parent.done() + if done.closed { + return none + } + if p_ptr := parent.value(cancel_context_key) { + if !isnil(p_ptr) { + mut p := &CancelContext(p_ptr) + pdone := p.done() + if done == pdone { + return *p + } + } + } + return none +} + +// remove_child removes a context from its parent. +fn remove_child(parent Context, child Canceler) { + mut p := parent_cancel_context(parent) or { return } + p.children.delete(child.id) +} diff --git a/vlib/context/cancel_test.v b/vlib/context/cancel_test.v new file mode 100644 index 0000000000..be99cd067f --- /dev/null +++ b/vlib/context/cancel_test.v @@ -0,0 +1,39 @@ +import context + +// This example demonstrates the use of a cancelable context to prevent a +// routine leak. By the end of the example function, the routine started +// by gen will return without leaking. +fn test_with_cancel() { + // gen generates integers in a separate routine and + // sends them to the returned channel. + // The callers of gen need to cancel the context once + // they are done consuming generated integers not to leak + // the internal routine started by gen. + gen := fn (mut ctx context.CancelerContext) chan int { + dst := chan int{} + go fn (mut ctx context.CancelerContext, dst chan int) { + ch := ctx.done() + loop: for i in 0 .. 5 { + select { + _ := <-ch { + // returning not to leak the routine + break loop + } + dst <- i {} + } + } + }(mut ctx, dst) + return dst + } + + mut ctx := context.with_cancel(context.background()) + defer { + context.cancel(mut ctx) + } + + ch := gen(mut ctx) + for i in 0 .. 5 { + v := <-ch + assert i == v + } +} diff --git a/vlib/context/deadline.v b/vlib/context/deadline.v new file mode 100644 index 0000000000..c115a90490 --- /dev/null +++ b/vlib/context/deadline.v @@ -0,0 +1,92 @@ +module context + +import rand +import time + +// A TimerContext carries a timer and a deadline. It embeds a CancelContext to +// implement done and err. It implements cancel by stopping its timer then +// delegating to CancelContext.cancel +pub struct TimerContext { + id string +mut: + cancel_ctx CancelContext + deadline time.Time +} + +// with_deadline returns a copy of the parent context with the deadline adjusted +// to be no later than d. If the parent's deadline is already earlier than d, +// with_deadline(parent, d) is semantically equivalent to parent. The returned +// context's Done channel is closed when the deadline expires, when the returned +// cancel function is called, or when the parent context's Done channel is +// closed, whichever happens first. +// +// Canceling this context releases resources associated with it, so code should +// call cancel as soon as the operations running in this Context complete. +pub fn with_deadline(parent Context, d time.Time) &CancelerContext { + id := rand.uuid_v4() + if cur := parent.deadline() { + if cur < d { + // The current deadline is already sooner than the new one. + return with_cancel(parent) + } + } + cancel_ctx := new_cancel_context(parent) + mut ctx := &TimerContext{ + cancel_ctx: cancel_ctx + deadline: d + id: id + } + propagate_cancel(parent, mut ctx) + dur := d - time.now() + if dur.nanoseconds() <= 0 { + ctx.cancel(true, deadline_exceeded) // deadline has already passed + return ctx + } + + if ctx.cancel_ctx.err() == '' { + go fn (mut ctx TimerContext, dur time.Duration) { + time.sleep(dur) + ctx_ch := ctx.done() + ctx_ch <- 0 + ctx.cancel(true, deadline_exceeded) + }(mut ctx, dur) + } + return ctx +} + +// with_timeout returns with_deadline(parent, time.now().add(timeout)). +// +// Canceling this context releases resources associated with it, so code should +// call cancel as soon as the operations running in this Context complete +pub fn with_timeout(parent Context, timeout time.Duration) &CancelerContext { + return with_deadline(parent, time.now().add(timeout)) +} + +pub fn (ctx TimerContext) deadline() ?time.Time { + return ctx.deadline +} + +pub fn (mut ctx TimerContext) done() chan int { + return ctx.cancel_ctx.done() +} + +pub fn (mut ctx TimerContext) err() string { + return ctx.cancel_ctx.err() +} + +pub fn (ctx TimerContext) value(key string) ?voidptr { + return ctx.cancel_ctx.value(key) +} + +pub fn (mut ctx TimerContext) cancel(remove_from_parent bool, err string) { + ctx.cancel_ctx.cancel(false, err) + if remove_from_parent { + // Remove this TimerContext from its parent CancelContext's children. + remove_child(ctx.cancel_ctx.context, ctx) + } +} + +pub fn (ctx TimerContext) str() string { + return context_name(ctx.cancel_ctx.context) + '.with_deadline(' + ctx.deadline.str() + ' [' + + (time.now() - ctx.deadline).str() + '])' +} diff --git a/vlib/context/empty.v b/vlib/context/empty.v new file mode 100644 index 0000000000..9cfb53b230 --- /dev/null +++ b/vlib/context/empty.v @@ -0,0 +1,37 @@ +module context + +import time + +// An EmptyContext is never canceled, has no values, and has no deadline. It is not +// struct{}, since vars of this type must have distinct addresses. +pub type EmptyContext = int + +pub fn (ctx EmptyContext) deadline() ?time.Time { + return none +} + +pub fn (ctx EmptyContext) done() chan int { + ch := chan int{} + defer { + ch.close() + } + return ch +} + +pub fn (ctx EmptyContext) err() string { + return '' +} + +pub fn (ctx EmptyContext) value(key string) ?voidptr { + return none +} + +pub fn (ctx EmptyContext) str() string { + if ctx == background { + return 'context.Background' + } + if ctx == todo { + return 'context.TODO' + } + return 'unknown empty Context' +} diff --git a/vlib/context/empty_test.v b/vlib/context/empty_test.v new file mode 100644 index 0000000000..8900fb58cb --- /dev/null +++ b/vlib/context/empty_test.v @@ -0,0 +1,19 @@ +module context + +fn test_background() { + ctx := background() + assert 'context.Background' == ctx.str() + if _ := ctx.value('') { + println('This should not happen') + assert false + } +} + +fn test_todo() { + ctx := todo() + assert 'context.TODO' == ctx.str() + if _ := ctx.value('') { + println('This should not happen') + assert false + } +} diff --git a/vlib/context/value.v b/vlib/context/value.v new file mode 100644 index 0000000000..de314fabcf --- /dev/null +++ b/vlib/context/value.v @@ -0,0 +1,56 @@ +module context + +import time + +// A ValueContext carries a key-value pair. It implements Value for that key and +// delegates all other calls to the embedded Context. +pub struct ValueContext { + key string + value voidptr +mut: + context Context +} + +// with_value returns a copy of parent in which the value associated with key is +// val. +// +// Use context Values only for request-scoped data that transits processes and +// APIs, not for passing optional parameters to functions. +// +// The provided key must be comparable and should not be of type +// string or any other built-in type to avoid collisions between +// packages using context. Users of with_value should define their own +// types for keys +pub fn with_value(parent Context, key string, value voidptr) &ValueContext { + if isnil(key) { + panic('nil key') + } + return &ValueContext{ + context: parent + key: key + value: value + } +} + +pub fn (ctx ValueContext) deadline() ?time.Time { + return ctx.context.deadline() +} + +pub fn (ctx ValueContext) done() chan int { + return ctx.context.done() +} + +pub fn (ctx ValueContext) err() string { + return ctx.context.err() +} + +pub fn (ctx ValueContext) value(key string) ?voidptr { + if ctx.key == key { + return ctx.value + } + return ctx.context.value(key) +} + +pub fn (ctx ValueContext) str() string { + return context_name(ctx.context) + '.with_value' +} diff --git a/vlib/context/value_test.v b/vlib/context/value_test.v new file mode 100644 index 0000000000..d10b5486ea --- /dev/null +++ b/vlib/context/value_test.v @@ -0,0 +1,23 @@ +import context + +type ValueContextKey = string + +// This example demonstrates how a value can be passed to the context +// and also how to retrieve it if it exists. +fn test_with_value() { + f := fn (ctx context.ValueContext, key ValueContextKey) string { + if value := ctx.value(key) { + if !isnil(value) { + return *(&string(value)) + } + } + return 'key not found' + } + + key := ValueContextKey('language') + value := 'VAL' + ctx := context.with_value(context.background(), key, &value) + + assert value == f(ctx, key) + assert 'key not found' == f(ctx, ValueContextKey('color')) +}