From c720c7bcf18b314ea23472a258ad8417e3deb9b9 Mon Sep 17 00:00:00 2001 From: Alvin Ramadan Date: Thu, 26 Sep 2024 18:40:25 +0700 Subject: [PATCH] create router constructor that accepts client --- webui/context.go | 141 +++++++++++++++++++++++++++++++++ webui/options.go | 11 --- webui/router.go | 52 ++++++------ webui/server.go | 52 ++++++++++++ webui/webui.go | 189 -------------------------------------------- webui/webui_test.go | 38 ++++----- 6 files changed, 234 insertions(+), 249 deletions(-) create mode 100644 webui/context.go delete mode 100644 webui/options.go create mode 100644 webui/server.go diff --git a/webui/context.go b/webui/context.go new file mode 100644 index 00000000..101d4431 --- /dev/null +++ b/webui/context.go @@ -0,0 +1,141 @@ +package webui + +import ( + "strconv" + "time" + + "github.com/gocraft/web" + "github.com/gojek/work" +) + +type context struct { + client *work.Client +} + +func (c *context) ping(rw web.ResponseWriter, r *web.Request) { + render(rw, map[string]string{"ping": "pong", "current_time": time.Now().Format(time.RFC3339)}, nil) +} + +func (c *context) queues(rw web.ResponseWriter, r *web.Request) { + response, err := c.client.Queues() + render(rw, response, err) +} + +func (c *context) workerPools(rw web.ResponseWriter, r *web.Request) { + response, err := c.client.WorkerPoolHeartbeats() + render(rw, response, err) +} + +func (c *context) busyWorkers(rw web.ResponseWriter, r *web.Request) { + observations, err := c.client.WorkerObservations() + if err != nil { + renderError(rw, err) + return + } + + var busyObservations []*work.WorkerObservation + for _, ob := range observations { + if ob.IsBusy { + busyObservations = append(busyObservations, ob) + } + } + + render(rw, busyObservations, err) +} + +func (c *context) retryJobs(rw web.ResponseWriter, r *web.Request) { + page, err := parsePage(r) + if err != nil { + renderError(rw, err) + return + } + + jobs, count, err := c.client.RetryJobs(page) + if err != nil { + renderError(rw, err) + return + } + + response := struct { + Count int64 `json:"count"` + Jobs []*work.RetryJob `json:"jobs"` + }{Count: count, Jobs: jobs} + + render(rw, response, err) +} + +func (c *context) scheduledJobs(rw web.ResponseWriter, r *web.Request) { + page, err := parsePage(r) + if err != nil { + renderError(rw, err) + return + } + + jobs, count, err := c.client.ScheduledJobs(page) + if err != nil { + renderError(rw, err) + return + } + + response := struct { + Count int64 `json:"count"` + Jobs []*work.ScheduledJob `json:"jobs"` + }{Count: count, Jobs: jobs} + + render(rw, response, err) +} + +func (c *context) deadJobs(rw web.ResponseWriter, r *web.Request) { + page, err := parsePage(r) + if err != nil { + renderError(rw, err) + return + } + + jobs, count, err := c.client.DeadJobs(page) + if err != nil { + renderError(rw, err) + return + } + + response := struct { + Count int64 `json:"count"` + Jobs []*work.DeadJob `json:"jobs"` + }{Count: count, Jobs: jobs} + + render(rw, response, err) +} + +func (c *context) deleteDeadJob(rw web.ResponseWriter, r *web.Request) { + diedAt, err := strconv.ParseInt(r.PathParams["died_at"], 10, 64) + if err != nil { + renderError(rw, err) + return + } + + err = c.client.DeleteDeadJob(diedAt, r.PathParams["job_id"]) + + render(rw, map[string]string{"status": "ok"}, err) +} + +func (c *context) retryDeadJob(rw web.ResponseWriter, r *web.Request) { + diedAt, err := strconv.ParseInt(r.PathParams["died_at"], 10, 64) + if err != nil { + renderError(rw, err) + return + } + + err = c.client.RetryDeadJob(diedAt, r.PathParams["job_id"]) + + render(rw, map[string]string{"status": "ok"}, err) +} + +func (c *context) deleteAllDeadJobs(rw web.ResponseWriter, r *web.Request) { + err := c.client.DeleteAllDeadJobs() + render(rw, map[string]string{"status": "ok"}, err) +} + +func (c *context) retryAllDeadJobs(rw web.ResponseWriter, r *web.Request) { + err := c.client.RetryAllDeadJobs() + render(rw, map[string]string{"status": "ok"}, err) +} diff --git a/webui/options.go b/webui/options.go deleted file mode 100644 index bab56722..00000000 --- a/webui/options.go +++ /dev/null @@ -1,11 +0,0 @@ -package webui - -type opts func(*Server) *Server - -func WithPrefix(prefix string) opts { - return func(server *Server) *Server { - server.pathPrefix = prefix - - return server - } -} diff --git a/webui/router.go b/webui/router.go index e1071429..4babd8f8 100644 --- a/webui/router.go +++ b/webui/router.go @@ -4,36 +4,39 @@ import ( "html/template" "github.com/gocraft/web" + "github.com/gojek/work" ) -func (s *Server) setupRouter() { - s.router.Middleware(func(c *context, rw web.ResponseWriter, r *web.Request, next web.NextMiddlewareFunc) { - c.Server = s - next(rw, r) - }) - s.router.Middleware(func(rw web.ResponseWriter, r *web.Request, next web.NextMiddlewareFunc) { +type RouterOptions struct { + PathPrefix string +} + +func NewRouter(client *work.Client, opts RouterOptions) *web.Router { + ctx := context{client: client} + router := web.New(ctx) + + router.Middleware(func(rw web.ResponseWriter, r *web.Request, next web.NextMiddlewareFunc) { rw.Header().Set("Content-Type", "application/json; charset=utf-8") next(rw, r) }) - subRouter := s.router.Subrouter(context{}, s.pathPrefix) - - subRouter.Get("/ping", (*context).ping) - subRouter.Get("/queues", (*context).queues) - subRouter.Get("/worker_pools", (*context).workerPools) - subRouter.Get("/busy_workers", (*context).busyWorkers) - subRouter.Get("/retry_jobs", (*context).retryJobs) - subRouter.Get("/scheduled_jobs", (*context).scheduledJobs) - subRouter.Get("/dead_jobs", (*context).deadJobs) - subRouter.Post("/delete_dead_job/:died_at:\\d.*/:job_id", (*context).deleteDeadJob) - subRouter.Post("/retry_dead_job/:died_at:\\d.*/:job_id", (*context).retryDeadJob) - subRouter.Post("/delete_all_dead_jobs", (*context).deleteAllDeadJobs) - subRouter.Post("/retry_all_dead_jobs", (*context).retryAllDeadJobs) + subRouter := router.Subrouter(ctx, opts.PathPrefix) + subRouter.Get("/ping", ctx.ping) + subRouter.Get("/queues", ctx.queues) + subRouter.Get("/worker_pools", ctx.workerPools) + subRouter.Get("/busy_workers", ctx.busyWorkers) + subRouter.Get("/retry_jobs", ctx.retryJobs) + subRouter.Get("/scheduled_jobs", ctx.scheduledJobs) + subRouter.Get("/dead_jobs", ctx.deadJobs) + subRouter.Post("/delete_dead_job/:died_at:\\d.*/:job_id", ctx.deleteDeadJob) + subRouter.Post("/retry_dead_job/:died_at:\\d.*/:job_id", ctx.retryDeadJob) + subRouter.Post("/delete_all_dead_jobs", ctx.deleteAllDeadJobs) + subRouter.Post("/retry_all_dead_jobs", ctx.retryAllDeadJobs) // // Build the HTML page: // - assetRouter := subRouter.Subrouter(context{}, "") + assetRouter := subRouter.Subrouter(ctx, "") assetRouter.Get("/", func(c *context, rw web.ResponseWriter, req *web.Request) { rw.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -46,11 +49,8 @@ func (s *Server) setupRouter() { return } - data := struct { - PathPrefix string - }{ - PathPrefix: c.pathPrefix, - } + // TODO: check if map works + data := struct{ PathPrefix string }{PathPrefix: opts.PathPrefix} err = indexTemplate.Execute(rw, data) if err != nil { @@ -63,4 +63,6 @@ func (s *Server) setupRouter() { rw.Header().Set("Content-Type", "application/javascript; charset=utf-8") _, _ = rw.Write(mustAsset("work.js")) }) + + return router } diff --git a/webui/server.go b/webui/server.go new file mode 100644 index 00000000..dda82cc2 --- /dev/null +++ b/webui/server.go @@ -0,0 +1,52 @@ +package webui + +import ( + "net/http" + "sync" + + "github.com/braintree/manners" + "github.com/gocraft/web" + "github.com/gojek/work" + "github.com/gomodule/redigo/redis" +) + +// Server implements an HTTP server which exposes a JSON API to view and manage gojek/work items. +type Server struct { + namespace string + pool *redis.Pool + hostPort string + server *manners.GracefulServer + wg sync.WaitGroup + router *web.Router + pathPrefix string +} + +// NewServer creates and returns a new server. The 'namespace' param is the redis namespace to use. The hostPort param is the address to bind on to expose the API. +func NewServer(namespace string, pool *redis.Pool, hostPort string) *Server { + client := work.NewClient(namespace, pool) + router := NewRouter(client, RouterOptions{}) + + return &Server{ + namespace: namespace, + pool: pool, + hostPort: hostPort, + router: router, + server: manners.NewWithServer(&http.Server{Addr: hostPort, Handler: router}), + } +} + +// Start starts the server listening for requests on the hostPort specified in NewServer. +func (w *Server) Start() { + w.wg.Add(1) + go func(w *Server) { + _ = w.server.ListenAndServe() + + w.wg.Done() + }(w) +} + +// Stop stops the server and blocks until it has finished. +func (w *Server) Stop() { + w.server.Close() + w.wg.Wait() +} diff --git a/webui/webui.go b/webui/webui.go index ac19e65a..69bca99f 100644 --- a/webui/webui.go +++ b/webui/webui.go @@ -5,53 +5,11 @@ import ( "fmt" "net/http" "strconv" - "sync" - "time" - "github.com/braintree/manners" "github.com/gocraft/web" - "github.com/gojek/work" "github.com/gojek/work/webui/internal/assets" - "github.com/gomodule/redigo/redis" ) -// Server implements an HTTP server which exposes a JSON API to view and manage gojek/work items. -type Server struct { - namespace string - pool *redis.Pool - client *work.Client - hostPort string - server *manners.GracefulServer - wg sync.WaitGroup - router *web.Router - pathPrefix string -} - -type context struct { - *Server -} - -// NewServer creates and returns a new server. The 'namespace' param is the redis namespace to use. The hostPort param is the address to bind on to expose the API. -func NewServer(namespace string, pool *redis.Pool, hostPort string, serverOptions ...opts) *Server { - server := &Server{ - namespace: namespace, - pool: pool, - client: work.NewClient(namespace, pool), - hostPort: hostPort, - router: web.New(context{}), - } - - for _, serverOption := range serverOptions { - server = serverOption(server) - } - - server.setupRouter() - - server.server = manners.NewWithServer(&http.Server{Addr: hostPort, Handler: server.router}) - - return server -} - func mustAsset(name string) []byte { b, err := assets.Asset(name) if err != nil { @@ -60,153 +18,6 @@ func mustAsset(name string) []byte { return b } -// Start starts the server listening for requests on the hostPort specified in NewServer. -func (w *Server) Start() { - w.wg.Add(1) - go func(w *Server) { - w.server.ListenAndServe() - w.wg.Done() - }(w) -} - -// Stop stops the server and blocks until it has finished. -func (w *Server) Stop() { - w.server.Close() - w.wg.Wait() -} - -func (w *Server) Router() *web.Router { - return w.router -} - -func (c *context) ping(rw web.ResponseWriter, r *web.Request) { - render(rw, map[string]string{"ping": "pong", "current_time": time.Now().Format(time.RFC3339)}, nil) -} - -func (c *context) queues(rw web.ResponseWriter, r *web.Request) { - response, err := c.client.Queues() - render(rw, response, err) -} - -func (c *context) workerPools(rw web.ResponseWriter, r *web.Request) { - response, err := c.client.WorkerPoolHeartbeats() - render(rw, response, err) -} - -func (c *context) busyWorkers(rw web.ResponseWriter, r *web.Request) { - observations, err := c.client.WorkerObservations() - if err != nil { - renderError(rw, err) - return - } - - var busyObservations []*work.WorkerObservation - for _, ob := range observations { - if ob.IsBusy { - busyObservations = append(busyObservations, ob) - } - } - - render(rw, busyObservations, err) -} - -func (c *context) retryJobs(rw web.ResponseWriter, r *web.Request) { - page, err := parsePage(r) - if err != nil { - renderError(rw, err) - return - } - - jobs, count, err := c.client.RetryJobs(page) - if err != nil { - renderError(rw, err) - return - } - - response := struct { - Count int64 `json:"count"` - Jobs []*work.RetryJob `json:"jobs"` - }{Count: count, Jobs: jobs} - - render(rw, response, err) -} - -func (c *context) scheduledJobs(rw web.ResponseWriter, r *web.Request) { - page, err := parsePage(r) - if err != nil { - renderError(rw, err) - return - } - - jobs, count, err := c.client.ScheduledJobs(page) - if err != nil { - renderError(rw, err) - return - } - - response := struct { - Count int64 `json:"count"` - Jobs []*work.ScheduledJob `json:"jobs"` - }{Count: count, Jobs: jobs} - - render(rw, response, err) -} - -func (c *context) deadJobs(rw web.ResponseWriter, r *web.Request) { - page, err := parsePage(r) - if err != nil { - renderError(rw, err) - return - } - - jobs, count, err := c.client.DeadJobs(page) - if err != nil { - renderError(rw, err) - return - } - - response := struct { - Count int64 `json:"count"` - Jobs []*work.DeadJob `json:"jobs"` - }{Count: count, Jobs: jobs} - - render(rw, response, err) -} - -func (c *context) deleteDeadJob(rw web.ResponseWriter, r *web.Request) { - diedAt, err := strconv.ParseInt(r.PathParams["died_at"], 10, 64) - if err != nil { - renderError(rw, err) - return - } - - err = c.client.DeleteDeadJob(diedAt, r.PathParams["job_id"]) - - render(rw, map[string]string{"status": "ok"}, err) -} - -func (c *context) retryDeadJob(rw web.ResponseWriter, r *web.Request) { - diedAt, err := strconv.ParseInt(r.PathParams["died_at"], 10, 64) - if err != nil { - renderError(rw, err) - return - } - - err = c.client.RetryDeadJob(diedAt, r.PathParams["job_id"]) - - render(rw, map[string]string{"status": "ok"}, err) -} - -func (c *context) deleteAllDeadJobs(rw web.ResponseWriter, r *web.Request) { - err := c.client.DeleteAllDeadJobs() - render(rw, map[string]string{"status": "ok"}, err) -} - -func (c *context) retryAllDeadJobs(rw web.ResponseWriter, r *web.Request) { - err := c.client.RetryAllDeadJobs() - render(rw, map[string]string{"status": "ok"}, err) -} - func render(rw web.ResponseWriter, jsonable interface{}, err error) { if err != nil { renderError(rw, err) diff --git a/webui/webui_test.go b/webui/webui_test.go index 51c80188..d878cb06 100644 --- a/webui/webui_test.go +++ b/webui/webui_test.go @@ -34,29 +34,18 @@ func TestWebUIPing(t *testing.T) { cleanKeyspace(ns, pool) t.Run("with inbuilt server", func(t *testing.T) { - t.Run("vanilla server", func(t *testing.T) { - s := NewServer(ns, pool, ":6666") - - recorder := httptest.NewRecorder() - request, _ := http.NewRequest("GET", "/ping", nil) - s.router.ServeHTTP(recorder, request) - assert.Equal(t, 200, recorder.Code) - }) - - t.Run("with prefix", func(t *testing.T) { - s := NewServer(ns, pool, ":6666", WithPrefix("/api")) + s := NewServer(ns, pool, ":6666") - recorder := httptest.NewRecorder() - request, _ := http.NewRequest("GET", "/api/ping", nil) - s.router.ServeHTTP(recorder, request) - assert.Equal(t, 200, recorder.Code) - }) + recorder := httptest.NewRecorder() + request, _ := http.NewRequest("GET", "/ping", nil) + s.router.ServeHTTP(recorder, request) + assert.Equal(t, 200, recorder.Code) }) t.Run("with external server", func(t *testing.T) { - t.Run("vanilla server", func(t *testing.T) { - unstartedWorkerUI := NewServer(ns, pool, ":6666") - testServer := httptest.NewServer(unstartedWorkerUI.Router()) + t.Run("no prefix", func(t *testing.T) { + router := NewRouter(work.NewClient(ns, pool), RouterOptions{}) + testServer := httptest.NewServer(router) request, err := http.NewRequest("GET", fmt.Sprintf("%s/ping", testServer.URL), nil) require.NoError(t, err) @@ -68,8 +57,8 @@ func TestWebUIPing(t *testing.T) { }) t.Run("with prefix", func(t *testing.T) { - unstartedWorkerUI := NewServer(ns, pool, ":6666", WithPrefix("/api")) - testServer := httptest.NewServer(unstartedWorkerUI.Router()) + router := NewRouter(work.NewClient(ns, pool), RouterOptions{PathPrefix: "/api"}) + testServer := httptest.NewServer(router) request, err := http.NewRequest("GET", fmt.Sprintf("%s/api/ping", testServer.URL), nil) require.NoError(t, err) @@ -524,18 +513,19 @@ func TestWebUIAssets(t *testing.T) { t.Run("router has prefix", func(t *testing.T) { pool := newTestPool(t) ns := "testwork" - s := NewServer(ns, pool, ":6666", WithPrefix("/prefix")) + + router := NewRouter(work.NewClient(ns, pool), RouterOptions{PathPrefix: "/prefix"}) recorder := httptest.NewRecorder() request, _ := http.NewRequest("GET", "/prefix", nil) - s.router.ServeHTTP(recorder, request) + router.ServeHTTP(recorder, request) body := string(recorder.Body.Bytes()) assert.Regexp(t, "html", body) assert.Regexp(t, `src="/prefix/work.js"`, body) recorder = httptest.NewRecorder() request, _ = http.NewRequest("GET", "/prefix/work.js", nil) - s.router.ServeHTTP(recorder, request) + router.ServeHTTP(recorder, request) }) }