11package main
22
33import (
4+ "cmp"
45 _ "embed"
56 "encoding/json"
67 "fmt"
78 "math/rand"
89 "net"
910 "net/http"
1011 "os"
11- "strconv"
12- "strings"
1312 "sync"
14- "sync/atomic"
1513 "time"
1614
1715 "github.com/go-chi/chi/v5"
1816 "github.com/go-chi/chi/v5/middleware"
1917 "github.com/go-chi/render"
18+ "github.com/google/uuid"
2019 "github.com/r3labs/sse/v2"
2120 "golang.org/x/exp/maps"
2221)
2322
23+ const (
24+ // maximum number of entities returned in a single request
25+ limit = 100
26+ )
27+
2428var (
2529 // hostname used by the server
26- hostname = envOrDefault ( "HTTP_HOST" , "localhost" )
30+ hostname = cmp . Or ( os . Getenv ( "HTTP_HOST" ) , "localhost" )
2731
2832 // port used by the server
29- port = envOrDefault ("PORT" , "3000" )
30-
31- // maximum number of entities returned in a single request
32- limit = 100
33+ port = cmp .Or (os .Getenv ("PORT" ), "3000" )
3334)
3435
3536//go:embed data.json
@@ -43,22 +44,18 @@ func main() {
4344
4445 r .Use (middleware .Logger )
4546 r .Use (middleware .Recoverer )
46- r .Use (middleware .Timeout (60 * time .Second ))
47+ r .Use (middleware .Timeout (30 * time .Second ))
4748
4849 r .Handle ("/events" , http .HandlerFunc (events .ServeHTTP ))
4950
5051 r .Group (func (r chi.Router ) {
5152 r .Use (render .SetContentType (render .ContentTypeJSON ))
5253 r .Use (chaosMiddleware )
5354 r .Get ("/chats" , app .GetChats )
54- r .Get ("/chats/{chatID}/messages" , app .GetMessages )
55- r .Post ("/chats/{chatID}/messages" , app .PostMessages )
55+ r .Get ("/chats/{chatID}/messages" , app .GetChatMessages )
56+ r .Post ("/chats/{chatID}/messages" , app .PostChatMessages )
5657 })
5758
58- r .Get ("/livez" , http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
59- w .WriteHeader (http .StatusOK )
60- }))
61-
6259 addr := net .JoinHostPort (hostname , port )
6360 fmt .Printf ("Starting server on http://%s\n " , addr )
6461 if err := http .ListenAndServe (addr , r ); err != nil {
@@ -71,18 +68,19 @@ func main() {
7168//
7269
7370type Chat struct {
74- ID uint32 `json:"id"`
75- Name string `json:"name"`
71+ ID uuid. UUID `json:"id"`
72+ Name string `json:"name"`
7673
7774 messages []* Message
7875}
7976
8077type Message struct {
81- ID uint32 `json:"id"`
82- ChatID uint32 `json:"chat_id"`
83- Author string `json:"author"`
84- Text string `json:"text"`
85- SentAt time.Time `json:"sent_at"`
78+ ID uuid.UUID `json:"id"`
79+ ChatID uuid.UUID `json:"chat_id"`
80+ Author string `json:"author"`
81+ Text string `json:"text"`
82+ SentAt time.Time `json:"sent_at"`
83+ IdempotencyKey string `json:"idempotency_key"`
8684}
8785
8886// App is both our controller and our data store. This coupling allows to keep
@@ -93,7 +91,7 @@ type App struct {
9391
9492 mu sync.RWMutex
9593 idempotencyKeys map [string ]struct {} // store idempotency keys
96- store map [uint32 ]* Chat // in-memory store of chats and messages
94+ store map [uuid. UUID ]* Chat // in-memory store of chats and messages
9795}
9896
9997func NewApp (events * sse.Server ) * App {
@@ -107,22 +105,21 @@ func NewApp(events *sse.Server) *App {
107105 now := time .Now ()
108106
109107 // store contains all the chats, indexed by their ID
110- store := map [uint32 ]* Chat {}
111- for i , chat := range []* Chat {
112- {Name : "John" , messages : []* Message {
113- {ID : newID (), Author : "bot" , Text : "Sounds good 👍" , SentAt : now },
108+ store := map [uuid. UUID ]* Chat {}
109+ for _ , chat := range []* Chat {
110+ {ID : uuid . New (), Name : "John" , messages : []* Message {
111+ {ID : uuid . New (), Author : "bot" , Text : "Sounds good 👍" , SentAt : now , IdempotencyKey : uuid . New (). String () },
114112 }},
115- {Name : "Jessica" , messages : []* Message {
116- {ID : newID (), Author : "bot" , Text : "How are you!?" , SentAt : now .Add (- 1 * time .Minute )},
113+ {ID : uuid . New (), Name : "Jessica" , messages : []* Message {
114+ {ID : uuid . New (), Author : "bot" , Text : "How are you!?" , SentAt : now .Add (- 1 * time .Minute ), IdempotencyKey : uuid . New (). String ( )},
117115 }},
118- {Name : "Matt" , messages : []* Message {
119- {ID : newID (), Author : "bot" , Text : "ok chat soon :)" , SentAt : now .Add (- 32 * time .Minute )},
116+ {ID : uuid . New (), Name : "Matt" , messages : []* Message {
117+ {ID : uuid . New (), Author : "bot" , Text : "ok chat soon :)" , SentAt : now .Add (- 32 * time .Minute ), IdempotencyKey : uuid . New (). String ( )},
120118 }},
121- {Name : "Sarah" , messages : []* Message {
122- {ID : newID (), Author : "bot" , Text : "ok talk later!" , SentAt : now .Add (- 24 * time .Hour )},
119+ {ID : uuid . New (), Name : "Sarah" , messages : []* Message {
120+ {ID : uuid . New (), Author : "bot" , Text : "ok talk later!" , SentAt : now .Add (- 24 * time .Hour ), IdempotencyKey : uuid . New (). String ( )},
123121 }},
124122 } {
125- chat .ID = uint32 (i + 1 ) // we don't want to have a chat id == 0
126123 for _ , message := range chat .messages {
127124 message .ChatID = chat .ID
128125 }
@@ -147,7 +144,7 @@ func (app *App) GetChats(w http.ResponseWriter, r *http.Request) {
147144 render .JSON (w , r , chats )
148145}
149146
150- func (app * App ) GetMessages (w http.ResponseWriter , r * http.Request ) {
147+ func (app * App ) GetChatMessages (w http.ResponseWriter , r * http.Request ) {
151148 app .mu .RLock ()
152149 defer app .mu .RUnlock ()
153150
@@ -164,7 +161,7 @@ func (app *App) GetMessages(w http.ResponseWriter, r *http.Request) {
164161 render .JSON (w , r , chat .messages [start :end ])
165162}
166163
167- func (app * App ) PostMessages (w http.ResponseWriter , r * http.Request ) {
164+ func (app * App ) PostChatMessages (w http.ResponseWriter , r * http.Request ) {
168165 app .mu .Lock ()
169166 defer app .mu .Unlock ()
170167
@@ -192,18 +189,21 @@ func (app *App) PostMessages(w http.ResponseWriter, r *http.Request) {
192189 }
193190
194191 newMessage := & Message {
195- ID : newID (),
196- ChatID : chat .ID ,
197- Author : "user" ,
198- Text : message .Text ,
199- SentAt : time .Now (),
192+ ID : uuid .New (),
193+ ChatID : chat .ID ,
194+ Author : "user" ,
195+ Text : message .Text ,
196+ SentAt : time .Now (),
197+ IdempotencyKey : idempotencyKey ,
200198 }
201199
202200 chat .messages = append (chat .messages , newMessage )
203201 app .publishEvent ("messages" , newMessage )
204- app . idempotencyKeys [ idempotencyKey ] = struct {}{}
202+
205203 go app .sendDelayedAnswer (chat )
206204
205+ app .idempotencyKeys [idempotencyKey ] = struct {}{}
206+
207207 w .WriteHeader (http .StatusOK )
208208 render .JSON (w , r , newMessage )
209209}
@@ -217,26 +217,27 @@ func (app *App) sendDelayedAnswer(chat *Chat) {
217217 text := app .jokes [rand .Intn (len (app .jokes ))]
218218
219219 newMessage := & Message {
220- ID : newID (),
221- ChatID : chat .ID ,
222- Author : "bot" ,
223- Text : text ,
224- SentAt : time .Now (),
220+ ID : uuid .New (),
221+ ChatID : chat .ID ,
222+ Author : "bot" ,
223+ Text : text ,
224+ SentAt : time .Now (),
225+ IdempotencyKey : uuid .New ().String (),
225226 }
226227
227228 chat .messages = append (chat .messages , newMessage )
228229 app .publishEvent ("messages" , newMessage )
229230}
230231
231232func (app * App ) findChatByID (rawID string ) (* Chat , error ) {
232- chatID , err := strconv . ParseUint (rawID , 10 , 32 )
233+ parsedID , err := uuid . Parse (rawID )
233234 if err != nil {
234235 return nil , fmt .Errorf ("invalid chat ID %s: %w" , rawID , err )
235236 }
236237
237- chat , ok := app .store [uint32 ( chatID ) ]
238+ chat , ok := app .store [parsedID ]
238239 if ! ok {
239- return nil , fmt .Errorf ("chat %s not found" , rawID )
240+ return nil , fmt .Errorf ("chat not found with ID %s " , rawID )
240241 }
241242
242243 return chat , nil
@@ -253,28 +254,6 @@ func (app *App) publishEvent(stream string, payload any) {
253254 }
254255}
255256
256- //
257- // Helpers
258- //
259-
260- // lastID is the last ID generated by newID. It starts counting from the current
261- // timestamp in seconds.
262- var lastID = uint32 (time .Now ().Unix ())
263-
264- // newID generates a new ID, unique for the lifetime of this server.
265- func newID () uint32 {
266- return atomic .AddUint32 (& lastID , 1 )
267- }
268-
269- // envOrDefault returns the value of the environment variable at the given key.
270- // Fallbacks to the given default if the value found is missing or empty.
271- func envOrDefault (key string , defaultValue string ) string {
272- if v := strings .TrimSpace (os .Getenv (key )); len (v ) > 0 {
273- return v
274- }
275- return defaultValue
276- }
277-
278257// chaosMiddleware makes sure to randomly return errors and adds artificial
279258// latency to simulate a real world system.
280259func chaosMiddleware (next http.Handler ) http.Handler {
@@ -288,7 +267,7 @@ func chaosMiddleware(next http.Handler) http.Handler {
288267 return
289268 }
290269
291- // 5% chance of timeout
270+ // 5% chance of timeout after 5 seconds
292271 if rand .Intn (100 ) < 5 {
293272 <- time .After (5 * time .Second )
294273 http .Error (w , "timeout" , http .StatusGatewayTimeout )
0 commit comments