-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat(backend,dist-http): add internal HTTP server & HTTP transport with context-aware replication #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(backend,dist-http): add internal HTTP server & HTTP transport with context-aware replication #47
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,138 @@ | ||||||||||||||||
| package backend | ||||||||||||||||
|
|
||||||||||||||||
| import ( | ||||||||||||||||
| "context" | ||||||||||||||||
| "net" | ||||||||||||||||
| "strconv" | ||||||||||||||||
| "time" | ||||||||||||||||
|
|
||||||||||||||||
| "github.com/goccy/go-json" | ||||||||||||||||
|
|
||||||||||||||||
| "github.com/hyp3rd/ewrap" | ||||||||||||||||
|
|
||||||||||||||||
| fiber "github.com/gofiber/fiber/v3" | ||||||||||||||||
|
|
||||||||||||||||
| cache "github.com/hyp3rd/hypercache/pkg/cache/v2" | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| type distHTTPServer struct { | ||||||||||||||||
| app *fiber.App | ||||||||||||||||
| ln net.Listener | ||||||||||||||||
| addr string | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // minimal request/response types reused by transport | ||||||||||||||||
| // request/response DTOs defined in dist_http_types.go | ||||||||||||||||
|
|
||||||||||||||||
| const ( | ||||||||||||||||
| httpReadTimeout = 5 * time.Second | ||||||||||||||||
| httpWriteTimeout = 5 * time.Second | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| func newDistHTTPServer(addr string) *distHTTPServer { | ||||||||||||||||
| app := fiber.New(fiber.Config{ReadTimeout: httpReadTimeout, WriteTimeout: httpWriteTimeout}) | ||||||||||||||||
|
|
||||||||||||||||
| return &distHTTPServer{app: app, addr: addr} | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| func (s *distHTTPServer) start(ctx context.Context, dm *DistMemory) error { //nolint:ireturn | ||||||||||||||||
| // routes | ||||||||||||||||
| // set | ||||||||||||||||
| // POST /internal/cache/set | ||||||||||||||||
| // body: httpSetRequest | ||||||||||||||||
| s.app.Post("/internal/cache/set", func(fctx fiber.Ctx) error { | ||||||||||||||||
| var req httpSetRequest | ||||||||||||||||
|
|
||||||||||||||||
| err := json.Unmarshal(fctx.Body(), &req) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return fctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| it := &cache.Item{Key: req.Key, Value: req.Value, Expiration: time.Duration(req.Expiration) * time.Millisecond, Version: req.Version, Origin: req.Origin} | ||||||||||||||||
| if req.Replicate { | ||||||||||||||||
| dm.applySet(ctx, it, true) | ||||||||||||||||
|
|
||||||||||||||||
| return fctx.JSON(httpSetResponse{}) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| dm.applySet(ctx, it, false) | ||||||||||||||||
|
||||||||||||||||
| dm.applySet(ctx, it, false) | |
| dm.applySet(fctx.UserContext(), it, true) | |
| return fctx.JSON(httpSetResponse{}) | |
| } | |
| dm.applySet(fctx.UserContext(), it, false) |
Copilot
AI
Aug 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The context ctx passed to applyRemove is the server startup context, not the request context. This could cause operations to be cancelled when the server shuts down rather than respecting request timeouts. Use fctx.UserContext() to get the request context instead.
| dm.applyRemove(ctx, key, replicate) | |
| dm.applyRemove(fctx.UserContext(), key, replicate) |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,241 @@ | ||||
| // Package backend provides backend implementations including a distributed HTTP transport. | ||||
| package backend | ||||
|
|
||||
| import ( | ||||
| "bytes" | ||||
| "context" | ||||
| "fmt" | ||||
| "io" | ||||
| "net/http" | ||||
| "time" | ||||
|
|
||||
| "github.com/goccy/go-json" | ||||
|
|
||||
| "github.com/hyp3rd/ewrap" | ||||
|
|
||||
| "github.com/hyp3rd/hypercache/internal/sentinel" | ||||
| cache "github.com/hyp3rd/hypercache/pkg/cache/v2" | ||||
| ) | ||||
|
|
||||
| // DistHTTPTransport implements DistTransport over HTTP JSON. | ||||
| type DistHTTPTransport struct { // minimal MVP | ||||
| client *http.Client | ||||
| baseURLFn func(nodeID string) (string, bool) // resolves nodeID -> base URL (scheme+host) | ||||
| } | ||||
|
|
||||
| // internal status code threshold for error classification. | ||||
| const statusThreshold = 300 | ||||
|
|
||||
| // NewDistHTTPTransport creates a new HTTP transport. | ||||
| func NewDistHTTPTransport(timeout time.Duration, resolver func(string) (string, bool)) *DistHTTPTransport { | ||||
| if timeout <= 0 { | ||||
| timeout = 2 * time.Second | ||||
| } | ||||
|
|
||||
| return &DistHTTPTransport{ | ||||
| client: &http.Client{Timeout: timeout}, | ||||
| baseURLFn: resolver, | ||||
| } | ||||
| } | ||||
|
|
||||
| // request/response DTOs moved to dist_http_types.go | ||||
|
|
||||
| // ForwardSet forwards a set (or replication) request to a remote node. | ||||
| func (t *DistHTTPTransport) ForwardSet(ctx context.Context, nodeID string, item *cache.Item, replicate bool) error { //nolint:ireturn | ||||
| base, ok := t.baseURLFn(nodeID) | ||||
| if !ok { | ||||
| return sentinel.ErrBackendNotFound | ||||
| } | ||||
|
|
||||
| reqBody := httpSetRequest{Key: item.Key, Value: item.Value, Expiration: item.Expiration.Milliseconds(), Version: item.Version, Origin: item.Origin, Replicate: replicate} | ||||
|
|
||||
| payloadBytes, err := json.Marshal(&reqBody) | ||||
| if err != nil { | ||||
| return ewrap.Wrap(err, "marshal set request") | ||||
| } | ||||
|
|
||||
| url := base + "/internal/cache/set" | ||||
|
|
||||
| hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payloadBytes)) // background ctx; caller handles outer deadline | ||||
| if err != nil { | ||||
| return ewrap.Wrap(err, "new request") | ||||
| } | ||||
|
|
||||
| hreq.Header.Set("Content-Type", "application/json") | ||||
|
|
||||
| resp, err := t.client.Do(hreq) | ||||
| if err != nil { | ||||
| return ewrap.Wrap(err, "do request") | ||||
| } | ||||
|
|
||||
| defer func() { | ||||
| _ = resp.Body.Close() //nolint:errcheck // best-effort | ||||
| }() | ||||
|
|
||||
| if resp.StatusCode == http.StatusNotFound { | ||||
| return sentinel.ErrBackendNotFound | ||||
| } | ||||
|
|
||||
| const statusThreshold = 300 // local redeclare for linter clarity | ||||
|
||||
| const statusThreshold = 300 // local redeclare for linter clarity |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The context
ctxpassed toapplySetis the server startup context, not the request context. This could cause operations to be cancelled when the server shuts down rather than respecting request timeouts. Usefctx.UserContext()to get the request context instead.