adding context

master
Drew Bednar 3 months ago
parent 630fe8ee45
commit de64f65984

@ -0,0 +1,24 @@
# Using Context
Remember that a common pattern of signaling events or completion is using a blocking channel that receives and empty struct. An empty struct has zero memory footprint. It is typically used to signal completion or stopping.
The Context.Done() returns a channel that is closed when this Context is canceled `Done() <-chan struct{}`. Contexts have a root and can form a parent child tree structure. A child is "derived" from a parent context and when a Context is cancelled all derived contexts are also canceled. WithCancel and WithTimeout return derived Context values that can be canceled sooner than the parent Context.
> 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 WithCancel, WithDeadline, WithTimeout, or WithValue. When a Context is canceled, all Contexts derived from it are also canceled.
## What about Context.Value
The problem with context.Values is that it's just an untyped map so you have no type-safety and you have to handle it not actually containing your value. You have to create a coupling of map keys from one module to another and if someone changes something things start breaking.
In short, if a function needs some values, put them as typed parameters rather than trying to fetch them from
context.Value
. This makes it statically checked and documented for everyone to see.
Context.Value is good for things like "A trace ID". It's information that is not needed for by every function in the call stack.
> Context.Value should inform not control. The content of context.Value is for maintainers not users. It should never be required input for documented or expected results.
## Readings
https://go.dev/blog/context
https://go.dev/blog/pipelines

@ -0,0 +1,47 @@
package my_context
import (
"context"
"fmt"
"net/http"
)
type Store interface {
Fetch(ctx context.Context) (string, error)
// instead of calling cancel we want to use context above to cross API boundries
// Cancel()
}
func Server(store Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// THIS WAS WHAT WE DID WHEN WE DIDN"T PROPOGATE CONTEXT
// // This is the requests ctx not a derived context
// ctx := r.Context()
// data := make(chan string, 1)
// go func() {
// data <- store.Fetch()
// }()
// // Here we are simply waiting on the which channel
// // will close out first. If the Done() channel on the request
// // does store.Cancel() is called.
// // Remember select lets use wait on multiple async operations and execute cases
// // based on which one completes first as signaled by data being received on that channel.
// select {
// case d := <-data:
// fmt.Fprint(w, d)
// case <-ctx.Done():
// store.Cancel()
// }
data, err := store.Fetch(r.Context())
if err != nil {
return // todo log the error however you like
}
fmt.Fprint(w, data)
}
}

@ -0,0 +1,175 @@
package my_context
import (
"context"
"errors"
"log"
"net/http"
"net/http/httptest"
"testing"
"time"
)
// THIS WAS WHAT WE DID WHEN WE DIDN"T PROPOGATE CONTEXT
// type SpyStore struct {
// response string
// cancelled bool
// t *testing.T
// }
// func (s *SpyStore) assertWasCancelled() {
// s.t.Helper()
// if !s.cancelled {
// s.t.Error("store was not told to cancel")
// }
// }
// func (s *SpyStore) assertWasNotCancelled() {
// s.t.Helper()
// if s.cancelled {
// s.t.Error("store was told to cancel")
// }
// }
// func (s *SpyStore) Fetch() string {
// time.Sleep(100 * time.Millisecond)
// return s.response
// }
// func (s *SpyStore) Cancel() {
// s.cancelled = true
// }
// func TestServer(t *testing.T) {
// data := "Hello, Gopher"
// t.Run("Happy path", func(t *testing.T) {
// store := &SpyStore{response: data, t: t}
// svr := Server(store)
// request := httptest.NewRequest(http.MethodGet, "/", nil)
// response := httptest.NewRecorder()
// svr.ServeHTTP(response, request)
// if response.Body.String() != data {
// t.Errorf(`got "%s" want "%s"`, response.Body.String(), data)
// }
// store.assertWasNotCancelled()
// })
// t.Run("Should cancel", func(t *testing.T) {
// store := &SpyStore{response: data, t: t}
// svr := Server(store)
// request := httptest.NewRequest(http.MethodGet, "/", nil)
// // we are copying the parent context but 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.
// cancellingCtx, cancel := context.WithCancel(request.Context())
// time.AfterFunc(5*time.Millisecond, cancel)
// // changing the current request context to our new cancellingCtx
// request = request.WithContext(cancellingCtx)
// response := httptest.NewRecorder()
// svr.ServeHTTP(response, request)
// store.a
// })
// }
type SpyStore struct {
response string
t *testing.T
}
func (s *SpyStore) Fetch(ctx context.Context) (string, error) {
data := make(chan string, 1)
go func() {
var result string
for _, c := range s.response {
select {
case <-ctx.Done():
log.Println("spy store got cancelled")
return
default:
// simulates some network traffic?
time.Sleep(10 * time.Millisecond)
result += string(c)
}
}
data <- result
}()
select {
case <-ctx.Done():
return "", ctx.Err()
case res := <-data:
return res, nil
}
}
// Rolling our own Spy for ResponseWriter because httptest.ResponseRecorder can't be
// used to determine if any response was writing in the error case
// SpyResponse Writer implements http.ResponseWriter so we can use it in the test.
type SpyResponseWriter struct {
written bool
}
func (s *SpyResponseWriter) Header() http.Header {
s.written = true
return nil
}
func (s *SpyResponseWriter) Write([]byte) (int, error) {
s.written = true
return 0, errors.New("not implemented")
}
func (s *SpyResponseWriter) WriteHeader(statusCode int) {
s.written = true
}
func TestServer(t *testing.T) {
data := "Hello, Gopher"
t.Run("Happy Path", func(t *testing.T) {
store := &SpyStore{response: data, t: t}
svr := Server(store)
request := httptest.NewRequest(http.MethodGet, "/", nil)
response := httptest.NewRecorder()
svr.ServeHTTP(response, request)
if response.Body.String() != data {
t.Errorf(`got "%s", want "%s"`, response.Body.String(), data)
}
})
t.Run("tells store to cancel work if request is cancelled", func(t *testing.T) {
store := &SpyStore{response: data, t: t}
svr := Server(store)
request := httptest.NewRequest(http.MethodGet, "/", nil)
cancellingCtx, cancel := context.WithCancel(request.Context())
time.AfterFunc(5*time.Millisecond, cancel)
request = request.WithContext(cancellingCtx)
response := &SpyResponseWriter{}
svr.ServeHTTP(response, request)
if response.written {
t.Errorf("A response should not have been written")
}
})
}
Loading…
Cancel
Save