ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #183 from paulgmiller/readiness

readiness does one time check

authored by

Paul Miller and committed by
GitHub
e351abc1 85eb3947

+86 -13
+42
cmd/careme/ready.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + "net/http" 7 + ) 8 + 9 + type readyOnce struct { 10 + done bool 11 + checks []func(context.Context) error 12 + } 13 + 14 + func (r *readyOnce) Ready(ctx context.Context) error { 15 + if r.done { 16 + return nil 17 + } 18 + for _, check := range r.checks { 19 + if err := check(ctx); err != nil { 20 + return err 21 + } 22 + } 23 + //not thread safe? only ever set to true 24 + r.done = true 25 + return nil 26 + } 27 + 28 + func (r *readyOnce) Add(f func(context.Context) error) { 29 + r.checks = append(r.checks, f) 30 + } 31 + 32 + func (r *readyOnce) ServeHTTP(w http.ResponseWriter, req *http.Request) { 33 + if err := r.Ready(req.Context()); err != nil { 34 + http.Error(w, "not ready: "+err.Error(), http.StatusServiceUnavailable) 35 + return 36 + } 37 + if _, err := w.Write([]byte("OK")); err != nil { 38 + slog.ErrorContext(req.Context(), "failed to write readiness response", "error", err) 39 + } 40 + 41 + w.WriteHeader(http.StatusOK) 42 + }
+6 -4
cmd/careme/web.go
··· 147 147 authClient.Logout(w, r) 148 148 }) 149 149 150 - mux.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) { 151 - if _, err := w.Write([]byte("OK")); err != nil { 152 - slog.ErrorContext(r.Context(), "failed to write readiness response", "error", err) 153 - } 150 + ro := &readyOnce{} 151 + ro.Add(generator.Ready) 152 + ro.Add(func(ctx context.Context) error { 153 + return locations.Ready(ctx, locationserver) 154 154 }) 155 + 156 + mux.Handle("/ready", ro) 155 157 156 158 mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { 157 159 w.Header().Set("Content-Type", "image/png") // <= without this, many UAs ignore it
+14 -9
cmd/careme/web_e2e_test.go
··· 1 1 package main 2 2 3 3 import ( 4 + "context" 4 5 "io" 5 6 "net/http" 6 - "net/http/cookiejar" 7 7 "net/http/httptest" 8 8 "net/url" 9 9 "path/filepath" ··· 27 27 defer srv.Close() 28 28 29 29 client := newTestClient(t) 30 + resp := mustGet(t, client, srv.URL+"/ready") //our readiness probe works even with mocks? 31 + if resp.StatusCode != http.StatusOK { 32 + t.Fatalf("expected /ready to return 200 OK, got %d", resp.StatusCode) 33 + } 30 34 31 35 // Step 1: query locations for 90005 and ensure it returns a /recipes?location link. 32 36 locationsBody := mustGetBody(t, client, srv.URL+"/locations?zip=90005") ··· 97 101 users.NewHandler(userStorage, locationServer, mockAuth).Register(mux) 98 102 recipes.NewHandler(cfg, userStorage, generator, locationServer, cacheStore, mockAuth).Register(mux) 99 103 104 + ro := &readyOnce{} 105 + ro.Add(generator.Ready) 106 + ro.Add(func(ctx context.Context) error { 107 + return locations.Ready(ctx, locationServer) 108 + }) 109 + 110 + mux.Handle("/ready", ro) 111 + 100 112 return httptest.NewServer(WithMiddleware(mux)) 101 113 } 102 114 103 115 func newTestClient(t *testing.T) *http.Client { 104 - t.Helper() 105 - jar, err := cookiejar.New(nil) 106 - if err != nil { 107 - t.Fatalf("failed to create cookie jar: %v", err) 108 - } 109 - return &http.Client{ 110 - Jar: jar, 111 - } 116 + return &http.Client{} 112 117 } 113 118 114 119 func mustGet(t *testing.T, client *http.Client, url string) *http.Response {
+9
internal/ai/client.go
··· 251 251 252 252 return messages, nil 253 253 } 254 + 255 + func (c *Client) Ready(ctx context.Context) error { 256 + // more CORRECT to do a very simple response request with allowed tokens 1 but this seems cheaper 257 + // https://chatgpt.com/share/6984da16-ff88-8009-8486-4e0479ac6a01 258 + // could only do it once to ensure startup 259 + client := openai.NewClient(option.WithAPIKey(c.apiKey)) 260 + _, err := client.Models.List(ctx) 261 + return err 262 + }
+5
internal/locations/locations.go
··· 111 111 return locations, nil 112 112 } 113 113 114 + func Ready(ctx context.Context, l locationGetter) error { 115 + _, err := l.GetLocationsByZip(ctx, "98005") //magic number is my zip code :) 116 + return err 117 + } 118 + 114 119 func Register(l locationGetter, mux *http.ServeMux) { 115 120 mux.HandleFunc("/locations", func(w http.ResponseWriter, r *http.Request) { 116 121 ctx := r.Context()
+5
internal/recipes/generator.go
··· 23 23 type aiClient interface { 24 24 GenerateRecipes(ctx context.Context, location *locations.Location, ingredients []kroger.Ingredient, instructions string, date time.Time, lastRecipes []string) (*ai.ShoppingList, error) 25 25 Regenerate(ctx context.Context, newinstruction string, conversationID string) (*ai.ShoppingList, error) 26 + Ready(ctx context.Context) error 26 27 } 27 28 28 29 type Generator struct { ··· 248 249 } 249 250 250 251 return ingredients, nil 252 + } 253 + 254 + func (g *Generator) Ready(ctx context.Context) error { 255 + return g.aiClient.Ready(ctx) 251 256 } 252 257 253 258 // toStr returns the string value if non-nil, or "empty" otherwise.
+4
internal/recipes/mock.go
··· 344 344 }, 345 345 } 346 346 347 + func (m mock) Ready(ctx context.Context) error { 348 + return nil 349 + } 350 + 347 351 func (m mock) GenerateRecipes(ctx context.Context, p *generatorParams) (*ai.ShoppingList, error) { 348 352 id := p.ConversationID 349 353 if id == "" {
+1
internal/recipes/server.go
··· 30 30 31 31 type generator interface { 32 32 GenerateRecipes(ctx context.Context, p *generatorParams) (*ai.ShoppingList, error) 33 + Ready(ctx context.Context) error 33 34 } 34 35 35 36 type server struct {