diff options
Diffstat (limited to 'srv/src/http/static/component-oriented-design')
4 files changed, 0 insertions, 1191 deletions
diff --git a/srv/src/http/static/component-oriented-design/v1/main.go b/srv/src/http/static/component-oriented-design/v1/main.go deleted file mode 100644 index 490a516..0000000 --- a/srv/src/http/static/component-oriented-design/v1/main.go +++ /dev/null @@ -1,314 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "log" - "math/rand" - "net" - "net/http" - "os" - "sort" - "strconv" - "sync" - "time" -) - -// Logger describes a simple component used for printing log lines. -type Logger interface { - Printf(string, ...interface{}) -} - -//////////////////////////////////////////////////////////////////////////////// -// The scoreboard component - -// File wraps the standard os.File type. -type File interface { - io.ReadWriter - Truncate(int64) error - Seek(int64, int) (int64, error) -} - -// scoreboard loads player scores from a save file, tracks score updates, and -// periodically saves those scores back to the save file. -type scoreboard struct { - file File - scoresM map[string]int - scoresLock sync.Mutex - - // this field will only be set in tests, and is used to synchronize with the - // the for-select loop in saveLoop. - saveLoopWaitCh chan struct{} -} - -// newScoreboard initializes a scoreboard using scores saved in the given File -// (which may be empty). The scoreboard will rewrite the save file with the -// latest scores everytime saveTicker is written to. -func newScoreboard(file File, saveTicker <-chan time.Time, logger Logger) (*scoreboard, error) { - fileBody, err := ioutil.ReadAll(file) - if err != nil { - return nil, fmt.Errorf("reading saved scored: %w", err) - } - - scoresM := map[string]int{} - if len(fileBody) > 0 { - if err := json.Unmarshal(fileBody, &scoresM); err != nil { - return nil, fmt.Errorf("decoding saved scores: %w", err) - } - } - - scoreboard := &scoreboard{ - file: file, - scoresM: scoresM, - saveLoopWaitCh: make(chan struct{}), - } - - go scoreboard.saveLoop(saveTicker, logger) - - return scoreboard, nil -} - -func (s *scoreboard) guessedCorrect(name string) int { - s.scoresLock.Lock() - defer s.scoresLock.Unlock() - - s.scoresM[name] += 1000 - return s.scoresM[name] -} - -func (s *scoreboard) guessedIncorrect(name string) int { - s.scoresLock.Lock() - defer s.scoresLock.Unlock() - - s.scoresM[name] -= 1 - return s.scoresM[name] -} - -func (s *scoreboard) scores() map[string]int { - s.scoresLock.Lock() - defer s.scoresLock.Unlock() - - scoresCp := map[string]int{} - for name, score := range s.scoresM { - scoresCp[name] = score - } - - return scoresCp -} - -func (s *scoreboard) save() error { - scores := s.scores() - if _, err := s.file.Seek(0, 0); err != nil { - return fmt.Errorf("seeking to start of save file: %w", err) - } else if err := s.file.Truncate(0); err != nil { - return fmt.Errorf("truncating save file: %w", err) - } else if err := json.NewEncoder(s.file).Encode(scores); err != nil { - return fmt.Errorf("encoding scores to save file: %w", err) - } - return nil -} - -func (s *scoreboard) saveLoop(ticker <-chan time.Time, logger Logger) { - for { - select { - case <-ticker: - if err := s.save(); err != nil { - logger.Printf("error saving scoreboard to file: %v", err) - } - case <-s.saveLoopWaitCh: - // test will unblock, nothing to do here. - } - } -} - -//////////////////////////////////////////////////////////////////////////////// -// The httpHandlers component - -// Scoreboard describes the scoreboard component from the point of view of the -// httpHandler component (which only needs a subset of scoreboard's methods). -type Scoreboard interface { - guessedCorrect(name string) int - guessedIncorrect(name string) int - scores() map[string]int -} - -// RandSrc describes a randomness component which can produce random integers. -type RandSrc interface { - Int() int -} - -// httpHandlers implements the http.HandlerFuncs used by the httpServer. -type httpHandlers struct { - scoreboard Scoreboard - randSrc RandSrc - logger Logger - - mux *http.ServeMux - n int - nLock sync.Mutex -} - -func newHTTPHandlers(scoreboard Scoreboard, randSrc RandSrc, logger Logger) *httpHandlers { - n := randSrc.Int() - logger.Printf("first n is %v", n) - - httpHandlers := &httpHandlers{ - scoreboard: scoreboard, - randSrc: randSrc, - logger: logger, - mux: http.NewServeMux(), - n: n, - } - - httpHandlers.mux.HandleFunc("/guess", httpHandlers.handleGuess) - httpHandlers.mux.HandleFunc("/scores", httpHandlers.handleScores) - - return httpHandlers -} - -func (h *httpHandlers) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - h.mux.ServeHTTP(rw, r) -} - -func (h *httpHandlers) handleGuess(rw http.ResponseWriter, r *http.Request) { - r.Header.Set("Content-Type", "text/plain") - - name := r.FormValue("name") - nStr := r.FormValue("n") - if name == "" || nStr == "" { - http.Error(rw, `"name" and "n" GET args are required`, http.StatusBadRequest) - return - } - - n, err := strconv.Atoi(nStr) - if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - h.nLock.Lock() - defer h.nLock.Unlock() - - if h.n == n { - newScore := h.scoreboard.guessedCorrect(name) - h.n = h.randSrc.Int() - h.logger.Printf("new n is %v", h.n) - rw.WriteHeader(http.StatusOK) - fmt.Fprintf(rw, "Correct! Your score is now %d\n", newScore) - return - } - - hint := "higher" - if h.n < n { - hint = "lower" - } - - newScore := h.scoreboard.guessedIncorrect(name) - rw.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(rw, "Try %s. Your score is now %d\n", hint, newScore) -} - -func (h *httpHandlers) handleScores(rw http.ResponseWriter, r *http.Request) { - r.Header.Set("Content-Type", "text/plain") - - h.nLock.Lock() - defer h.nLock.Unlock() - - type scoreTup struct { - name string - score int - } - - scores := h.scoreboard.scores() - scoresTups := make([]scoreTup, 0, len(scores)) - for name, score := range scores { - scoresTups = append(scoresTups, scoreTup{name, score}) - } - - sort.Slice(scoresTups, func(i, j int) bool { - return scoresTups[i].score > scoresTups[j].score - }) - - for _, scoresTup := range scoresTups { - fmt.Fprintf(rw, "%s: %d\n", scoresTup.name, scoresTup.score) - } -} - -//////////////////////////////////////////////////////////////////////////////// -// The httpServer component. - -type httpServer struct { - httpServer *http.Server - errCh chan error -} - -func newHTTPServer(listener net.Listener, httpHandlers *httpHandlers, logger Logger) *httpServer { - loggingHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - ip, _, _ := net.SplitHostPort(r.RemoteAddr) - logger.Printf("HTTP request -> %s %s %s", ip, r.Method, r.URL.String()) - httpHandlers.ServeHTTP(rw, r) - }) - - server := &httpServer{ - httpServer: &http.Server{ - Handler: loggingHandler, - }, - errCh: make(chan error, 1), - } - - go func() { - err := server.httpServer.Serve(listener) - if errors.Is(err, http.ErrServerClosed) { - err = nil - } - server.errCh <- err - }() - - return server -} - -//////////////////////////////////////////////////////////////////////////////// -// main - -const ( - saveFilePath = "./save.json" - listenAddr = ":8888" - saveInterval = 5 * time.Second -) - -func main() { - logger := log.New(os.Stdout, "", log.LstdFlags) - - logger.Printf("opening scoreboard save file %q", saveFilePath) - file, err := os.OpenFile(saveFilePath, os.O_RDWR|os.O_CREATE, 0644) - if err != nil { - logger.Fatalf("failed to open file %q: %v", saveFilePath, err) - } - - saveTicker := time.NewTicker(saveInterval) - randSrc := rand.New(rand.NewSource(time.Now().UnixNano())) - - logger.Printf("initializing scoreboard") - scoreboard, err := newScoreboard(file, saveTicker.C, logger) - if err != nil { - logger.Fatalf("failed to initialize scoreboard: %v", err) - } - - logger.Printf("listening on %q", listenAddr) - listener, err := net.Listen("tcp", listenAddr) - if err != nil { - logger.Fatalf("failed to listen on %q: %v", listenAddr, err) - } - - logger.Printf("setting up HTTP handlers") - httpHandlers := newHTTPHandlers(scoreboard, randSrc, logger) - - logger.Printf("serving HTTP requests") - newHTTPServer(listener, httpHandlers, logger) - - logger.Printf("initialization done") - select {} // block forever -} diff --git a/srv/src/http/static/component-oriented-design/v1/main_test.go b/srv/src/http/static/component-oriented-design/v1/main_test.go deleted file mode 100644 index 6cfd9fb..0000000 --- a/srv/src/http/static/component-oriented-design/v1/main_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package main - -import ( - "bytes" - "net/http" - "net/http/httptest" - "reflect" - "testing" - "time" -) - -type nullLogger struct{} - -func (nullLogger) Printf(string, ...interface{}) {} - -//////////////////////////////////////////////////////////////////////////////// -// Test scoreboard component - -type fileStub struct { - *bytes.Buffer -} - -func newFileStub(init string) *fileStub { - return &fileStub{Buffer: bytes.NewBufferString(init)} -} - -func (fs *fileStub) Truncate(i int64) error { - fs.Buffer.Truncate(int(i)) - return nil -} - -func (fs *fileStub) Seek(i int64, whence int) (int64, error) { - return i, nil -} - -func TestScoreboard(t *testing.T) { - newScoreboard := func(t *testing.T, fileStub *fileStub, saveTicker <-chan time.Time) *scoreboard { - t.Helper() - scoreboard, err := newScoreboard(fileStub, saveTicker, nullLogger{}) - if err != nil { - t.Errorf("unexpected error checking saved scored: %v", err) - } - return scoreboard - } - - assertScores := func(t *testing.T, expScores, gotScores map[string]int) { - t.Helper() - if !reflect.DeepEqual(expScores, gotScores) { - t.Errorf("expected scores of %+v, but instead got %+v", expScores, gotScores) - } - } - - assertSavedScores := func(t *testing.T, expScores map[string]int, fileStub *fileStub) { - t.Helper() - fileStubCp := newFileStub(fileStub.String()) - tmpScoreboard := newScoreboard(t, fileStubCp, nil) - assertScores(t, expScores, tmpScoreboard.scores()) - } - - t.Run("loading", func(t *testing.T) { - // make sure loading scoreboards with various file contents works - assertSavedScores(t, map[string]int{}, newFileStub("")) - assertSavedScores(t, map[string]int{"foo": 1}, newFileStub(`{"foo":1}`)) - assertSavedScores(t, map[string]int{"foo": 1, "bar": -2}, newFileStub(`{"foo":1,"bar":-2}`)) - }) - - t.Run("tracking", func(t *testing.T) { - scoreboard := newScoreboard(t, newFileStub(""), nil) - assertScores(t, map[string]int{}, scoreboard.scores()) // sanity check - - scoreboard.guessedCorrect("foo") - assertScores(t, map[string]int{"foo": 1000}, scoreboard.scores()) - - scoreboard.guessedIncorrect("bar") - assertScores(t, map[string]int{"foo": 1000, "bar": -1}, scoreboard.scores()) - - scoreboard.guessedIncorrect("foo") - assertScores(t, map[string]int{"foo": 999, "bar": -1}, scoreboard.scores()) - }) - - t.Run("saving", func(t *testing.T) { - // this test tests scoreboard's periodic save feature using a ticker - // channel which will be written to manually. The saveLoopWaitCh is used - // here to ensure that each ticker has been fully processed. - ticker := make(chan time.Time) - fileStub := newFileStub("") - scoreboard := newScoreboard(t, fileStub, ticker) - - tick := func() { - ticker <- time.Time{} - scoreboard.saveLoopWaitCh <- struct{}{} - } - - // this should not effect the save file at first - scoreboard.guessedCorrect("foo") - assertSavedScores(t, map[string]int{}, fileStub) - - // after the ticker the new score should get saved - tick() - assertSavedScores(t, map[string]int{"foo": 1000}, fileStub) - - // ticker again after no changes should save the same thing as before - tick() - assertSavedScores(t, map[string]int{"foo": 1000}, fileStub) - - // buffer a bunch of changes, shouldn't get saved till after tick - scoreboard.guessedCorrect("foo") - scoreboard.guessedCorrect("bar") - scoreboard.guessedCorrect("bar") - assertSavedScores(t, map[string]int{"foo": 1000}, fileStub) - tick() - assertSavedScores(t, map[string]int{"foo": 2000, "bar": 2000}, fileStub) - }) -} - -//////////////////////////////////////////////////////////////////////////////// -// Test httpHandlers component - -type mockScoreboard map[string]int - -func (mockScoreboard) guessedCorrect(name string) int { return 1 } - -func (mockScoreboard) guessedIncorrect(name string) int { return -1 } - -func (m mockScoreboard) scores() map[string]int { return m } - -type mockRandSrc struct{} - -func (m mockRandSrc) Int() int { return 666 } - -func TestHTTPHandlers(t *testing.T) { - mockScoreboard := mockScoreboard{"foo": 1, "bar": 2} - httpHandlers := newHTTPHandlers(mockScoreboard, mockRandSrc{}, nullLogger{}) - - assertRequest := func(t *testing.T, expCode int, expBody string, r *http.Request) { - t.Helper() - rw := httptest.NewRecorder() - httpHandlers.ServeHTTP(rw, r) - if rw.Code != expCode { - t.Errorf("expected HTTP response code %d, got %d", expCode, rw.Code) - } else if rw.Body.String() != expBody { - t.Errorf("expected HTTP response body %q, got %q", expBody, rw.Body.String()) - } - } - - r := httptest.NewRequest("GET", "/guess?name=foo&n=665", nil) - assertRequest(t, 400, "Try higher. Your score is now -1\n", r) - - r = httptest.NewRequest("GET", "/guess?name=foo&n=667", nil) - assertRequest(t, 400, "Try lower. Your score is now -1\n", r) - - r = httptest.NewRequest("GET", "/guess?name=foo&n=666", nil) - assertRequest(t, 200, "Correct! Your score is now 1\n", r) - - r = httptest.NewRequest("GET", "/scores", nil) - assertRequest(t, 200, "bar: 2\nfoo: 1\n", r) -} - -//////////////////////////////////////////////////////////////////////////////// -// -// httpServer is NOT tested, for the following reasons: -// * It depends on a `net.Listener`, which is not trivial to mock. -// * It does very little besides passing an httpHandlers along to an http.Server -// and managing cleanup. -// * It isn't likely to be changed often. -// * If it were to break it would be very apparent in subsequent testing stages. -// diff --git a/srv/src/http/static/component-oriented-design/v2/main.go b/srv/src/http/static/component-oriented-design/v2/main.go deleted file mode 100644 index fb5773c..0000000 --- a/srv/src/http/static/component-oriented-design/v2/main.go +++ /dev/null @@ -1,320 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "io/ioutil" - "log" - "math/rand" - "net" - "net/http" - "os" - "sort" - "strconv" - "sync" - "time" -) - -// Logger describes a simple component used for printing log lines. -type Logger interface { - Printf(string, ...interface{}) -} - -//////////////////////////////////////////////////////////////////////////////// -// The scoreboard component - -// File wraps the standard os.File type. -type File interface { - io.ReadWriter - Truncate(int64) error - Seek(int64, int) (int64, error) -} - -// scoreboard loads player scores from a save file, tracks score updates, and -// periodically saves those scores back to the save file. -type scoreboard struct { - file File - scoresM map[string]int - scoresLock sync.Mutex - - pointsOnCorrect, pointsOnIncorrect int - - // this field will only be set in tests, and is used to synchronize with the - // the for-select loop in saveLoop. - saveLoopWaitCh chan struct{} -} - -// newScoreboard initializes a scoreboard using scores saved in the given File -// (which may be empty). The scoreboard will rewrite the save file with the -// latest scores everytime saveTicker is written to. -func newScoreboard(file File, saveTicker <-chan time.Time, logger Logger, pointsOnCorrect, pointsOnIncorrect int) (*scoreboard, error) { - fileBody, err := ioutil.ReadAll(file) - if err != nil { - return nil, fmt.Errorf("reading saved scored: %w", err) - } - - scoresM := map[string]int{} - if len(fileBody) > 0 { - if err := json.Unmarshal(fileBody, &scoresM); err != nil { - return nil, fmt.Errorf("decoding saved scores: %w", err) - } - } - - scoreboard := &scoreboard{ - file: file, - scoresM: scoresM, - pointsOnCorrect: pointsOnCorrect, - pointsOnIncorrect: pointsOnIncorrect, - saveLoopWaitCh: make(chan struct{}), - } - - go scoreboard.saveLoop(saveTicker, logger) - - return scoreboard, nil -} - -func (s *scoreboard) guessedCorrect(name string) int { - s.scoresLock.Lock() - defer s.scoresLock.Unlock() - - s.scoresM[name] += s.pointsOnCorrect - return s.scoresM[name] -} - -func (s *scoreboard) guessedIncorrect(name string) int { - s.scoresLock.Lock() - defer s.scoresLock.Unlock() - - s.scoresM[name] += s.pointsOnIncorrect - return s.scoresM[name] -} - -func (s *scoreboard) scores() map[string]int { - s.scoresLock.Lock() - defer s.scoresLock.Unlock() - - scoresCp := map[string]int{} - for name, score := range s.scoresM { - scoresCp[name] = score - } - - return scoresCp -} - -func (s *scoreboard) save() error { - scores := s.scores() - if _, err := s.file.Seek(0, 0); err != nil { - return fmt.Errorf("seeking to start of save file: %w", err) - } else if err := s.file.Truncate(0); err != nil { - return fmt.Errorf("truncating save file: %w", err) - } else if err := json.NewEncoder(s.file).Encode(scores); err != nil { - return fmt.Errorf("encoding scores to save file: %w", err) - } - return nil -} - -func (s *scoreboard) saveLoop(ticker <-chan time.Time, logger Logger) { - for { - select { - case <-ticker: - if err := s.save(); err != nil { - logger.Printf("error saving scoreboard to file: %v", err) - } - case <-s.saveLoopWaitCh: - // test will unblock, nothing to do here. - } - } -} - -//////////////////////////////////////////////////////////////////////////////// -// The httpHandlers component - -// Scoreboard describes the scoreboard component from the point of view of the -// httpHandler component (which only needs a subset of scoreboard's methods). -type Scoreboard interface { - guessedCorrect(name string) int - guessedIncorrect(name string) int - scores() map[string]int -} - -// RandSrc describes a randomness component which can produce random integers. -type RandSrc interface { - Int() int -} - -// httpHandlers implements the http.HandlerFuncs used by the httpServer. -type httpHandlers struct { - scoreboard Scoreboard - randSrc RandSrc - logger Logger - - mux *http.ServeMux - n int - nLock sync.Mutex -} - -func newHTTPHandlers(scoreboard Scoreboard, randSrc RandSrc, logger Logger) *httpHandlers { - n := randSrc.Int() - logger.Printf("first n is %v", n) - - httpHandlers := &httpHandlers{ - scoreboard: scoreboard, - randSrc: randSrc, - logger: logger, - mux: http.NewServeMux(), - n: n, - } - - httpHandlers.mux.HandleFunc("/guess", httpHandlers.handleGuess) - httpHandlers.mux.HandleFunc("/scores", httpHandlers.handleScores) - - return httpHandlers -} - -func (h *httpHandlers) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - h.mux.ServeHTTP(rw, r) -} - -func (h *httpHandlers) handleGuess(rw http.ResponseWriter, r *http.Request) { - r.Header.Set("Content-Type", "text/plain") - - name := r.FormValue("name") - nStr := r.FormValue("n") - if name == "" || nStr == "" { - http.Error(rw, `"name" and "n" GET args are required`, http.StatusBadRequest) - return - } - - n, err := strconv.Atoi(nStr) - if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - h.nLock.Lock() - defer h.nLock.Unlock() - - if h.n == n { - newScore := h.scoreboard.guessedCorrect(name) - h.n = h.randSrc.Int() - h.logger.Printf("new n is %v", h.n) - rw.WriteHeader(http.StatusOK) - fmt.Fprintf(rw, "Correct! Your score is now %d\n", newScore) - return - } - - hint := "higher" - if h.n < n { - hint = "lower" - } - - newScore := h.scoreboard.guessedIncorrect(name) - rw.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(rw, "Try %s. Your score is now %d\n", hint, newScore) -} - -func (h *httpHandlers) handleScores(rw http.ResponseWriter, r *http.Request) { - r.Header.Set("Content-Type", "text/plain") - - h.nLock.Lock() - defer h.nLock.Unlock() - - type scoreTup struct { - name string - score int - } - - scores := h.scoreboard.scores() - scoresTups := make([]scoreTup, 0, len(scores)) - for name, score := range scores { - scoresTups = append(scoresTups, scoreTup{name, score}) - } - - sort.Slice(scoresTups, func(i, j int) bool { - return scoresTups[i].score > scoresTups[j].score - }) - - for _, scoresTup := range scoresTups { - fmt.Fprintf(rw, "%s: %d\n", scoresTup.name, scoresTup.score) - } -} - -//////////////////////////////////////////////////////////////////////////////// -// The httpServer component. - -type httpServer struct { - httpServer *http.Server - errCh chan error -} - -func newHTTPServer(listener net.Listener, httpHandlers *httpHandlers, logger Logger) *httpServer { - loggingHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - ip, _, _ := net.SplitHostPort(r.RemoteAddr) - logger.Printf("HTTP request -> %s %s %s", ip, r.Method, r.URL.String()) - httpHandlers.ServeHTTP(rw, r) - }) - - server := &httpServer{ - httpServer: &http.Server{ - Handler: loggingHandler, - }, - errCh: make(chan error, 1), - } - - go func() { - err := server.httpServer.Serve(listener) - if errors.Is(err, http.ErrServerClosed) { - err = nil - } - server.errCh <- err - }() - - return server -} - -//////////////////////////////////////////////////////////////////////////////// -// main - -func main() { - saveFilePath := flag.String("save-file", "./save.json", "File used to save scores") - listenAddr := flag.String("listen-addr", ":8888", "Address to listen for HTTP requests on") - saveInterval := flag.Duration("save-interval", 5*time.Second, "How often to resave scores") - pointsOnCorrect := flag.Int("points-on-correct", 1000, "Amount to change a user's score by upon a correct score") - pointsOnIncorrect := flag.Int("points-on-incorrect", -1, "Amount to change a user's score by upon an incorrect score") - flag.Parse() - - logger := log.New(os.Stdout, "", log.LstdFlags) - - logger.Printf("opening scoreboard save file %q", *saveFilePath) - file, err := os.OpenFile(*saveFilePath, os.O_RDWR|os.O_CREATE, 0644) - if err != nil { - logger.Fatalf("failed to open file %q: %v", *saveFilePath, err) - } - - saveTicker := time.NewTicker(*saveInterval) - randSrc := rand.New(rand.NewSource(time.Now().UnixNano())) - - logger.Printf("initializing scoreboard") - scoreboard, err := newScoreboard(file, saveTicker.C, logger, *pointsOnCorrect, *pointsOnIncorrect) - if err != nil { - logger.Fatalf("failed to initialize scoreboard: %v", err) - } - - logger.Printf("listening on %q", *listenAddr) - listener, err := net.Listen("tcp", *listenAddr) - if err != nil { - logger.Fatalf("failed to listen on %q: %v", *listenAddr, err) - } - - logger.Printf("setting up HTTP handlers") - httpHandlers := newHTTPHandlers(scoreboard, randSrc, logger) - - logger.Printf("serving HTTP requests") - newHTTPServer(listener, httpHandlers, logger) - - logger.Printf("initialization done") - select {} // block forever -} diff --git a/srv/src/http/static/component-oriented-design/v3/main.go b/srv/src/http/static/component-oriented-design/v3/main.go deleted file mode 100644 index afe8bab..0000000 --- a/srv/src/http/static/component-oriented-design/v3/main.go +++ /dev/null @@ -1,390 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "io/ioutil" - "log" - "math/rand" - "net" - "net/http" - "os" - "os/signal" - "sort" - "strconv" - "sync" - "time" -) - -// Logger describes a simple component used for printing log lines. -type Logger interface { - Printf(string, ...interface{}) -} - -//////////////////////////////////////////////////////////////////////////////// -// The scoreboard component - -// File wraps the standard os.File type. -type File interface { - io.ReadWriter - Truncate(int64) error - Seek(int64, int) (int64, error) -} - -// scoreboard loads player scores from a save file, tracks score updates, and -// periodically saves those scores back to the save file. -type scoreboard struct { - file File - scoresM map[string]int - scoresLock sync.Mutex - - pointsOnCorrect, pointsOnIncorrect int - - // The cleanup method closes cleanupCh to signal to all scoreboard's running - // go-routines to clean themselves up, and cleanupWG is then used to wait - // for those goroutines to do so. - cleanupCh chan struct{} - cleanupWG sync.WaitGroup - - // this field will only be set in tests, and is used to synchronize with the - // the for-select loop in saveLoop. - saveLoopWaitCh chan struct{} -} - -// newScoreboard initializes a scoreboard using scores saved in the given File -// (which may be empty). The scoreboard will rewrite the save file with the -// latest scores everytime saveTicker is written to. -func newScoreboard(file File, saveTicker <-chan time.Time, logger Logger, pointsOnCorrect, pointsOnIncorrect int) (*scoreboard, error) { - fileBody, err := ioutil.ReadAll(file) - if err != nil { - return nil, fmt.Errorf("reading saved scored: %w", err) - } - - scoresM := map[string]int{} - if len(fileBody) > 0 { - if err := json.Unmarshal(fileBody, &scoresM); err != nil { - return nil, fmt.Errorf("decoding saved scores: %w", err) - } - } - - scoreboard := &scoreboard{ - file: file, - scoresM: scoresM, - pointsOnCorrect: pointsOnCorrect, - pointsOnIncorrect: pointsOnIncorrect, - cleanupCh: make(chan struct{}), - saveLoopWaitCh: make(chan struct{}), - } - - scoreboard.cleanupWG.Add(1) - go func() { - scoreboard.saveLoop(saveTicker, logger) - scoreboard.cleanupWG.Done() - }() - - return scoreboard, nil -} - -func (s *scoreboard) cleanup() error { - close(s.cleanupCh) - s.cleanupWG.Wait() - - if err := s.save(); err != nil { - return fmt.Errorf("saving scores during cleanup: %w", err) - } - return nil -} - -func (s *scoreboard) guessedCorrect(name string) int { - s.scoresLock.Lock() - defer s.scoresLock.Unlock() - - s.scoresM[name] += s.pointsOnCorrect - return s.scoresM[name] -} - -func (s *scoreboard) guessedIncorrect(name string) int { - s.scoresLock.Lock() - defer s.scoresLock.Unlock() - - s.scoresM[name] += s.pointsOnIncorrect - return s.scoresM[name] -} - -func (s *scoreboard) scores() map[string]int { - s.scoresLock.Lock() - defer s.scoresLock.Unlock() - - scoresCp := map[string]int{} - for name, score := range s.scoresM { - scoresCp[name] = score - } - - return scoresCp -} - -func (s *scoreboard) save() error { - scores := s.scores() - if _, err := s.file.Seek(0, 0); err != nil { - return fmt.Errorf("seeking to start of save file: %w", err) - } else if err := s.file.Truncate(0); err != nil { - return fmt.Errorf("truncating save file: %w", err) - } else if err := json.NewEncoder(s.file).Encode(scores); err != nil { - return fmt.Errorf("encoding scores to save file: %w", err) - } - return nil -} - -func (s *scoreboard) saveLoop(ticker <-chan time.Time, logger Logger) { - for { - select { - case <-ticker: - if err := s.save(); err != nil { - logger.Printf("error saving scoreboard to file: %v", err) - } - case <-s.cleanupCh: - return - case <-s.saveLoopWaitCh: - // test will unblock, nothing to do here. - } - } -} - -//////////////////////////////////////////////////////////////////////////////// -// The httpHandlers component - -// Scoreboard describes the scoreboard component from the point of view of the -// httpHandler component (which only needs a subset of scoreboard's methods). -type Scoreboard interface { - guessedCorrect(name string) int - guessedIncorrect(name string) int - scores() map[string]int -} - -// RandSrc describes a randomness component which can produce random integers. -type RandSrc interface { - Int() int -} - -// httpHandlers implements the http.HandlerFuncs used by the httpServer. -type httpHandlers struct { - scoreboard Scoreboard - randSrc RandSrc - logger Logger - - mux *http.ServeMux - n int - nLock sync.Mutex -} - -func newHTTPHandlers(scoreboard Scoreboard, randSrc RandSrc, logger Logger) *httpHandlers { - n := randSrc.Int() - logger.Printf("first n is %v", n) - - httpHandlers := &httpHandlers{ - scoreboard: scoreboard, - randSrc: randSrc, - logger: logger, - mux: http.NewServeMux(), - n: n, - } - - httpHandlers.mux.HandleFunc("/guess", httpHandlers.handleGuess) - httpHandlers.mux.HandleFunc("/scores", httpHandlers.handleScores) - - return httpHandlers -} - -func (h *httpHandlers) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - h.mux.ServeHTTP(rw, r) -} - -func (h *httpHandlers) handleGuess(rw http.ResponseWriter, r *http.Request) { - r.Header.Set("Content-Type", "text/plain") - - name := r.FormValue("name") - nStr := r.FormValue("n") - if name == "" || nStr == "" { - http.Error(rw, `"name" and "n" GET args are required`, http.StatusBadRequest) - return - } - - n, err := strconv.Atoi(nStr) - if err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - h.nLock.Lock() - defer h.nLock.Unlock() - - if h.n == n { - newScore := h.scoreboard.guessedCorrect(name) - h.n = h.randSrc.Int() - h.logger.Printf("new n is %v", h.n) - rw.WriteHeader(http.StatusOK) - fmt.Fprintf(rw, "Correct! Your score is now %d\n", newScore) - return - } - - hint := "higher" - if h.n < n { - hint = "lower" - } - - newScore := h.scoreboard.guessedIncorrect(name) - rw.WriteHeader(http.StatusBadRequest) - fmt.Fprintf(rw, "Try %s. Your score is now %d\n", hint, newScore) -} - -func (h *httpHandlers) handleScores(rw http.ResponseWriter, r *http.Request) { - r.Header.Set("Content-Type", "text/plain") - - h.nLock.Lock() - defer h.nLock.Unlock() - - type scoreTup struct { - name string - score int - } - - scores := h.scoreboard.scores() - scoresTups := make([]scoreTup, 0, len(scores)) - for name, score := range scores { - scoresTups = append(scoresTups, scoreTup{name, score}) - } - - sort.Slice(scoresTups, func(i, j int) bool { - return scoresTups[i].score > scoresTups[j].score - }) - - for _, scoresTup := range scoresTups { - fmt.Fprintf(rw, "%s: %d\n", scoresTup.name, scoresTup.score) - } -} - -//////////////////////////////////////////////////////////////////////////////// -// The httpServer component. - -type httpServer struct { - httpServer *http.Server - errCh chan error -} - -func newHTTPServer(listener net.Listener, httpHandlers *httpHandlers, logger Logger) *httpServer { - loggingHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - ip, _, _ := net.SplitHostPort(r.RemoteAddr) - logger.Printf("HTTP request -> %s %s %s", ip, r.Method, r.URL.String()) - httpHandlers.ServeHTTP(rw, r) - }) - - server := &httpServer{ - httpServer: &http.Server{ - Handler: loggingHandler, - }, - errCh: make(chan error, 1), - } - - go func() { - err := server.httpServer.Serve(listener) - if errors.Is(err, http.ErrServerClosed) { - err = nil - } - server.errCh <- err - }() - - return server -} - -func (s *httpServer) cleanup() error { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - if err := s.httpServer.Shutdown(ctx); err != nil { - return fmt.Errorf("shutting down http server: %w", err) - } - return <-s.errCh -} - -//////////////////////////////////////////////////////////////////////////////// -// main - -func main() { - saveFilePath := flag.String("save-file", "./save.json", "File used to save scores") - listenAddr := flag.String("listen-addr", ":8888", "Address to listen for HTTP requests on") - saveInterval := flag.Duration("save-interval", 5*time.Second, "How often to resave scores") - pointsOnCorrect := flag.Int("points-on-correct", 1000, "Amount to change a user's score by upon a correct score") - pointsOnIncorrect := flag.Int("points-on-incorrect", -1, "Amount to change a user's score by upon an incorrect score") - flag.Parse() - - logger := log.New(os.Stdout, "", log.LstdFlags) - - logger.Printf("opening scoreboard save file %q", *saveFilePath) - file, err := os.OpenFile(*saveFilePath, os.O_RDWR|os.O_CREATE, 0644) - if err != nil { - logger.Fatalf("failed to open file %q: %v", *saveFilePath, err) - } - - saveTicker := time.NewTicker(*saveInterval) - randSrc := rand.New(rand.NewSource(time.Now().UnixNano())) - - logger.Printf("initializing scoreboard") - scoreboard, err := newScoreboard(file, saveTicker.C, logger, *pointsOnCorrect, *pointsOnIncorrect) - if err != nil { - logger.Fatalf("failed to initialize scoreboard: %v", err) - } - - logger.Printf("listening on %q", *listenAddr) - listener, err := net.Listen("tcp", *listenAddr) - if err != nil { - logger.Fatalf("failed to listen on %q: %v", *listenAddr, err) - } - - logger.Printf("setting up HTTP handlers") - httpHandlers := newHTTPHandlers(scoreboard, randSrc, logger) - - logger.Printf("serving HTTP requests") - httpServer := newHTTPServer(listener, httpHandlers, logger) - - logger.Printf("initialization done, waiting for interrupt signal") - sigCh := make(chan os.Signal) - signal.Notify(sigCh, os.Interrupt) - <-sigCh - logger.Printf("interrupt signal received, cleaning up") - go func() { - <-sigCh - log.Fatalf("interrupt signal received again, forcing shutdown") - }() - - if err := httpServer.cleanup(); err != nil { - logger.Fatalf("cleaning up http server: %v", err) - } - - // NOTE go's builtin http server does not follow component property 5a, and - // instead closes the net.Listener given to it as a parameter when Shutdown - // is called. Because of that inconsistency this Close would error if it - // were called. - // - // While there are ways to work around this, it's instead highlighted in - // this example as an instance of a language making the component-oriented - // pattern more difficult. - // - //if err := listener.Close(); err != nil { - // logger.Fatalf("closing listener %q: %v", listenAddr, err) - //} - - if err := scoreboard.cleanup(); err != nil { - logger.Fatalf("cleaning up scoreboard: %v", err) - } - - saveTicker.Stop() - - if err := file.Close(); err != nil { - logger.Fatalf("closing file %q: %v", *saveFilePath, err) - } - - os.Stdout.Sync() -} |