Skip to content

Instantly share code, notes, and snippets.

@drio
Last active May 3, 2025 17:40
Show Gist options
  • Save drio/d64a8d05a5ccce486046cd787acd3513 to your computer and use it in GitHub Desktop.
Save drio/d64a8d05a5ccce486046cd787acd3513 to your computer and use it in GitHub Desktop.
RC database
package main
import (
"fmt"
"log"
"net/http"
"sync"
)
/*
Progress:
- [X] Basic functionality
- [X] Refactor server to be able to test
- [X] Add basic tests
- [X] Synchronize the store so it works concurrently without race conditions
- [X] Refactor the store to work with different backends (mem, file)
- [ ] Enumerate the many things that can go wrong and that we should look for,
particularly in a production setup
Extras:
- [ ] Add explicit tests to test the locking mechanism (currently using -race with test)
*/
type Store interface {
Get(key string) (string, bool)
Set(key, value string)
}
type Server struct {
store Store
}
type MemoryStore struct {
mu sync.RWMutex
data map[string]string
}
func NewMemoryStore() Store {
return &MemoryStore{
data: make(map[string]string),
}
}
func (m *MemoryStore) Get(key string) (string, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
val, ok := m.data[key]
return val, ok
}
func (m *MemoryStore) Set(key, value string) {
m.mu.Lock()
defer m.mu.Unlock()
m.data[key] = value
}
func (s *Server) setHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("GET %s?%s", r.URL.Path, r.URL.RawQuery)
query := r.URL.Query()
// NOTE: I am allowing for multiple keys. Maybe that is not what we want.
// Also, when we send multiple values of a key, I pick the first one.
for key, values := range query {
s.store.Set(key, values[0])
}
fmt.Fprint(w, "ok")
}
func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("GET %s?%s", r.URL.Path, r.URL.RawQuery)
query := r.URL.Query()
keys := query["key"]
if len(keys) != 1 {
http.Error(w, "Exactly one 'key' parameter is required", http.StatusBadRequest)
return
}
if value, ok := s.store.Get(keys[0]); ok {
fmt.Fprintf(w, "%s\n", value)
} else {
http.Error(w, "key not found", http.StatusNotFound)
}
}
func main() {
s := &Server{
store: NewMemoryStore(),
}
http.HandleFunc("/set", s.setHandler)
http.HandleFunc("/get", s.getHandler)
fmt.Println("Server is running on http://localhost:4000.")
err := http.ListenAndServe(":4000", nil)
if err != nil {
panic(err)
}
}
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestSetAndGet(t *testing.T) {
s := &Server{
store: NewMemoryStore(),
}
// The handler needs a http response writer and a request
// Fake both and ...
reqSet := httptest.NewRequest("GET", "/set?foo=bar", nil)
wSet := httptest.NewRecorder()
s.setHandler(wSet, reqSet)
// the fake writer for testing
if wSet.Code != http.StatusOK {
t.Fatalf("when sending a valid /set request I expect 200, but got %d", wSet.Code)
}
reqGet := httptest.NewRequest("GET", "/get?key=foo", nil)
wGet := httptest.NewRecorder()
s.getHandler(wGet, reqGet)
if wGet.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", wGet.Code)
}
expected := "bar\n"
if wGet.Body.String() != expected {
t.Errorf("expected %q, got %q", expected, wGet.Body.String())
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment