Skip to content

Commit

Permalink
Add generic future implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
ianlopshire committed Feb 11, 2023
1 parent 75d6807 commit 7ba6caa
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 5 deletions.
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,37 @@ Package `async` provides asynchronous primitives and utilities.

## Usage

`Latch` is a synchronization primitive that can be used to block until a desired state is reached.
`Future` is a generic type that represents a value that will be resolved at some point in
the future.

```go
type User struct {
ID int
Name string
}

The zero value for `Latch` is in an open (blocking) state. Use the package level `Resolve` function to resolve a Latch. Once resolved, the Latch cannot be reopened.
fut := new(async.Future[User])

// Simulate long computation or IO by sleeping before and resolving the future.
go func() {
time.Sleep(500 * time.Millisecond)
user := User{ID: 1, Name: "John Does"}

#### Custom Future
async.ResolveFuture(fut, user, nil)
}()

`Latch` is useful for implementing Futures.
// Block until the future is resolved.
user, err := fut.Value()

fmt.Println(user, err)
// output: {1 John Does} <nil>
```


### Custom Futures & `Latch`

`Latch` is a synchronization primitive that can be used to block until a desired state is reached.
It is useful for implementing custom future types.

```go
type User struct {
Expand Down
36 changes: 36 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,47 @@ package async_test

import (
"fmt"
"log"
"time"

"github.com/ianlopshire/go-async"
)

func ExampleFuture() {
// Create a new Future
fut := new(async.Future[string])

// Simulate long computation or IO by sleeping before setting the value and resolving
// the future.
go func() {
time.Sleep(500 * time.Millisecond)
async.ResolveFuture(fut, "Hello World!", nil)
}()

// Block until the Future is resolved.
v, err := fut.Value()
if err != nil {
log.Fatal(err)
}

fmt.Println(v, err)
// output: Hello World! <nil>
}

func ExampleFuture_select() {
fut := new(async.Future[string])

// The channel returned by Done() can be used directly in a select statement.
select {
case <-fut.Done():
fmt.Println(fut.Value())
default:
fmt.Println("Future not yet resolved")
}

// output: Future not yet resolved
}

func ExampleLatch() {
l := new(async.Latch)

Expand Down
55 changes: 55 additions & 0 deletions future.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package async

import (
"context"
)

// Future is a proxy for a result that is initially unknown.
//
// Use `new(Future[T])` to create a new Future. Use the package level ResolveFuture
// function to resolve a Latch.
//
// A Future must not be copied after first use.
type Future[T any] struct {
l Latch
v T
err error
}

// Done returns a channel that will be closed when the Future is resolved.
func (fut *Future[T]) Done() <-chan struct{} {
return fut.l.Done()
}

// Value blocks until the Future is resolved and returns resulting value and error.
//
// It is safe to call Value multiple times form multiple goroutines.
func (fut *Future[T]) Value() (T, error) {
Await(fut)
return fut.v, fut.err
}

// ValueCtx blocks until the Future is resolved or the context is canceled.
//
// If the context is canceled, the returned error will be the context's error. A canceled
// context does not necessarily mean that the Future was not resolved or will not resolve
// in the future.
//
// It is safe to call ValueCtx multiple times form multiple goroutines.
func (fut *Future[T]) ValueCtx(ctx context.Context) (T, error) {
if err := AwaitCtx(ctx, fut); err != nil {
var zero T
return zero, err
}
return fut.v, fut.err
}

// ResolveFuture resolves a Future and sets its value and error.
//
// Resolving a Future more than once will panic with ErrAlreadyResolved.
func ResolveFuture[T any](fut *Future[T], v T, err error) {
Resolve(&fut.l, func() {
fut.v = v
fut.err = err
})
}
93 changes: 93 additions & 0 deletions future_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package async_test

import (
"context"
"errors"
"testing"
"time"

"github.com/ianlopshire/go-async"
)

func TestFuture_alreadyResolved(t *testing.T) {
// When an already-resolved Future is resolved it should panic with ErrAlreadyResolved.
defer func() {
if recover() != async.ErrAlreadyResolved {
t.Fatal("expected Future to panic with ErrAlreadyResolved")
}
}()

fut := new(async.Future[string])
async.ResolveFuture(fut, "Hello World!", nil)
async.ResolveFuture(fut, "Hello World!", nil)
}

func TestFuture_Value(t *testing.T) {
for name, tt := range map[string]struct {
v string
err error
}{
"with value": {"Hello World!", nil},
"with error": {"", errors.New("error")},
} {
t.Run(name, func(t *testing.T) {
fut := new(async.Future[string])
async.ResolveFuture(fut, tt.v, tt.err)

v, err := fut.Value()
if err != tt.err {
t.Fatalf("Value() unexpected error have %v, want %v", err, tt.err)
}
if v != tt.v {
t.Fatalf("Value() unexpected value have %v, want %v", v, tt.v)
}
})
}
}

func TestFuture_ValueCtx(t *testing.T) {
for name, tt := range map[string]struct {
v string
err error
}{
"with value": {"Hello World!", nil},
"with error": {"", errors.New("error")},
} {
t.Run(name, func(t *testing.T) {
fut := new(async.Future[string])
async.ResolveFuture(fut, tt.v, tt.err)

v, err := fut.ValueCtx(context.Background())
if err != tt.err {
t.Fatalf("Value() unexpected error have %v, want %v", err, tt.err)
}
if v != tt.v {
t.Fatalf("Value() unexpected value have %v, want %v", v, tt.v)
}
})
}

t.Run("with canceled context", func(t *testing.T) {
timeout := time.After(time.Second)
done := make(chan bool)

go func() {
ctx, cancel := context.WithCancel(context.Background())
cancel()

fut := new(async.Future[string])
_, err := fut.ValueCtx(ctx)
if err != context.Canceled {
t.Errorf("ValueCtx() unexpected error have %v, want %v", err, context.Canceled)
}

done <- true
}()

select {
case <-timeout:
t.Fatal("ValueCtx() future should not have blocked")
case <-done:
}
})
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/ianlopshire/go-async

go 1.13
go 1.18

0 comments on commit 7ba6caa

Please sign in to comment.