Compare commits
No commits in common. 'master' and 'drew/sql-it' have entirely different histories.
master
...
drew/sql-i
@ -1,26 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
https://faiface.github.io/post/context-should-go-away-go2/
|
|
@ -1,3 +0,0 @@
|
|||||||
module my_context
|
|
||||||
|
|
||||||
go 1.23.1
|
|
@ -1,47 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,175 +0,0 @@
|
|||||||
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…
Reference in New Issue