detach

package
v0.9.17 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 22, 2024 License: BSD-3-Clause Imports: 3 Imported by: 1

Documentation

Overview

Package detach provides utility function for getting new contexts that are detached from their parent's timeouts and cancel calls but inheriting any values registered with the library.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Async

func Async(parent context.Context, run func(ctx context.Context))

Async wraps run in the Async Hooks registered with the library, creates a new context, and calls the wrapped run function with that context. The wrappers may attach values from the parent context to the new context and perform any cleanup tasks after run completes.

Async should be used when you want to run an async task as a part of another all, but need to keep some values from the parent context.

Example
package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/reddit/baseplate.go/detach"
)

// In a real package, you should register hooks for both inline and async
// calls at the same time.
//
// It should likely be done within an `init()` function rather than calling the
// function explicitly.
func InitAsync() {
	detach.Register(detach.Hooks{
		Inline: nil,
		Async: func(dst, src context.Context, next func(ctx context.Context)) {
			if v, ok := src.Value(asyncContextKey).(*asyncContextVal); ok && v != nil {
				newVal := &asyncContextVal{val: v.val}
				dst = context.WithValue(dst, asyncContextKey, newVal)
				defer func() {
					newVal.close()
				}()
			}
			next(dst)
		},
	})
}

type asyncContextKeyType struct{}

var asyncContextKey asyncContextKeyType

type asyncContextVal struct {
	mu sync.Mutex

	val    int
	closed bool
}

func (v *asyncContextVal) close() {
	v.mu.Lock()
	defer v.mu.Unlock()

	v.closed = true
}

func main() {
	InitAsync()

	const parentTimeout = time.Millisecond

	// asyncDone is used in this example to let us wait until the async call
	// finishes, so we can test the output. In real code you probably wouldn't do
	// this.
	asyncDone := sync.WaitGroup{}
	asyncDone.Add(1)

	run := func() {
		v := &asyncContextVal{val: 1}
		defer v.close()

		ctx, cancel := context.WithTimeout(context.Background(), parentTimeout)
		defer cancel()

		ctx = context.WithValue(ctx, asyncContextKey, v)

		ch := make(chan bool, 1)
		go func() {
			time.Sleep(5 * parentTimeout)
			ch <- true
		}()

		select {
		case <-ch:
			return
		case <-ctx.Done():
		}
		fmt.Println("timed out")

		go detach.Async(ctx, func(detachedCtx context.Context) {
			detachedCtx, detachedCancel := context.WithTimeout(detachedCtx, 100*parentTimeout)
			defer detachedCancel()

			fmt.Printf("parent.Err() == %v\n", ctx.Err())
			fmt.Printf("detached.Err() ==  %v\n", detachedCtx.Err())
			detachedV, _ := detachedCtx.Value(asyncContextKey).(*asyncContextVal)
			fmt.Printf("value equal: %v\n", v == detachedV)
			fmt.Printf("value.val equal: %v\n", v.val == detachedV.val)

			ch := make(chan bool, 1)
			go func() {
				time.Sleep(parentTimeout)
				ch <- true
			}()

			select {
			case <-detachedCtx.Done():
				fmt.Println("never happens")
			case <-ch:
			}

			asyncDone.Done()
		})
	}

	fmt.Println("running...")
	run()

	fmt.Println("waiting for async cleanup")
	asyncDone.Wait()
	fmt.Println("finished cleanup")

}
Output:

running...
timed out
waiting for async cleanup
parent.Err() == context deadline exceeded
detached.Err() ==  <nil>
value equal: false
value.val equal: true
finished cleanup

func Inline

func Inline(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc)

Inline returns a new context who inherits the values attached using the Inline Hooks registered with the library from the parent but replaces the parent's timeout/cancel with the timeout given and the returned cancel func.

Inline should be used when you need to run something within the current call, rather than in a new goroutine, but need to ignore any timeouts/cancellations from the parent context, such as cleaning up after a call that might have been timed out or canceled.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/reddit/baseplate.go/detach"
)

// In a real package, you should register hooks for both inline and async
// calls at the same time.
//
// It should likely be done within an `init()` function rather than calling the
// function explicitly.
func InitInline() {
	detach.Register(detach.Hooks{
		Inline: func(dst, src context.Context) context.Context {
			if v, ok := src.Value(inlineContextKey).(*inlineContextVal); ok && v != nil {
				dst = context.WithValue(dst, inlineContextKey, v)
			}
			return dst
		},
		Async: nil,
	})
}

type inlineContextKeyType struct{}

var inlineContextKey inlineContextKeyType

type inlineContextVal struct {
	val    int
	closed bool
}

func (v *inlineContextVal) close() {
	v.closed = true
}

func main() {
	InitInline()

	const parentTimeout = time.Millisecond

	v := &inlineContextVal{val: 1}
	defer v.close()

	ctx, cancel := context.WithTimeout(context.Background(), parentTimeout)
	defer cancel()

	ctx = context.WithValue(ctx, inlineContextKey, v)

	defer func() {
		detachedCtx, detachedCancel := detach.Inline(ctx, 100*parentTimeout)
		defer detachedCancel()

		fmt.Printf("parent.Err() == %v\n", ctx.Err())
		fmt.Printf("detached.Err() ==  %v\n", detachedCtx.Err())
		detachedV, _ := detachedCtx.Value(inlineContextKey).(*inlineContextVal)
		fmt.Printf("value equal: %v\n", v == detachedV)
		fmt.Printf("value closed: %v\n", detachedV.closed)

		ch := make(chan bool, 1)
		go func() {
			time.Sleep(parentTimeout)
			ch <- true
		}()

		select {
		case <-detachedCtx.Done():
			fmt.Println("never happens")
		case <-ch:
			fmt.Println("finished cleanup")
		}
	}()

	ch := make(chan bool, 1)
	go func() {
		time.Sleep(100 * parentTimeout)
		ch <- true
	}()

	select {
	case <-ctx.Done():
		return
	case <-ch:
		fmt.Println("never happens")
	}

}
Output:

parent.Err() == context deadline exceeded
detached.Err() ==  <nil>
value equal: true
value closed: false
finished cleanup

func Register

func Register(hooks Hooks)

Register adds a new Hooks to the library's registry. If all functions in the Hooks are nil, Register will panic.

Types

type Hooks

type Hooks struct {
	// Inline functions are used to attach values from the src context to the dst
	// context in calls to Inline.
	//
	// Inline functions should assume that the calling function will wait for the
	// any calls after Inline to return before it returns, so they may just
	// copy values from src to dst.
	Inline func(dst, src context.Context) context.Context

	// Async functions wrap the run methods passed to Async. They should derive
	// values from the src context to add to the dst context and do any
	// setup/cleanup around those values.
	//
	// Async functions should assume that the calling function could return before
	// they finish, so if context values on src could be closed or otherwise
	// cleaned up, the wrapper should likely create new values and clean those up
	// rather than just copying the values from src to dst.
	Async func(dst, src context.Context, next func(ctx context.Context))
}

Hooks are functions that are called with specific detach helpers to attach values from the parent context to the new context. In order to be registered, at least one function in Hooks must be non-nil. Nil functions will be skipped.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL