Skip to content

Commit 06fb565

Browse files
authored
c: able to join contexts (#20)
1 parent ac9acc9 commit 06fb565

4 files changed

Lines changed: 418 additions & 2 deletions

File tree

README.md

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,123 @@ maintaining 100% compatibility.
1111

1212
### Requirements
1313

14-
The minimum Go version is `go1.23`.
14+
The minimum Go version is `go1.26`.
1515

1616
### Install
1717

18-
The `forms` package can be added to a project with `go get`.
18+
The `scope` package can be added to a project with `go get`.
1919

2020
```shell
2121
go get -u cattlecloud.net/go/scope@latest
2222
```
2323

24+
### Examples
25+
26+
##### New
27+
28+
```go
29+
ctx := scope.New()
30+
```
31+
32+
##### TTL
33+
34+
```go
35+
ctx, cancel := scope.TTL(5 * time.Second)
36+
// ctx is canceled after 5 seconds
37+
defer cancel()
38+
```
39+
40+
##### Deadline
41+
42+
```go
43+
ctx, cancel := scope.Deadline(time.Now().Add(10 * time.Second))
44+
// ctx is canceled at the specified time
45+
defer cancel()
46+
```
47+
48+
##### Cancelable
49+
50+
```go
51+
ctx, cancel := scope.Cancelable()
52+
// ctx can be canceled manually
53+
defer cancel()
54+
```
55+
56+
##### WithCancel
57+
58+
```go
59+
ctx, cancel := scope.WithCancel(parentCtx)
60+
defer cancel()
61+
```
62+
63+
##### WithTTL
64+
65+
```go
66+
ctx, cancel := scope.WithTTL(parentCtx, 3 * time.Second)
67+
// parentCtx with a 3 second timeout
68+
defer cancel()
69+
```
70+
71+
##### WithValue
72+
73+
```go
74+
ctx := scope.WithValue(parentCtx, "userID", 123)
75+
```
76+
77+
##### Value
78+
79+
```go
80+
userID := scope.Value[int](ctx, "userID")
81+
```
82+
83+
##### Join
84+
85+
```go
86+
ctx1, cancel1 := scope.WithCancel(scope.New())
87+
ctx2, cancel2 := scope.TTL(5 * time.Second)
88+
89+
joined, cancel := scope.Join(ctx1, ctx2)
90+
// joined is canceled when ctx1 or ctx2 is canceled
91+
defer cancel()
92+
defer cancel1()
93+
defer cancel2()
94+
```
95+
96+
###### Deadline
97+
98+
```go
99+
ctx1, _ := scope.Deadline(time.Now().Add(10 * time.Second))
100+
ctx2, _ := scope.Deadline(time.Now().Add(20 * time.Second))
101+
102+
joined, _ := scope.Join(ctx1, ctx2)
103+
deadline, ok := joined.Deadline() // deadline is 10 seconds, ok is true
104+
```
105+
106+
###### Done
107+
108+
```go
109+
joined, cancel := scope.Join(ctx1, ctx2)
110+
<-joined.Done() // blocks until either ctx1 or ctx2 is done
111+
```
112+
113+
###### Err
114+
115+
```go
116+
joined, cancel := scope.Join(ctx1, ctx2)
117+
<-joined.Done()
118+
err := joined.Err() // returns the error from the first canceled context
119+
```
120+
121+
###### Value
122+
123+
```go
124+
ctx1 := scope.WithValue(scope.New(), "key", "value1")
125+
ctx2 := scope.WithValue(scope.New(), "key", "value2")
126+
127+
joined, _ := scope.Join(ctx1, ctx2)
128+
val := joined.Value("key") // returns value1 (ctx1's value is checked first)
129+
```
130+
24131
### License
25132

26133
The `cattlecloud.net/go/scope` module is open source under the [BSD](LICENSE) license.

context.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ func TTL(duration time.Duration) (C, Cancel) {
2727
return context.WithTimeout(New(), duration)
2828
}
2929

30+
// Deadline will create a fresh context not part of any preceding chain of
31+
// values, and will expire at the given expiration time.
32+
func Deadline(expiration time.Time) (C, Cancel) {
33+
return context.WithDeadline(New(), expiration)
34+
}
35+
3036
// Cancelable will create a fresh context not part of any preceding chain of
3137
// values, and includes a Cancel function.
3238
func Cancelable() (C, Cancel) {

join.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package scope
2+
3+
import (
4+
"context"
5+
"sync"
6+
"time"
7+
)
8+
9+
// join implements a context that is canceled when either of two
10+
// contexts is canceled. It is a variation on the original implementation
11+
// with race condition bug fixes.
12+
//
13+
// https://github.com/LK4D4/joincontext/blob/master/context.go
14+
type join struct {
15+
once sync.Once
16+
a C
17+
b C
18+
done chan struct{}
19+
20+
lock *sync.Mutex
21+
err error
22+
}
23+
24+
// Join combines two contexts into a single context that is canceled when
25+
// either input context is canceled. The returned context's Deadline,
26+
// Value, and Err methods delegate to the earliest of the two input
27+
// contexts. If either context is already done, the returned context is
28+
// immediately done with that context's error.
29+
func Join(a, b C) (C, Cancel) {
30+
j := &join{
31+
a: a,
32+
b: b,
33+
done: make(chan struct{}),
34+
lock: new(sync.Mutex),
35+
}
36+
37+
// check if either context is already done before spawning the goroutine
38+
select {
39+
case <-a.Done():
40+
j.lock.Lock()
41+
j.err = a.Err()
42+
j.lock.Unlock()
43+
44+
close(j.done)
45+
return j, func() {}
46+
47+
case <-b.Done():
48+
j.lock.Lock()
49+
j.err = b.Err()
50+
j.lock.Unlock()
51+
52+
close(j.done)
53+
return j, func() {}
54+
55+
default:
56+
}
57+
58+
go j.run()
59+
return j, j.cancel
60+
}
61+
62+
// Deadline returns the earliest deadline from either context.
63+
// If neither context has a deadline, ok is false.
64+
func (j *join) Deadline() (deadline time.Time, ok bool) {
65+
a, aok := j.a.Deadline()
66+
if !aok {
67+
return j.b.Deadline()
68+
}
69+
70+
b, bok := j.b.Deadline()
71+
if !bok {
72+
return a, true
73+
}
74+
75+
if b.Before(a) {
76+
return b, true
77+
}
78+
79+
return a, true
80+
}
81+
82+
// Done returns a channel that is closed when either context is done.
83+
func (j *join) Done() <-chan struct{} {
84+
return j.done
85+
}
86+
87+
// Err returns the error from whichever context was canceled first,
88+
// or ErrCanceled if Cancel was called on the joined context.
89+
func (j *join) Err() error {
90+
j.lock.Lock()
91+
defer j.lock.Unlock()
92+
return j.err
93+
}
94+
95+
// Value returns the value associated with key in either context,
96+
// prioritizing the first context's value if present.
97+
func (j *join) Value(key any) any {
98+
v := j.a.Value(key)
99+
100+
if v == nil {
101+
v = j.b.Value(key)
102+
}
103+
104+
return v
105+
}
106+
107+
func (j *join) run() {
108+
select {
109+
case <-j.a.Done():
110+
j.once.Do(func() {
111+
j.lock.Lock()
112+
j.err = j.a.Err()
113+
j.lock.Unlock()
114+
close(j.done)
115+
})
116+
case <-j.b.Done():
117+
j.once.Do(func() {
118+
j.lock.Lock()
119+
j.err = j.b.Err()
120+
j.lock.Unlock()
121+
close(j.done)
122+
})
123+
case <-j.done:
124+
return
125+
}
126+
}
127+
128+
func (j *join) cancel() {
129+
j.once.Do(func() {
130+
j.lock.Lock()
131+
j.err = context.Canceled
132+
j.lock.Unlock()
133+
close(j.done)
134+
})
135+
}

0 commit comments

Comments
 (0)