Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+269
View File
@@ -0,0 +1,269 @@
# 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). Calling the cancel function
cancels the child and its children, removes the parent's reference to the child,
and stops any associated timers.
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.
## Cancellation with Causes
When a context is canceled, you can optionally attach a **cause** — an error
describing why the cancellation happened. Use `with_cancel_cause` instead of
`with_cancel` to attach a cause:
```v
import context
fn main() {
mut bg := context.background()
mut ctx, cancel := context.with_cancel_cause(mut bg)
defer { cancel(none) }
// Cancel with a specific error cause
cancel(error('request timed out'))
}
```
Retrieve the cause from any context using `cause(ctx)`:
```v
import context
fn main() {
mut bg := context.background()
mut ctx, cancel := context.with_cancel_cause(mut bg)
cancel(none)
c := context.cause(ctx)
if c !is none {
println('canceled: ${c}')
}
}
```
The cause is propagated through the context tree. If a parent context is
canceled with a cause, all derived contexts inherit the same cause.
If multiple ancestors are canceled, the first cause encountered is returned.
### Convenience variants
- `with_timeout_cause(ctx, dur, err)` — timeout with a custom cause
- `with_deadline_cause(ctx, deadline, err)` — deadline with a custom cause
## Examples
In this section you can see some usage examples for this module
### Context With Cancellation
```v
import time
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 main() {
// 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.Context) chan int {
dst := chan int{}
spawn fn (mut ctx context.Context, dst chan int) {
mut v := 0
ch := ctx.done()
for {
select {
_ := <-ch {
// returning not to leak the routine
eprintln('> go thread returns because ctx was canceled/done')
return
}
dst <- v {
v++
}
}
}
}(mut ctx, dst)
return dst
}
mut background := context.background()
mut ctx, cancel := context.with_cancel(mut background)
defer {
cancel()
time.sleep(2 * time.millisecond) // give a small time window, in which the go thread routine has a chance to return
}
mut mut_ctx := ctx
mut ctx2 := &mut_ctx
ch := gen(mut ctx2)
for i in 0 .. 5 {
v := <-ch
println('> received value: ${v}')
assert i == v
}
println('> main is finished here')
}
```
### Context With Deadline
```v
import time
import context
const short_duration = 2 * time.millisecond // a reasonable duration to block in an example
// 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 main() {
dur := time.now().add(short_duration)
mut background := context.background()
mut ctx, cancel := context.with_deadline(mut 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.
cancel()
time.sleep(short_duration) // give a small time window, in which the go thread routine has a chance to return
eprintln('> defer block finishes')
}
ctx_ch := ctx.done()
select {
_ := <-ctx_ch {
println('>> read from ctx_ch succeeded')
}
1 * time.second {
panic('This should not happen')
}
}
eprintln('> main finishes')
}
```
### Context With Timeout
```v
import time
import context
const short_duration = 2 * time.millisecond // a reasonable duration to block in an example
// This example passes a context with a timeout to tell a blocking function that
// it should abandon its work after the timeout elapses.
fn main() {
// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
mut background := context.background()
mut ctx, cancel := context.with_timeout(mut background, short_duration)
defer {
cancel()
eprintln('> defer finishes')
}
ctx_ch := ctx.done()
select {
_ := <-ctx_ch {
eprintln('> reading from ctx_ch succeeded')
}
1 * time.second {
panic('This should not happen')
}
}
eprintln('> main finishes')
}
```
### Context With Value
```v
import context
const not_found_value = &Value{
val: 'key not found'
}
struct Value {
val string
}
// This example demonstrates how a value can be passed to the context
// and also how to retrieve it if it exists.
fn main() {
f := fn (ctx context.Context, key context.Key) &Value {
if value := ctx.value(key) {
match value {
Value {
return value
}
else {}
}
}
return not_found_value
}
key := 'language'
value := &Value{
val: 'VAL'
}
ctx := context.with_value(context.background(), key, value)
assert value == dump(f(ctx, key))
assert not_found_value == dump(f(ctx, 'color'))
}
```
### Context With Cancellation Cause
```v
import context
struct AppError {
msg string
}
fn (e AppError) msg() string {
return e.msg
}
fn (e AppError) code() int {
return 0
}
fn main() {
mut bg := context.background()
mut ctx, cancel := context.with_cancel_cause(mut bg)
defer { cancel(none) }
// Simulate an error condition and cancel with a cause
cancel(AppError{
msg: 'operation failed'
})
// Retrieve the cause
c := context.cause(ctx)
if c !is none {
eprintln('context canceled: ${c}')
}
}
```
+192
View File
@@ -0,0 +1,192 @@
// This module defines the Context type, which carries deadlines, cancellation signals,
// and other request-scoped values across API boundaries and between processes.
// Based on: https://github.com/golang/go/tree/master/src/context
// Last commit: https://github.com/golang/go/commit/52bf14e0e8bdcd73f1ddfb0c4a1d0200097d3ba2
module context
import rand
import sync
import time
pub type CancelFn = fn ()
pub interface Canceler {
id string
mut:
cancel(remove_from_parent bool, err IError)
done() chan int
}
// 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 = sync.new_mutex()
done chan int
children map[string]Canceler
err IError = none
}
// 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(mut parent Context) (Context, CancelFn) {
mut c := new_cancel_context(parent)
propagate_cancel(mut parent, mut c)
cancel_fn := fn [mut c] () {
c.cancel(true, canceled)
}
return Context(c), CancelFn(cancel_fn)
}
// 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()
done: chan int{cap: 2}
err: none
}
}
// deadline always returns none for a CancelContext, since it has no deadline.
pub fn (ctx &CancelContext) deadline() ?time.Time {
return none
}
// done returns the done channel, which is closed when this context is canceled.
pub fn (mut ctx CancelContext) done() chan int {
ctx.mutex.lock()
done := ctx.done
ctx.mutex.unlock()
return done
}
// err returns `canceled` after the context has been canceled, or `none` if not yet canceled.
pub fn (mut ctx CancelContext) err() IError {
ctx.mutex.lock()
err := ctx.err
ctx.mutex.unlock()
return err
}
// value returns the CancelContext itself if the key matches the cancel context key,
// otherwise delegates to the parent context.
pub fn (ctx &CancelContext) value(key Key) ?Any {
if key == cancel_context_key {
return ctx
}
return ctx.context.value(key)
}
// str returns a string representation of the CancelContext,
// showing the parent context name suffixed with '.with_cancel'.
pub fn (ctx &CancelContext) str() string {
return context_name(ctx.context) + '.with_cancel'
}
fn (mut ctx CancelContext) cancel(remove_from_parent bool, err IError) {
if err is none {
panic('context: internal error: missing cancel error')
}
ctx.mutex.lock()
if ctx.err !is none {
ctx.mutex.unlock()
// already canceled
return
}
ctx.err = err
if !ctx.done.closed {
ctx.done <- 0
ctx.done.close()
}
for _, child in ctx.children {
// NOTE: acquiring the child's lock while holding parent's lock.
mut c := child
c.cancel(false, err)
}
ctx.children = map[string]Canceler{}
ctx.mutex.unlock()
if remove_from_parent {
mut cc := &ctx.context
remove_child(mut cc, ctx)
}
}
fn propagate_cancel(mut 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(mut parent) or {
// Pre-extract the parent done channel and pass it by value into the
// spawn. The goroutine may outlive the caller's stack frame, so
// dereferencing `parent` inside the spawn (e.g. via parent.done())
// reads freed stack memory once the caller returns and reuses that
// region — observed as a consistent segfault in vlib/context/cause_test.v
// on Linux x86_64. Mirrors the fix in onecontext.run_two_contexts.
spawn fn (pdone chan int, mut parent Context, mut child Canceler) {
select {
_ := <-pdone {
child.cancel(false, parent.err())
}
}
}(done, mut parent, mut child)
return
}
if p.err is none {
p.children[child.id] = child
} else {
// parent has already been canceled
child.cancel(false, p.err)
}
}
// 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(mut parent Context) ?&CancelContext {
done := parent.done()
if done.closed {
return none
}
mut p := parent.value(cancel_context_key)?
match mut p {
CancelContext {
pdone := p.done()
if done == pdone {
return p
}
}
else {}
}
return none
}
// remove_child removes a context from its parent.
fn remove_child(mut parent Context, child Canceler) {
mut p := parent_cancel_context(mut parent) or { return }
p.children.delete(child.id)
}
+47
View File
@@ -0,0 +1,47 @@
// vtest retry: 3
// vtest build: amd64 || arm64
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.Context) chan int {
dst := chan int{}
spawn fn (mut ctx context.Context, dst chan int) {
mut v := 0
ch := ctx.done()
for {
select {
_ := <-ch {
// returning not to leak the routine
return
}
dst <- v {
v++
}
}
}
}(mut ctx, dst)
return dst
}
mut background := context.background()
mut ctx, cancel := context.with_cancel(mut background)
defer {
cancel()
}
mut mut_ctx := ctx
mut ctx2 := &mut_ctx
ch := gen(mut ctx2)
for i in 0 .. 5 {
v := <-ch
assert i == v
}
}
+202
View File
@@ -0,0 +1,202 @@
// This module defines the Context type, which carries deadlines, cancellation signals,
// and other request-scoped values across API boundaries and between processes.
// Based on: https://github.com/golang/go/tree/master/src/context
// Last commit: https://github.com/golang/go/commit/52bf14e0e8bdcd73f1ddfb0c4a1d0200097d3ba2
module context
import rand
import time
// cause_context_key is the value-context key used to store the cause in a CauseContext.
const cause_context_key = Key('context.CauseContext')
// CancelCauseFunc is a cancel function that accepts an optional cause error.
// Calling it with a non-none cause records that cause; calling it with none
// is equivalent to calling a plain CancelFn.
pub type CancelCauseFunc = fn (cause IError)
// A CauseContext wraps a CancelContext and records a cause error set via
// CancelCauseFunc. It implements the Context interface by delegating to the
// embedded CancelContext.
@[heap]
struct CauseContext {
id string
deadline time.Time
mut:
cancel_ctx CancelContext
cause IError = none
}
// with_cancel_cause behaves like with_cancel but returns a CancelCauseFunc
// instead of a CancelFn. Calling the returned function with a non-none error
// records that error as the cause, retrievable via cause(ctx). Calling it with
// none is identical to a plain cancel.
//
// Example:
// mut bg := context.background()
// mut ctx, cancel := context.with_cancel_cause(mut bg)
// defer { cancel(none) }
// cancel(my_err)
// assert context.cause(ctx).str() == my_err.str()
pub fn with_cancel_cause(mut parent Context) (Context, CancelCauseFunc) {
id := rand.uuid_v4()
inner := new_cancel_context(parent)
mut ctx := &CauseContext{
id: id
cancel_ctx: inner
}
propagate_cancel(mut parent, mut ctx)
cancel_fn := fn [mut ctx] (cause IError) {
if cause !is none {
ctx.cancel_ctx.mutex.lock()
if ctx.cause is none {
ctx.cause = cause
}
ctx.cancel_ctx.mutex.unlock()
}
ctx.cancel_ctx.cancel(true, canceled)
}
return Context(ctx), CancelCauseFunc(cancel_fn)
}
// with_deadline_cause behaves like with_deadline but accepts a cause error that
// is recorded when the deadline fires (instead of the generic deadline_exceeded).
// If cancel is called before the deadline, that cancellation takes effect instead.
pub fn with_deadline_cause(mut parent Context, d time.Time, deadline_cause IError) (Context, CancelFn) {
if cur := parent.deadline() {
if cur < d {
return with_cancel(mut parent)
}
}
inner := new_cancel_context(parent)
id := rand.uuid_v4()
mut ctx := &CauseContext{
id: id
cancel_ctx: inner
deadline: d
}
propagate_cancel(mut parent, mut ctx)
dur := d - time.now()
if dur.nanoseconds() <= 0 {
// deadline already passed
ctx.cause = deadline_cause
ctx.cancel_ctx.cancel(false, deadline_exceeded)
cancel_fn := fn [mut ctx] () {
ctx.cancel_ctx.cancel(true, canceled)
}
return Context(ctx), CancelFn(cancel_fn)
}
spawn fn (mut ctx CauseContext, dur time.Duration, deadline_cause IError) {
time.sleep(dur)
ctx.cancel_ctx.mutex.lock()
// Only record deadline cause if the context wasn't already canceled,
// so that the first cancellation's cause (manual or from parent) wins.
if ctx.cancel_ctx.err is none {
ctx.cause = deadline_cause
}
ctx.cancel_ctx.mutex.unlock()
ctx.cancel_ctx.cancel(true, deadline_exceeded)
}(mut ctx, dur, deadline_cause)
cancel_fn := fn [mut ctx] () {
ctx.cancel_ctx.cancel(true, canceled)
}
return Context(ctx), CancelFn(cancel_fn)
}
// with_timeout_cause is a convenience wrapper around with_deadline_cause using
// a relative duration instead of an absolute time.
pub fn with_timeout_cause(mut parent Context, timeout time.Duration, deadline_cause IError) (Context, CancelFn) {
return with_deadline_cause(mut parent, time.now().add(timeout), deadline_cause)
}
// cause returns the cause of the context's cancellation.
// If ctx was canceled via CancelCauseFunc with a non-none cause, that cause is
// returned. Otherwise, cause returns ctx.err() (i.e. canceled or
// deadline_exceeded).
pub fn cause(ctx Context) IError {
mut mctx := ctx
if val := mctx.value(cause_context_key) {
match val {
CauseContext {
val.cancel_ctx.mutex.lock()
c := val.cause
val.cancel_ctx.mutex.unlock()
if c !is none {
return c
}
}
else {}
}
}
return mctx.err()
}
// --- CauseContext implements Context and Canceler ---
// deadline returns the deadline for the context, or none if no deadline is set.
pub fn (ctx &CauseContext) deadline() ?time.Time {
if ctx.deadline != time.Time{} {
return ctx.deadline
}
return none
}
// done returns a channel that is closed when the context is canceled.
pub fn (mut ctx CauseContext) done() chan int {
return ctx.cancel_ctx.done()
}
// err returns the error that canceled the context, or none if not canceled.
pub fn (mut ctx CauseContext) err() IError {
return ctx.cancel_ctx.err()
}
// value returns the cause context itself when the key matches cause_context_key,
// otherwise delegates to the embedded cancel context.
pub fn (ctx &CauseContext) value(key Key) ?Any {
if key == cause_context_key {
return ctx
}
return ctx.cancel_ctx.value(key)
}
// str returns a string representation of the context (e.g. 'EmptyContext.with_cancel_cause').
pub fn (ctx &CauseContext) str() string {
return context_name(ctx.cancel_ctx.context) + '.with_cancel_cause'
}
// cancel cancels the context. If remove_from_parent is true, it also removes
// this context from its parent's children list. The err is recorded as the cause.
pub fn (mut ctx CauseContext) cancel(remove_from_parent bool, err IError) {
// If being canceled with the generic 'context canceled' error and we
// have a parent, propagate the parent's cause so children can retrieve it.
// We walk the value chain directly instead of calling cause(): cause()
// would fall through to err() on a parent CancelContext, which would
// deadlock here because this cancel is typically invoked from within a
// parent's cancel() while that parent's mutex is held. The cause field
// on a CauseContext is always assigned before its cancel propagates, so
// reading it without locking is safe along this downward path.
if err.str() == 'context canceled' {
match ctx.cancel_ctx.context {
EmptyContext {}
else {
mut parent := ctx.cancel_ctx.context
if val := parent.value(cause_context_key) {
match val {
CauseContext {
if val.cause !is none {
ctx.cause = val.cause
}
}
else {}
}
}
}
}
}
ctx.cancel_ctx.cancel(false, err)
if remove_from_parent {
mut cc := &ctx.cancel_ctx.context
remove_child(mut cc, ctx)
}
}
+116
View File
@@ -0,0 +1,116 @@
// vtest retry: 3
module context
import time
fn test_with_cancel_cause_no_cause() {
mut bg := background()
mut ctx, cancel := with_cancel_cause(mut bg)
defer {
cancel(none)
}
assert ctx.err() is none
assert cause(ctx) is none
cancel(none)
assert ctx.err().str() == 'context canceled'
// cause falls back to err() when no cause was set
assert cause(ctx).str() == 'context canceled'
}
fn test_with_cancel_cause_with_cause() {
my_err := error('my custom cause')
mut bg := background()
mut ctx, cancel := with_cancel_cause(mut bg)
defer {
cancel(none)
}
assert ctx.err() is none
cancel(my_err)
assert ctx.err().str() == 'context canceled'
assert cause(ctx).str() == 'my custom cause'
}
fn test_with_cancel_cause_first_cause_wins() {
first := error('first cause')
second := error('second cause')
mut bg := background()
mut ctx, cancel := with_cancel_cause(mut bg)
cancel(first)
cancel(second) // should be ignored
cancel(none) // should be ignored
assert cause(ctx).str() == 'first cause'
}
fn test_with_cancel_cause_parent_cancel_propagates() {
mut bg := background()
mut parent, parent_cancel := with_cancel(mut bg)
mut ctx, _ := with_cancel_cause(mut parent)
assert ctx.err() is none
parent_cancel()
// give goroutine a moment to propagate
time.sleep(5 * time.millisecond)
assert ctx.err().str() == 'context canceled'
}
fn test_with_timeout_cause_fires_cause() {
my_cause := error('timed out for testing')
mut bg := background()
mut ctx, cancel := with_timeout_cause(mut bg, 30 * time.millisecond, my_cause)
defer {
cancel()
}
// not done yet
assert ctx.err() is none
// poll for the deadline goroutine to record the cancellation; busy CI
// runners can take well over 60ms to schedule it.
deadline := time.now().add(2 * time.second)
for ctx.err() is none && time.now() < deadline {
time.sleep(5 * time.millisecond)
}
assert ctx.err().str() == 'context deadline exceeded'
assert cause(ctx).str() == 'timed out for testing'
}
fn test_with_timeout_cause_cancel_before_deadline() {
my_cause := error('deadline cause')
mut bg := background()
mut ctx, cancel := with_timeout_cause(mut bg, 500 * time.millisecond, my_cause)
// cancel before deadline fires
cancel()
time.sleep(5 * time.millisecond)
assert ctx.err().str() == 'context canceled'
// no cause was set via CancelCauseFunc; cause falls back to err()
assert cause(ctx).str() == 'context canceled'
}
fn test_cause_on_plain_cancel_context() {
// cause() on a non-cause context should just return ctx.err()
mut bg := background()
mut ctx, cancel := with_cancel(mut bg)
defer {
cancel()
}
assert cause(ctx) is none
cancel()
assert cause(ctx).str() == 'context canceled'
}
+106
View File
@@ -0,0 +1,106 @@
// This module defines the Context type, which carries deadlines, cancellation signals,
// and other request-scoped values across API boundaries and between processes.
// Based on: https://github.com/golang/go/tree/master/src/context
// Last commit: https://github.com/golang/go/commit/52bf14e0e8bdcd73f1ddfb0c4a1d0200097d3ba2
module context
import time
const cancel_context_key = Key('context.CancelContext')
// canceled is the error returned by Context.err when the context is canceled.
const canceled = error('context canceled')
// deadline_exceeded is the error returned by Context.err when the context's
// deadline passes.
const deadline_exceeded = error('context deadline exceeded')
// Key represents the type for the ValueContext key
pub type Key = bool | f32 | f64 | i16 | i64 | i8 | int | string | u16 | u32 | u64 | u8 | voidptr
// Any represents a generic type for the ValueContext
pub interface Any {}
// `Context` is an interface that defined the minimum required functionality
// for a 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.
//
// `value(key)` returns an Optional that wraps the value associated with this context for key.
// It returns none 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;
// modules should define keys as an unexported type to avoid
// collisions.
//
// `done()` returns a channel that's closed when work done on behalf of this
// context should be canceled. done may return a closed channel 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.
//
// `err()` returns an IError based on some conditions
// If done is not yet closed, err returns none.
// If done is closed, err returns a non-none error explaining why:
// canceled if the context was canceled
// or deadline_exceeded if the context's deadline passed.
// After err returns a non-none error, successive calls to err return the same error.
pub interface Context {
deadline() ?time.Time
value(key Key) ?Any
mut:
done() chan int
err() IError
}
// str returns the `str` method of the corresponding Context struct
pub fn (ctx &Context) str() string {
// since `Context` is an interface we have to manually match every possible
// type that implements `Context` if we want to use a `Context` as a field in a struct
// since the `Context` interface has to implement its own `str` method.
match ctx {
BackgroundContext {
return ctx.str()
}
EmptyContext {
return ctx.str()
}
TodoContext {
return ctx.str()
}
CancelContext {
return ctx.str()
}
CauseContext {
return ctx.str()
}
TimerContext {
return ctx.str()
}
ValueContext {
return ctx.str()
}
else {
return context_name(ctx)
}
}
}
fn context_name(ctx Context) string {
return typeof(ctx)
}
+111
View File
@@ -0,0 +1,111 @@
// This module defines the Context type, which carries deadlines, cancellation signals,
// and other request-scoped values across API boundaries and between processes.
// Based on: https://github.com/golang/go/tree/master/src/context
// Last commit: https://github.com/golang/go/commit/52bf14e0e8bdcd73f1ddfb0c4a1d0200097d3ba2
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(mut parent Context, d time.Time) (Context, CancelFn) {
id := rand.uuid_v4()
if cur := parent.deadline() {
if cur < d {
// The current deadline is already sooner than the new one.
return with_cancel(mut parent)
}
}
cancel_ctx := new_cancel_context(parent)
mut ctx := &TimerContext{
cancel_ctx: cancel_ctx
deadline: d
id: id
}
propagate_cancel(mut parent, mut ctx)
dur := d - time.now()
if dur.nanoseconds() <= 0 {
ctx.cancel(true, deadline_exceeded) // deadline has already passed
cancel_fn := fn [mut ctx] () {
ctx.cancel(true, canceled)
}
return Context(ctx), CancelFn(cancel_fn)
}
if ctx.err() is none {
spawn fn (mut ctx TimerContext, dur time.Duration) {
time.sleep(dur)
ctx.cancel(true, deadline_exceeded)
}(mut ctx, dur)
}
cancel_fn := fn [mut ctx] () {
ctx.cancel(true, canceled)
}
return Context(ctx), CancelFn(cancel_fn)
}
// 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(mut parent Context, timeout time.Duration) (Context, CancelFn) {
return with_deadline(mut parent, time.now().add(timeout))
}
// deadline returns the deadline for this context.
pub fn (ctx &TimerContext) deadline() ?time.Time {
return ctx.deadline
}
// done returns the done channel from the embedded CancelContext.
pub fn (mut ctx TimerContext) done() chan int {
return ctx.cancel_ctx.done()
}
// err returns the error from the embedded CancelContext.
pub fn (mut ctx TimerContext) err() IError {
return ctx.cancel_ctx.err()
}
// value returns the value associated with the given key by delegating
// to the embedded CancelContext.
pub fn (ctx &TimerContext) value(key Key) ?Any {
return ctx.cancel_ctx.value(key)
}
// cancel cancels the timer context by delegating to the embedded CancelContext
// and optionally removing itself from its parent's children.
pub fn (mut ctx TimerContext) cancel(remove_from_parent bool, err IError) {
ctx.cancel_ctx.cancel(false, err)
if remove_from_parent {
// Remove this TimerContext from its parent CancelContext's children.
mut cc := &ctx.cancel_ctx.context
remove_child(mut cc, ctx)
}
}
// str returns a string representation of the TimerContext, showing the
// parent context name, the deadline time, and the duration remaining.
pub fn (ctx &TimerContext) str() string {
return context_name(ctx.cancel_ctx.context) + '.with_deadline(' + ctx.deadline.str() + ' [' +
(time.now() - ctx.deadline).str() + '])'
}
+49
View File
@@ -0,0 +1,49 @@
// vtest build: false // TODO: it sometimes blocks, investigate why
import context
import time
// a reasonable duration to block in an example
const short_duration = 1 * time.millisecond
// 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 test_with_deadline() {
dur := time.now().add(short_duration)
mut background := context.background()
mut ctx, cancel := context.with_deadline(mut 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.
cancel()
}
ctx_ch := ctx.done()
select {
_ := <-ctx_ch {}
1 * time.second {
panic('This should not happen')
}
}
}
// This example passes a context with a timeout to tell a blocking function that
// it should abandon its work after the timeout elapses.
fn test_with_timeout() {
// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
mut background := context.background()
mut ctx, cancel := context.with_timeout(mut background, short_duration)
defer {
cancel()
}
ctx_ch := ctx.done()
select {
_ := <-ctx_ch {}
1 * time.second {
panic('This should not happen')
}
}
}
+80
View File
@@ -0,0 +1,80 @@
// This module defines the Context type, which carries deadlines, cancellation signals,
// and other request-scoped values across API boundaries and between processes.
// Based on: https://github.com/golang/go/tree/master/src/context
// Last commit: https://github.com/golang/go/commit/52bf14e0e8bdcd73f1ddfb0c4a1d0200097d3ba2
module context
import time
// An EmptyContext is never canceled, has no values.
// The done_ch field is an open channel that is never closed, returned by done()
// on every call. This mirrors Go's context.Background().Done() == nil behavior:
// selecting on this channel blocks forever, meaning the context is never done.
pub struct EmptyContext {
mut:
done_ch chan int
}
// deadline returns none, since an EmptyContext has no deadline.
pub fn (ctx &EmptyContext) deadline() ?time.Time {
return none
}
// done returns an open channel that is never closed, since an EmptyContext can
// never be canceled. Selecting on the returned channel blocks forever.
// The same channel instance is returned on every call for a given EmptyContext.
pub fn (mut ctx EmptyContext) done() chan int {
return ctx.done_ch
}
// err returns none, since an EmptyContext is never canceled.
pub fn (ctx &EmptyContext) err() IError {
return none
}
// value returns none, since an EmptyContext carries no values.
pub fn (ctx &EmptyContext) value(key Key) ?Any {
return none
}
// str returns a string describing the Context.
pub fn (ctx &EmptyContext) str() string {
return 'unknown empty Context'
}
// A BackgroundContext is never canceled, has no values.
struct BackgroundContext {
EmptyContext
}
// str returns a string describing the Context.
pub fn (ctx &BackgroundContext) str() string {
return 'context.Background'
}
// 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 &BackgroundContext{}
}
// A TodoContext is never canceled, has no values, and has no deadline. It is
// never used for real work.
struct TodoContext {
EmptyContext
}
// str returns a string describing the Context.
pub fn (ctx &TodoContext) str() string {
return 'context.TODO'
}
// 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 &TodoContext{}
}
+18
View File
@@ -0,0 +1,18 @@
// vtest build: amd64 || arm64
module context
fn test_background() {
ctx := background()
assert '&context.Background' == ctx.str()
if _ := ctx.value('') {
panic('This should never happen')
}
}
fn test_todo() {
ctx := todo()
assert '&context.TODO' == ctx.str()
if _ := ctx.value('') {
panic('This should never happen')
}
}
+40
View File
@@ -0,0 +1,40 @@
# onecontext
A library to merge existing V contexts.
## Overview
Have you ever faced the situation where you have to merge multiple existing contexts?
If not, then you might, eventually.
For example, we can face the situation where we are building an application
using a library that gives us a global context.
This context expires once the application is stopped.
Meanwhile, we are exposing a service like this:
```v ignore
fn (f Foo) get(ctx context.Context, bar Bar) ?Baz {
. . .
}
```
Here, we receive another context provided by the service.
Then, in the `get` implementation, we want for example to query a database and
we must provide a context for that.
Ideally, we would like to provide a merged context that would expire either:
- When the application is stopped
- Or when the received service context expires
This is exactly the purpose of this library.
In our case, we can now merge the two contexts in a single one like this:
```v ignore
ctx, cancel := onecontext.merge(ctx1, ctx2)
```
This returns a merged context that we can now propagate
@@ -0,0 +1,164 @@
module onecontext
import context
import sync
import time
// canceled is the error returned when the cancel function is called on a merged context
pub const canceled = error('canceled context')
@[heap]
struct OneContext {
mut:
ctx context.Context
ctxs []context.Context
done chan int
err IError = none
err_mutex &sync.Mutex = sync.new_mutex()
cancel_fn context.CancelFn = unsafe { nil }
cancel_ctx context.Context
}
// merge allows to merge multiple contexts
// it returns the merged context
pub fn merge(ctx context.Context, ctxs ...context.Context) (context.Context, context.CancelFn) {
mut background := context.background()
cancel_ctx, cancel := context.with_cancel(mut background)
mut octx := &OneContext{
done: chan int{cap: 3}
ctx: ctx
ctxs: ctxs
cancel_fn: cancel
cancel_ctx: cancel_ctx
}
spawn octx.run()
return context.Context(octx), context.CancelFn(cancel)
}
// deadline returns the earliest deadline among all merged contexts,
// or none if no context has a deadline set.
pub fn (octx OneContext) deadline() ?time.Time {
mut min := time.Time{}
mut found := false
if deadline := octx.ctx.deadline() {
min = deadline
found = true
}
for ctx in octx.ctxs {
if deadline := ctx.deadline() {
if !found || deadline < min {
min = deadline
}
found = true
}
}
if !found {
return none
}
return min
}
// done returns the done channel, which is closed when the merged context is canceled.
pub fn (octx OneContext) done() chan int {
return octx.done
}
// err returns the error from the merged context, or `none` if not yet canceled.
pub fn (mut octx OneContext) err() IError {
octx.err_mutex.lock()
defer {
octx.err_mutex.unlock()
}
return octx.err
}
// value looks up a value by key across all merged contexts, returning
// the first match found or none if no context holds the key.
pub fn (octx OneContext) value(key context.Key) ?context.Any {
if value := octx.ctx.value(key) {
return value
}
for ctx in octx.ctxs {
if value := ctx.value(key) {
return value
}
}
return none
}
// run starts listening for cancellation signals from all merged contexts.
pub fn (mut octx OneContext) run() {
mut wrapped_ctx := &octx.ctx
if octx.ctxs.len == 1 {
mut first_ctx := &octx.ctxs[0]
octx.run_two_contexts(mut wrapped_ctx, mut first_ctx)
return
}
octx.run_multiple_contexts(mut wrapped_ctx)
for mut ctx in octx.ctxs {
octx.run_multiple_contexts(mut ctx)
}
}
// str returns a string representation of the OneContext.
pub fn (octx OneContext) str() string {
return ''
}
// cancel cancels the merged context with the given error, closing
// the done channel and propagating cancellation to the underlying context.
pub fn (mut octx OneContext) cancel(err IError) {
octx.cancel_fn()
octx.err_mutex.lock()
octx.err = err
octx.err_mutex.unlock()
if !octx.done.closed {
octx.done <- 0
octx.done.close()
}
}
// run_two_contexts spawns a listener that cancels the merged context
// when either of the two given contexts is done.
pub fn (mut octx OneContext) run_two_contexts(mut ctx1 context.Context, mut ctx2 context.Context) {
octx_cancel_done := octx.cancel_ctx.done()
c1done := ctx1.done()
c2done := ctx2.done()
spawn fn (mut octx OneContext, octx_cancel_done chan int, c1done chan int, c2done chan int, mut ctx1 context.Context, mut ctx2 context.Context) {
select {
_ := <-octx_cancel_done {
octx.cancel(canceled)
}
_ := <-c1done {
octx.cancel(ctx1.err())
}
_ := <-c2done {
octx.cancel(ctx2.err())
}
}
}(mut &octx, octx_cancel_done, c1done, c2done, mut ctx1, mut ctx2)
}
// run_multiple_contexts spawns a listener that cancels the merged context
// when the given context is done.
pub fn (mut octx OneContext) run_multiple_contexts(mut ctx context.Context) {
octx_cancel_done := octx.cancel_ctx.done()
cdone := ctx.done()
spawn fn (mut octx OneContext, octx_cancel_done chan int, cdone chan int, mut ctx context.Context) {
select {
_ := <-octx_cancel_done {
octx.cancel(canceled)
}
_ := <-cdone {
octx.cancel(ctx.err())
}
}
}(mut &octx, octx_cancel_done, cdone, mut ctx)
}
@@ -0,0 +1,178 @@
module onecontext
import context
import time
fn eventually(ch chan int) bool {
mut background := context.background()
mut timeout, cancel := context.with_timeout(mut background, 30 * time.millisecond)
defer {
cancel()
}
tdone := timeout.done()
select {
_ := <-ch {
return true
}
_ := <-tdone {
return false
}
}
return false
}
struct Value {
val string
}
fn test_merge_nominal() {
mut background := context.background()
foo := 'foo'
mut value_ctx1 := context.with_value(background, 'foo', foo)
mut ctx1, cancel := context.with_cancel(mut value_ctx1)
defer {
cancel()
}
bar := 'bar'
mut value_ctx2 := context.with_value(background, 'bar', bar)
mut ctx2, _ := context.with_cancel(mut value_ctx2)
mut ctx, cancel2 := merge(ctx1, ctx2)
if deadline := ctx.deadline() {
assert false
}
val1 := ctx.value('foo') or {
assert false
return
}
match val1 {
string {
assert foo == val1
}
else {
assert false
}
}
val2 := ctx.value('bar') or {
assert false
return
}
match val2 {
string {
assert bar == val2
}
else {
assert false
}
}
if _ := ctx.value('baz') {
assert false
}
assert !eventually(ctx.done())
assert ctx.err() is none
cancel2()
assert eventually(ctx.done())
assert ctx.err().str() == 'canceled context'
}
fn test_merge_deadline_context_1() {
mut background := context.background()
mut ctx1, cancel := context.with_timeout(mut background, time.second)
defer {
cancel()
}
ctx2 := context.background()
mut ctx, _ := merge(ctx1, ctx2)
if deadline := ctx.deadline() {
assert deadline.unix() != 0
} else {
assert false
}
}
fn test_merge_deadline_context_2() {
mut background := context.background()
ctx1 := context.background()
mut ctx2, cancel := context.with_timeout(mut background, time.second)
defer {
cancel()
}
mut ctx, _ := merge(ctx1, ctx2)
if deadline := ctx.deadline() {
assert deadline.unix() != 0
} else {
assert false
}
}
fn test_merge_deadline_context_n() {
mut background := context.background()
ctx1 := context.background()
mut ctxs := []context.Context{cap: 21}
for i in 0 .. 10 {
ctxs << context.background()
}
mut ctx_n, _ := context.with_timeout(mut background, 10 * time.second)
ctxs << ctx_n
for i in 0 .. 10 {
ctxs << context.background()
}
mut ctx, cancel := merge(ctx1, ...ctxs)
assert !eventually(ctx.done())
assert ctx.err() is none
cancel()
assert eventually(ctx.done())
assert ctx.err().str() == 'canceled context'
}
fn test_merge_deadline_none() {
ctx1 := context.background()
ctx2 := context.background()
mut ctx, _ := merge(ctx1, ctx2)
if _ := ctx.deadline() {
assert false
}
}
fn test_merge_cancel_two() {
ctx1 := context.background()
ctx2 := context.background()
mut ctx, cancel := merge(ctx1, ctx2)
cancel()
assert eventually(ctx.done())
assert ctx.err().str() == 'canceled context'
assert ctx.err().str() == 'canceled context'
}
fn test_merge_cancel_multiple() {
ctx1 := context.background()
ctx2 := context.background()
ctx3 := context.background()
mut ctx, cancel := merge(ctx1, ctx2, ctx3)
cancel()
assert eventually(ctx.done())
assert ctx.err().str() == 'canceled context'
assert ctx.err().str() == 'canceled context'
}
+65
View File
@@ -0,0 +1,65 @@
// This module defines the Context type, which carries deadlines, cancellation signals,
// and other request-scoped values across API boundaries and between processes.
// Based on: https://github.com/golang/go/tree/master/src/context
// Last commit: https://github.com/golang/go/commit/52bf14e0e8bdcd73f1ddfb0c4a1d0200097d3ba2
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 Key
value Any
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 Key, value Any) Context {
return &ValueContext{
context: parent
key: key
value: value
}
}
// deadline returns the deadline from the parent context.
pub fn (ctx &ValueContext) deadline() ?time.Time {
return ctx.context.deadline()
}
// done returns the done channel from the parent context.
pub fn (mut ctx ValueContext) done() chan int {
return ctx.context.done()
}
// err returns the error from the parent context.
pub fn (mut ctx ValueContext) err() IError {
return ctx.context.err()
}
// value returns the value associated with this context for the given key.
// If the key matches the one stored in this ValueContext, its value is returned.
// Otherwise, the lookup is delegated to the parent context.
pub fn (ctx &ValueContext) value(key Key) ?Any {
if ctx.key == key {
return ctx.value
}
return ctx.context.value(key)
}
// str returns a string representation of the ValueContext,
// showing the parent context name suffixed with '.with_value'.
pub fn (ctx &ValueContext) str() string {
return context_name(ctx.context) + '.with_value'
}
+35
View File
@@ -0,0 +1,35 @@
// vtest build: amd64 || arm64
import context
const not_found_value = &Value{
val: 'key not found'
}
struct Value {
val 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.Context, key context.Key) &Value {
if value := ctx.value(key) {
match value {
Value {
return value
}
else {}
}
}
return not_found_value
}
key := 'language'
value := &Value{
val: 'VAL'
}
ctx := context.with_value(context.background(), key, value)
assert value == f(ctx, key)
assert not_found_value == f(ctx, 'color')
}