context

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Dec 2, 2023 License: GPL-3.0 Imports: 1 Imported by: 0

README

context


Unfortunately, the standard golang context package does not control the closing order of child contexts (issue #51075).
(the parent context could exit earlier than his child, and in this case, you could get unpredicted execution behavior when you try to use some parent resources that are already closed)

To resolve this issue, here is another implementation of this context pattern.
It waits until all child contexts correctly close (parent event loop would be available for servicing their children). Only when all children would be closed parents will exits too.

Documentation: GoDoc
How to use it:

A full example can be found here: examples_test.go

1. Add a new context import to your project:
import (
	context "github.com/mcfly722/context"
)
2. Implement context.ContextedInstance interface with your node

Your node should contain Go(..) method:

type node struct {
	name: string,
}

func (node *node) Go(current context.Context) {
loop:
	for {
		select {
		case _, isOpened := <-current.Context(): // this method returns context channel. If it closes, it means that we need to finish select loop
			if !isOpened {
				break loop
			}
		default: // you can use default or not, it works in both cases
			{
			}
		}
	}
	fmt.Printf("context %v closed\n", node.name)
}
3. Create your node instance(s)
node0 := &node{name : "root"}
node1 := &node{name : "1"}
node2 := &node{name : "2"}
node3 := &node{name : "3"}
3. Create root context
ctx0 := context.NewRootContext(node0)
4. Now you can inherit from the root context and create child and subchild contexts:
ctx1, err := ctx0.NewContextFor(node1)
if err != nil {
	// child context is not created successfully, possibly the parent is in a closing state, you just need to exit
} else {
	// child context created successfully
}
ctx2, err := ctx1.NewContextFor(node2)
...
ctx3, err := ctx2.NewContextFor(node3)
...
5. Closing
ctx0.Close()

It would close all contexts in reverse order: 3->2->1->root.

Restrictions
  1. Do not exit from your context goroutine without checking that current.Context() channel is closed. It is a potential lock or race, and this library restricts it (panic occurs especially to exclude this code mistake).
  2. Always check NewContextFor(...) error. A parent could be in a closed state; in this case, a child would not be created.
Common questions
  1. Is there any send method to send some control messages from parent to child to change their state?
    No. The only possible way to implement this without races, is to use the same channel that currently used for closing. Unfortunately, GoLang has library race between channel.Send and channel.Close methods (see issue #30372).
  2. I want to wait until child context is closed. Where is context.Wait()?
    context.Wait() is a race condition potential mistake. You send close to the child and wait for the parent, but children at this moment do not know anything about closing. It continues to send data to parents through channels. Parent blocked, it waits with context.Wait(). The child was also blocked on channel sending. It is a full dead block.
  3. Why does rootContext.Wait() exist?
    rootContext has its own empty goroutine loop without any send or receive, so deadblock from scenario 3 is not possible.
  4. Where is 'Deadlines','Timeouts','Values' like in the original context?
    It's all sugar.
    This timeout or deadlines you can implement in your select loop (see: <-time.After(...) or <-time.Tick(...))
    For values, use the constructor function with parameters.
  5. Is it possible to add same instance more than ones to different parents?
    Yes, this feature is supported. In this case, the new child goroutine would not start a second time, several parents just waited for the same instance to close.

Documentation

Overview

Package implements a graceful shutdown context tree for your goroutines.

It means that parent context wouldn't close until all its children's worked.

example:

You are creating a context tree:

root => child1 => child2 => child3

and trying to close the root.

All subchilds will close in reverse order (first - child3, then child2, child1, root). This closing order is absolutely essential because the child context could use some parent resources or send some signals to the parent. If a parent closes before the child, it will cause undefined behavior or goroutine locking.

Unfortunately, context from the standard Go library does not guarantee this closing order. See issue: https://github.com/golang/go/issues/51075

This module resolves this problem and guarantees a correct closing order.

Example
package main

import (
	"fmt"
	"time"

	context "github.com/mcfly722/context"
)

type node struct {
	name string
}

func newNode(name string) *node {
	return &node{name: name}
}

func (node *node) getName() string {
	return node.name
}

// this method node should implement as Goroutine loop
func (node *node) Go(current context.Context) {
loop:
	for {
		select {
		case _, isOpened := <-current.Context(): // this method returns context channel. If it closes, it means that we need to finish select loop
			if !isOpened {
				break loop
			}
		default: // you can use default or not, it works in both cases
			{
			}
		}
	}
	fmt.Printf("4. context '%v' closed\n", node.getName())
}

func main() {

	rootContext := context.NewRootContext(newNode("root"))
	child1Context, _ := rootContext.NewContextFor(newNode("child1"))
	child2Context, _ := child1Context.NewContextFor(newNode("child2"))
	child2Context.NewContextFor(newNode("child3"))

	fmt.Printf("1. now waiting for 1 sec...\n")

	go func() {
		time.Sleep(1 * time.Second)
		fmt.Printf("3. one second pass\n")
		rootContext.Close()
	}()

	rootContext.Wait()

	fmt.Printf("5. end\n")

}
Output:

1. now waiting for 1 sec...
3. one second pass
4. context 'child3' closed
4. context 'child2' closed
4. context 'child1' closed
4. context 'root' closed
5. end

Index

Examples

Constants

View Source
const ExitFromContextWithoutClosePanic customPanic = "Exit from Context Without Close() method"

Variables

This section is empty.

Functions

This section is empty.

Types

type ChildContext added in v1.0.2

type ChildContext interface {

	// create a new child context, for instance, what implements the instance interface
	NewContextFor(instance ContextedInstance) (ChildContext, error)

	// Close current context
	Close()
}

ChildContext obtained from the [NewContextFor] function.

Any child could have several subchilds.

During closing, this child would be a parent for all its sub-childs.

type ClosingIsInProcessForDisposingError added in v1.0.6

type ClosingIsInProcessForDisposingError struct{}

func (*ClosingIsInProcessForDisposingError) Error added in v1.0.6

type ClosingIsInProcessForFreezeError added in v1.0.6

type ClosingIsInProcessForFreezeError struct{}

func (*ClosingIsInProcessForFreezeError) Error added in v1.0.6

type Context

type Context interface {

	// creates a new child context, for instance, what implements ContextedInstance interface
	NewContextFor(instance ContextedInstance) (ChildContext, error)

	// When this channel closes, it means that the child context should exit from the Go function.
	Context() chan struct{}

	// Close the current context and all children in reverse order.
	Close()
}

Instances of this interface are sent to your node through the Go() method.

(see ContextedInstance)

type ContextedInstance

type ContextedInstance interface {
	Go(current Context)
}

This interface should be implemented by your nodes.

The module automatically starts Go(...) method with the current Context and automatically waits until it ends. You could not exit from this method without context closing (otherwise ExitFromContextWithoutClosePanic occurs).

Example:

type node struct {}
func (node *node) Go(current context.Context) {
	loop:
	for {
		select {
		case _, isOpened := <-current.Context():
			if !isOpened {
				break loop
			}
		}
	}
}

type RootContext

type RootContext interface {

	// Creates new Context from your instance what implements [ContextedInstance] interface.
	// If current root context is already in closing state it returns [ClosingIsInProcessForFreezeError] or [ClosingIsInProcessForDisposingError]
	NewContextFor(instance ContextedInstance) (ChildContext, error)

	// Waits till current root context would be Closeed.
	Wait()

	// Close current root context and all childs according reverse order.
	Close()
}

The RootContext interface is returned by the NewRootContext function.

func NewRootContext

func NewRootContext(instance ContextedInstance) RootContext

NewRootContext function generates and starts new root context

Jump to

Keyboard shortcuts

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