From 27973e827bee38f2d2644bcebbfe633837e040ce Mon Sep 17 00:00:00 2001 From: gabriel ruttner Date: Mon, 1 Jul 2024 11:20:47 -0400 Subject: [PATCH 1/3] feat: worker paused state --- .../openapi/components/schemas/_index.yaml | 2 + .../openapi/components/schemas/worker.yaml | 8 + .../openapi/paths/worker/worker.yaml | 43 ++ api/v1/server/handlers/workers/list.go | 2 +- api/v1/server/handlers/workers/update.go | 35 ++ api/v1/server/oas/gen/openapi.gen.go | 410 +++++++++++------- api/v1/server/oas/transformers/worker.go | 16 +- frontend/app/src/lib/api/generated/Api.ts | 20 + .../src/lib/api/generated/data-contracts.ts | 7 +- .../src/pages/main/workers/$worker/index.tsx | 85 +++- .../main/workers/components/worker-table.tsx | 3 +- pkg/client/rest/gen.go | 172 ++++++++ pkg/repository/prisma/db/db_gen.go | 162 ++++++- .../prisma/dbsqlc/get_group_key_runs.sql | 1 + .../prisma/dbsqlc/get_group_key_runs.sql.go | 1 + pkg/repository/prisma/dbsqlc/models.go | 1 + pkg/repository/prisma/dbsqlc/schema.sql | 1 + pkg/repository/prisma/dbsqlc/step_runs.sql | 3 + pkg/repository/prisma/dbsqlc/step_runs.sql.go | 3 + pkg/repository/prisma/dbsqlc/workers.sql | 3 +- pkg/repository/prisma/dbsqlc/workers.sql.go | 25 +- pkg/repository/prisma/worker.go | 25 ++ pkg/repository/worker.go | 6 + .../20240701144845_v0_35_0/migration.sql | 2 + prisma/schema.prisma | 5 +- sql/migrations/20240701144852_v0_35_0.sql | 2 + sql/migrations/atlas.sum | 3 +- sql/schema/schema.sql | 1 + 28 files changed, 854 insertions(+), 193 deletions(-) create mode 100644 api/v1/server/handlers/workers/update.go create mode 100644 prisma/migrations/20240701144845_v0_35_0/migration.sql create mode 100644 sql/migrations/20240701144852_v0_35_0.sql diff --git a/api-contracts/openapi/components/schemas/_index.yaml b/api-contracts/openapi/components/schemas/_index.yaml index 9f9defa0fb..f7c72a885b 100644 --- a/api-contracts/openapi/components/schemas/_index.yaml +++ b/api-contracts/openapi/components/schemas/_index.yaml @@ -162,6 +162,8 @@ WorkerList: $ref: "./worker.yaml#/WorkerList" Worker: $ref: "./worker.yaml#/Worker" +UpdateWorkerRequest: + $ref: "./worker.yaml#/UpdateWorkerRequest" APIToken: $ref: "./api_tokens.yaml#/APIToken" CreateAPITokenRequest: diff --git a/api-contracts/openapi/components/schemas/worker.yaml b/api-contracts/openapi/components/schemas/worker.yaml index 5e1262ba07..8bda8da4f8 100644 --- a/api-contracts/openapi/components/schemas/worker.yaml +++ b/api-contracts/openapi/components/schemas/worker.yaml @@ -31,6 +31,7 @@ Worker: enum: - ACTIVE - INACTIVE + - PAUSED maxRuns: type: integer description: The maximum number of runs this worker can execute concurrently. @@ -49,6 +50,13 @@ Worker: - name type: object +UpdateWorkerRequest: + properties: + isPaused: + type: boolean + description: Whether the worker is paused and cannot accept new runs. + type: object + WorkerList: properties: pagination: diff --git a/api-contracts/openapi/paths/worker/worker.yaml b/api-contracts/openapi/paths/worker/worker.yaml index 0ea437f9be..1d3c73626f 100644 --- a/api-contracts/openapi/paths/worker/worker.yaml +++ b/api-contracts/openapi/paths/worker/worker.yaml @@ -37,6 +37,49 @@ withTenant: - Worker withWorker: + patch: + x-resources: ["tenant", "worker"] + description: Update a worker + operationId: worker:update + parameters: + - description: The worker id + in: path + name: worker + required: true + schema: + type: string + format: uuid + minLength: 36 + maxLength: 36 + requestBody: + content: + application/json: + schema: + $ref: "../../components/schemas/_index.yaml#/UpdateWorkerRequest" + description: The worker update + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "../../components/schemas/_index.yaml#/Worker" + description: Successfully updated the worker + "400": + content: + application/json: + schema: + $ref: "../../components/schemas/_index.yaml#/APIErrors" + description: A malformed or bad request + "403": + content: + application/json: + schema: + $ref: "../../components/schemas/_index.yaml#/APIErrors" + description: Forbidden + summary: Update worker + tags: + - Worker get: x-resources: ["tenant", "worker"] description: Get a worker diff --git a/api/v1/server/handlers/workers/list.go b/api/v1/server/handlers/workers/list.go index 5f461865aa..2cae8c8f24 100644 --- a/api/v1/server/handlers/workers/list.go +++ b/api/v1/server/handlers/workers/list.go @@ -30,7 +30,7 @@ func (t *WorkerService) WorkerList(ctx echo.Context, request gen.WorkerListReque workerCp := worker slots := int(worker.Slots) - rows[i] = *transformers.ToWorkerSqlc(&workerCp.Worker, &worker.RunningStepRuns, &slots) + rows[i] = *transformers.ToWorkerSqlc(&workerCp.Worker, &slots) } return gen.WorkerList200JSONResponse( diff --git a/api/v1/server/handlers/workers/update.go b/api/v1/server/handlers/workers/update.go new file mode 100644 index 0000000000..60aaddf76b --- /dev/null +++ b/api/v1/server/handlers/workers/update.go @@ -0,0 +1,35 @@ +package workers + +import ( + "github.com/labstack/echo/v4" + + "github.com/hatchet-dev/hatchet/api/v1/server/oas/gen" + "github.com/hatchet-dev/hatchet/api/v1/server/oas/transformers" + "github.com/hatchet-dev/hatchet/pkg/repository" + "github.com/hatchet-dev/hatchet/pkg/repository/prisma/db" +) + +func (t *WorkerService) WorkerUpdate(ctx echo.Context, request gen.WorkerUpdateRequestObject) (gen.WorkerUpdateResponseObject, error) { + worker := ctx.Get("worker").(*db.WorkerModel) + + // validate the request + if apiErrors, err := t.config.Validator.ValidateAPI(request.Body); err != nil { + return nil, err + } else if apiErrors != nil { + return gen.WorkerUpdate400JSONResponse(*apiErrors), nil + } + + update := repository.ApiUpdateWorkerOpts{} + + if request.Body.IsPaused != nil { + update.IsPaused = request.Body.IsPaused + } + + updatedWorker, err := t.config.APIRepository.Worker().UpdateWorker(worker.TenantID, worker.ID, update) + + if err != nil { + return nil, err + } + + return gen.WorkerUpdate200JSONResponse(*transformers.ToWorkerSqlc(updatedWorker, nil)), nil +} diff --git a/api/v1/server/oas/gen/openapi.gen.go b/api/v1/server/oas/gen/openapi.gen.go index 8af86d1573..1b2b93d4bf 100644 --- a/api/v1/server/oas/gen/openapi.gen.go +++ b/api/v1/server/oas/gen/openapi.gen.go @@ -121,6 +121,7 @@ const ( const ( ACTIVE WorkerStatus = "ACTIVE" INACTIVE WorkerStatus = "INACTIVE" + PAUSED WorkerStatus = "PAUSED" ) // Defines values for WorkflowConcurrencyLimitStrategy. @@ -776,6 +777,12 @@ type UpdateTenantRequest struct { Name *string `json:"name,omitempty"` } +// UpdateWorkerRequest defines model for UpdateWorkerRequest. +type UpdateWorkerRequest struct { + // IsPaused Whether the worker is paused and cannot accept new runs. + IsPaused *bool `json:"isPaused,omitempty"` +} + // User defines model for User. type User struct { // Email The email address of the user. @@ -1301,6 +1308,9 @@ type UserUpdatePasswordJSONRequestBody = UserChangePasswordRequest // UserCreateJSONRequestBody defines body for UserCreate for application/json ContentType. type UserCreateJSONRequestBody = UserRegisterRequest +// WorkerUpdateJSONRequestBody defines body for WorkerUpdate for application/json ContentType. +type WorkerUpdateJSONRequestBody = UpdateWorkerRequest + // WorkflowRunCreateJSONRequestBody defines body for WorkflowRunCreate for application/json ContentType. type WorkflowRunCreateJSONRequestBody = TriggerWorkflowRunRequest @@ -1504,6 +1514,9 @@ type ServerInterface interface { // Get worker // (GET /api/v1/workers/{worker}) WorkerGet(ctx echo.Context, worker openapi_types.UUID) error + // Update worker + // (PATCH /api/v1/workers/{worker}) + WorkerUpdate(ctx echo.Context, worker openapi_types.UUID) error // Delete workflow // (DELETE /api/v1/workflows/{workflow}) WorkflowDelete(ctx echo.Context, workflow openapi_types.UUID) error @@ -2975,6 +2988,26 @@ func (w *ServerInterfaceWrapper) WorkerGet(ctx echo.Context) error { return err } +// WorkerUpdate converts echo context to params. +func (w *ServerInterfaceWrapper) WorkerUpdate(ctx echo.Context) error { + var err error + // ------------- Path parameter "worker" ------------- + var worker openapi_types.UUID + + err = runtime.BindStyledParameterWithLocation("simple", false, "worker", runtime.ParamLocationPath, ctx.Param("worker"), &worker) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter worker: %s", err)) + } + + ctx.Set(BearerAuthScopes, []string{}) + + ctx.Set(CookieAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.WorkerUpdate(ctx, worker) + return err +} + // WorkflowDelete converts echo context to params. func (w *ServerInterfaceWrapper) WorkflowDelete(ctx echo.Context) error { var err error @@ -3232,6 +3265,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/api/v1/users/slack/callback", wrapper.UserUpdateSlackOauthCallback) router.DELETE(baseURL+"/api/v1/webhook-workers/:webhook", wrapper.WebhookDelete) router.GET(baseURL+"/api/v1/workers/:worker", wrapper.WorkerGet) + router.PATCH(baseURL+"/api/v1/workers/:worker", wrapper.WorkerUpdate) router.DELETE(baseURL+"/api/v1/workflows/:workflow", wrapper.WorkflowDelete) router.GET(baseURL+"/api/v1/workflows/:workflow", wrapper.WorkflowGet) router.GET(baseURL+"/api/v1/workflows/:workflow/metrics", wrapper.WorkflowGetMetrics) @@ -5612,6 +5646,42 @@ func (response WorkerGet403JSONResponse) VisitWorkerGetResponse(w http.ResponseW return json.NewEncoder(w).Encode(response) } +type WorkerUpdateRequestObject struct { + Worker openapi_types.UUID `json:"worker"` + Body *WorkerUpdateJSONRequestBody +} + +type WorkerUpdateResponseObject interface { + VisitWorkerUpdateResponse(w http.ResponseWriter) error +} + +type WorkerUpdate200JSONResponse Worker + +func (response WorkerUpdate200JSONResponse) VisitWorkerUpdateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type WorkerUpdate400JSONResponse APIErrors + +func (response WorkerUpdate400JSONResponse) VisitWorkerUpdateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type WorkerUpdate403JSONResponse APIErrors + +func (response WorkerUpdate403JSONResponse) VisitWorkerUpdateResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + type WorkflowDeleteRequestObject struct { Workflow openapi_types.UUID `json:"workflow"` } @@ -6013,6 +6083,8 @@ type StrictServerInterface interface { WorkerGet(ctx echo.Context, request WorkerGetRequestObject) (WorkerGetResponseObject, error) + WorkerUpdate(ctx echo.Context, request WorkerUpdateRequestObject) (WorkerUpdateResponseObject, error) + WorkflowDelete(ctx echo.Context, request WorkflowDeleteRequestObject) (WorkflowDeleteResponseObject, error) WorkflowGet(ctx echo.Context, request WorkflowGetRequestObject) (WorkflowGetResponseObject, error) @@ -7771,6 +7843,37 @@ func (sh *strictHandler) WorkerGet(ctx echo.Context, worker openapi_types.UUID) return nil } +// WorkerUpdate operation middleware +func (sh *strictHandler) WorkerUpdate(ctx echo.Context, worker openapi_types.UUID) error { + var request WorkerUpdateRequestObject + + request.Worker = worker + + var body WorkerUpdateJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.WorkerUpdate(ctx, request.(WorkerUpdateRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "WorkerUpdate") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(WorkerUpdateResponseObject); ok { + return validResponse.VisitWorkerUpdateResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("Unexpected response type: %T", response) + } + return nil +} + // WorkflowDelete operation middleware func (sh *strictHandler) WorkflowDelete(ctx echo.Context, workflow openapi_types.UUID) error { var request WorkflowDeleteRequestObject @@ -7934,159 +8037,160 @@ func (sh *strictHandler) WorkflowVersionGetDefinition(ctx echo.Context, workflow // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AA==", + "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", + "pfdKXmRp6H3wvOevz/8bAAD//wDhY4H/dwEA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v1/server/oas/transformers/worker.go b/api/v1/server/oas/transformers/worker.go index 87f0b42d7e..e52014c397 100644 --- a/api/v1/server/oas/transformers/worker.go +++ b/api/v1/server/oas/transformers/worker.go @@ -20,6 +20,10 @@ func ToWorker(worker *db.WorkerModel) *gen.Worker { status := gen.ACTIVE + if worker.IsPaused { + status = gen.PAUSED + } + if lastHeartbeat, ok := worker.LastHeartbeatAt(); ok && lastHeartbeat.Add(4*time.Second).Before(time.Now()) { status = gen.INACTIVE } @@ -63,7 +67,7 @@ func ToWorker(worker *db.WorkerModel) *gen.Worker { return res } -func ToWorkerSqlc(worker *dbsqlc.Worker, stepCount *int64, slots *int) *gen.Worker { +func ToWorkerSqlc(worker *dbsqlc.Worker, slots *int) *gen.Worker { dispatcherId := uuid.MustParse(pgUUIDToStr(worker.DispatcherId)) @@ -71,11 +75,19 @@ func ToWorkerSqlc(worker *dbsqlc.Worker, stepCount *int64, slots *int) *gen.Work status := gen.ACTIVE + if worker.IsPaused { + status = gen.PAUSED + } + if worker.LastHeartbeatAt.Time.Add(5 * time.Second).Before(time.Now()) { status = gen.INACTIVE } - availableRuns := maxRuns - *slots + var availableRuns int + + if slots != nil { + availableRuns = maxRuns - *slots + } res := &gen.Worker{ Metadata: *toAPIMetadata(pgUUIDToStr(worker.ID), worker.CreatedAt.Time, worker.UpdatedAt.Time), diff --git a/frontend/app/src/lib/api/generated/Api.ts b/frontend/app/src/lib/api/generated/Api.ts index ee2f1bad79..7d07c8e5a3 100644 --- a/frontend/app/src/lib/api/generated/Api.ts +++ b/frontend/app/src/lib/api/generated/Api.ts @@ -59,6 +59,7 @@ import { UpdateTenantAlertEmailGroupRequest, UpdateTenantInviteRequest, UpdateTenantRequest, + UpdateWorkerRequest, User, UserChangePasswordRequest, UserLoginRequest, @@ -1493,6 +1494,25 @@ export class Api extends HttpClient + this.request({ + path: `/api/v1/workers/${worker}`, + method: "PATCH", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }); /** * @description Get a worker * diff --git a/frontend/app/src/lib/api/generated/data-contracts.ts b/frontend/app/src/lib/api/generated/data-contracts.ts index 117f044524..81bf2d13e2 100644 --- a/frontend/app/src/lib/api/generated/data-contracts.ts +++ b/frontend/app/src/lib/api/generated/data-contracts.ts @@ -850,7 +850,7 @@ export interface Worker { /** The recent step runs for this worker. */ recentStepRuns?: StepRun[]; /** The status of the worker. */ - status?: "ACTIVE" | "INACTIVE"; + status?: "ACTIVE" | "INACTIVE" | "PAUSED"; /** The maximum number of runs this worker can execute concurrently. */ maxRuns?: number; /** The number of runs this worker can execute concurrently. */ @@ -865,6 +865,11 @@ export interface Worker { dispatcherId?: string; } +export interface UpdateWorkerRequest { + /** Whether the worker is paused and cannot accept new runs. */ + isPaused?: boolean; +} + export interface APIToken { metadata: APIResourceMeta; /** diff --git a/frontend/app/src/pages/main/workers/$worker/index.tsx b/frontend/app/src/pages/main/workers/$worker/index.tsx index 92ebaa1b6d..8e2dfcab6b 100644 --- a/frontend/app/src/pages/main/workers/$worker/index.tsx +++ b/frontend/app/src/pages/main/workers/$worker/index.tsx @@ -1,6 +1,6 @@ import { Separator } from '@/components/ui/separator'; -import { queries, Worker } from '@/lib/api'; -import { useQuery } from '@tanstack/react-query'; +import api, { queries, UpdateWorkerRequest, Worker } from '@/lib/api'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { Link, useOutletContext, useParams } from 'react-router-dom'; import invariant from 'tiny-invariant'; import { ServerStackIcon } from '@heroicons/react/24/outline'; @@ -9,7 +9,7 @@ import { DataTable } from '@/components/molecules/data-table/data-table'; import { columns } from './components/step-runs-columns'; import { Loading } from '@/components/ui/loading.tsx'; import { TenantContextType } from '@/lib/outlet'; -import { Badge } from '@/components/ui/badge'; +import { Badge, BadgeProps } from '@/components/ui/badge'; import { Tooltip, TooltipContent, @@ -17,6 +17,8 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import RelativeDate from '@/components/molecules/relative-date'; +import { useApiError } from '@/lib/hooks'; +import queryClient from '@/query-client'; export const isHealthy = (worker?: Worker) => { const reasons = []; @@ -42,34 +44,48 @@ export const isHealthy = (worker?: Worker) => { }; export const WorkerStatus = ({ - status, + status = 'INACTIVE', health, }: { - status?: 'ACTIVE' | 'INACTIVE'; + status?: 'ACTIVE' | 'INACTIVE' | 'PAUSED'; health: string[]; -}) => ( -
- - - - - {status === 'ACTIVE' ? 'Active' : 'Inactive'} - - - - {health.map((reason, i) => ( -
{reason}
- ))} -
-
-
-
-); +}) => { + const label: Record = { + ACTIVE: 'Active', + INACTIVE: 'Inactive', + PAUSED: 'Paused', + }; + + const variant: Record = { + ACTIVE: 'successful', + INACTIVE: 'failed', + PAUSED: 'inProgress', + }; + + return ( +
+ + + + {label[status]} + + + {health.map((reason, i) => ( +
{reason}
+ ))} +
+
+
+
+ ); +}; export default function ExpandedWorkflowRun() { const { tenant } = useOutletContext(); invariant(tenant); + const { handleApiError } = useApiError({}); + const params = useParams(); invariant(params.worker); @@ -82,6 +98,19 @@ export default function ExpandedWorkflowRun() { const healthy = isHealthy(worker); + const updateWorker = useMutation({ + mutationKey: ['worker:update', worker?.metadata.id], + mutationFn: async (data: UpdateWorkerRequest) => + (await api.workerUpdate(worker!.metadata.id, data)).data, + onSuccess: async () => { + // Handle success logic here + await queryClient.invalidateQueries({ + queryKey: queries.workers.get(worker!.metadata.id).queryKey, + }); + }, + onError: handleApiError, + }); + if (!worker || workerQuery.isLoading || !workerQuery.data) { return ; } @@ -100,6 +129,16 @@ export default function ExpandedWorkflowRun() { +

First Connected: {worker.lastListenerEstablished && ( diff --git a/frontend/app/src/pages/main/workers/components/worker-table.tsx b/frontend/app/src/pages/main/workers/components/worker-table.tsx index 8afb0a42f7..efa7e4232e 100644 --- a/frontend/app/src/pages/main/workers/components/worker-table.tsx +++ b/frontend/app/src/pages/main/workers/components/worker-table.tsx @@ -34,7 +34,7 @@ export function WorkersTable() { const [columnFilters, setColumnFilters] = useState([ { id: 'status', - value: ['ACTIVE'], + value: ['ACTIVE', 'PAUSED'], }, ]); @@ -173,6 +173,7 @@ export function WorkersTable() { title: 'Status', options: [ { value: 'ACTIVE', label: 'Active' }, + { value: 'PAUSED', label: 'Paused' }, { value: 'INACTIVE', label: 'Inactive' }, ], }, diff --git a/pkg/client/rest/gen.go b/pkg/client/rest/gen.go index 287f960fdf..e6ae1ddd93 100644 --- a/pkg/client/rest/gen.go +++ b/pkg/client/rest/gen.go @@ -118,6 +118,7 @@ const ( const ( ACTIVE WorkerStatus = "ACTIVE" INACTIVE WorkerStatus = "INACTIVE" + PAUSED WorkerStatus = "PAUSED" ) // Defines values for WorkflowConcurrencyLimitStrategy. @@ -773,6 +774,12 @@ type UpdateTenantRequest struct { Name *string `json:"name,omitempty"` } +// UpdateWorkerRequest defines model for UpdateWorkerRequest. +type UpdateWorkerRequest struct { + // IsPaused Whether the worker is paused and cannot accept new runs. + IsPaused *bool `json:"isPaused,omitempty"` +} + // User defines model for User. type User struct { // Email The email address of the user. @@ -1298,6 +1305,9 @@ type UserUpdatePasswordJSONRequestBody = UserChangePasswordRequest // UserCreateJSONRequestBody defines body for UserCreate for application/json ContentType. type UserCreateJSONRequestBody = UserRegisterRequest +// WorkerUpdateJSONRequestBody defines body for WorkerUpdate for application/json ContentType. +type WorkerUpdateJSONRequestBody = UpdateWorkerRequest + // WorkflowRunCreateJSONRequestBody defines body for WorkflowRunCreate for application/json ContentType. type WorkflowRunCreateJSONRequestBody = TriggerWorkflowRunRequest @@ -1608,6 +1618,11 @@ type ClientInterface interface { // WorkerGet request WorkerGet(ctx context.Context, worker openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + // WorkerUpdateWithBody request with any body + WorkerUpdateWithBody(ctx context.Context, worker openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + WorkerUpdate(ctx context.Context, worker openapi_types.UUID, body WorkerUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // WorkflowDelete request WorkflowDelete(ctx context.Context, workflow openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -2637,6 +2652,30 @@ func (c *Client) WorkerGet(ctx context.Context, worker openapi_types.UUID, reqEd return c.Client.Do(req) } +func (c *Client) WorkerUpdateWithBody(ctx context.Context, worker openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewWorkerUpdateRequestWithBody(c.Server, worker, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) WorkerUpdate(ctx context.Context, worker openapi_types.UUID, body WorkerUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewWorkerUpdateRequest(c.Server, worker, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) WorkflowDelete(ctx context.Context, workflow openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewWorkflowDeleteRequest(c.Server, workflow) if err != nil { @@ -5708,6 +5747,53 @@ func NewWorkerGetRequest(server string, worker openapi_types.UUID) (*http.Reques return req, nil } +// NewWorkerUpdateRequest calls the generic WorkerUpdate builder with application/json body +func NewWorkerUpdateRequest(server string, worker openapi_types.UUID, body WorkerUpdateJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewWorkerUpdateRequestWithBody(server, worker, "application/json", bodyReader) +} + +// NewWorkerUpdateRequestWithBody generates requests for WorkerUpdate with any type of body +func NewWorkerUpdateRequestWithBody(server string, worker openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "worker", runtime.ParamLocationPath, worker) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/workers/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PATCH", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewWorkflowDeleteRequest generates requests for WorkflowDelete func NewWorkflowDeleteRequest(server string, workflow openapi_types.UUID) (*http.Request, error) { var err error @@ -6306,6 +6392,11 @@ type ClientWithResponsesInterface interface { // WorkerGetWithResponse request WorkerGetWithResponse(ctx context.Context, worker openapi_types.UUID, reqEditors ...RequestEditorFn) (*WorkerGetResponse, error) + // WorkerUpdateWithBodyWithResponse request with any body + WorkerUpdateWithBodyWithResponse(ctx context.Context, worker openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*WorkerUpdateResponse, error) + + WorkerUpdateWithResponse(ctx context.Context, worker openapi_types.UUID, body WorkerUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*WorkerUpdateResponse, error) + // WorkflowDeleteWithResponse request WorkflowDeleteWithResponse(ctx context.Context, workflow openapi_types.UUID, reqEditors ...RequestEditorFn) (*WorkflowDeleteResponse, error) @@ -7898,6 +7989,30 @@ func (r WorkerGetResponse) StatusCode() int { return 0 } +type WorkerUpdateResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Worker + JSON400 *APIErrors + JSON403 *APIErrors +} + +// Status returns HTTPResponse.Status +func (r WorkerUpdateResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r WorkerUpdateResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type WorkflowDeleteResponse struct { Body []byte HTTPResponse *http.Response @@ -8785,6 +8900,23 @@ func (c *ClientWithResponses) WorkerGetWithResponse(ctx context.Context, worker return ParseWorkerGetResponse(rsp) } +// WorkerUpdateWithBodyWithResponse request with arbitrary body returning *WorkerUpdateResponse +func (c *ClientWithResponses) WorkerUpdateWithBodyWithResponse(ctx context.Context, worker openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*WorkerUpdateResponse, error) { + rsp, err := c.WorkerUpdateWithBody(ctx, worker, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseWorkerUpdateResponse(rsp) +} + +func (c *ClientWithResponses) WorkerUpdateWithResponse(ctx context.Context, worker openapi_types.UUID, body WorkerUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*WorkerUpdateResponse, error) { + rsp, err := c.WorkerUpdate(ctx, worker, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseWorkerUpdateResponse(rsp) +} + // WorkflowDeleteWithResponse request returning *WorkflowDeleteResponse func (c *ClientWithResponses) WorkflowDeleteWithResponse(ctx context.Context, workflow openapi_types.UUID, reqEditors ...RequestEditorFn) (*WorkflowDeleteResponse, error) { rsp, err := c.WorkflowDelete(ctx, workflow, reqEditors...) @@ -11360,6 +11492,46 @@ func ParseWorkerGetResponse(rsp *http.Response) (*WorkerGetResponse, error) { return response, nil } +// ParseWorkerUpdateResponse parses an HTTP response from a WorkerUpdateWithResponse call +func ParseWorkerUpdateResponse(rsp *http.Response) (*WorkerUpdateResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &WorkerUpdateResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Worker + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest APIErrors + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest APIErrors + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + } + + return response, nil +} + // ParseWorkflowDeleteResponse parses an HTTP response from a WorkflowDeleteWithResponse call func ParseWorkflowDeleteResponse(rsp *http.Response) (*WorkflowDeleteResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/pkg/repository/prisma/db/db_gen.go b/pkg/repository/prisma/db/db_gen.go index 39e5b5de4a..05b0f366e8 100644 --- a/pkg/repository/prisma/db/db_gen.go +++ b/pkg/repository/prisma/db/db_gen.go @@ -1367,7 +1367,10 @@ model Worker { // the last heartbeat time lastHeartbeatAt DateTime? - // whether this worker is active or not + // whether the worker has been marked as paused + isPaused Boolean @default(false) + + // whether this worker GRPC connection is active or not isActive Boolean @default(false) lastListenerEstablished DateTime? @@ -2511,6 +2514,7 @@ const ( WorkerScalarFieldEnumDeletedAt WorkerScalarFieldEnum = "deletedAt" WorkerScalarFieldEnumTenantID WorkerScalarFieldEnum = "tenantId" WorkerScalarFieldEnumLastHeartbeatAt WorkerScalarFieldEnum = "lastHeartbeatAt" + WorkerScalarFieldEnumIsPaused WorkerScalarFieldEnum = "isPaused" WorkerScalarFieldEnumIsActive WorkerScalarFieldEnum = "isActive" WorkerScalarFieldEnumLastListenerEstablished WorkerScalarFieldEnum = "lastListenerEstablished" WorkerScalarFieldEnumName WorkerScalarFieldEnum = "name" @@ -3779,6 +3783,8 @@ const workerFieldTenantID workerPrismaFields = "tenantId" const workerFieldLastHeartbeatAt workerPrismaFields = "lastHeartbeatAt" +const workerFieldIsPaused workerPrismaFields = "isPaused" + const workerFieldIsActive workerPrismaFields = "isActive" const workerFieldLastListenerEstablished workerPrismaFields = "lastListenerEstablished" @@ -9896,6 +9902,7 @@ type InnerWorker struct { DeletedAt *DateTime `json:"deletedAt,omitempty"` TenantID string `json:"tenantId"` LastHeartbeatAt *DateTime `json:"lastHeartbeatAt,omitempty"` + IsPaused bool `json:"isPaused"` IsActive bool `json:"isActive"` LastListenerEstablished *DateTime `json:"lastListenerEstablished,omitempty"` Name string `json:"name"` @@ -9911,6 +9918,7 @@ type RawWorkerModel struct { DeletedAt *RawDateTime `json:"deletedAt,omitempty"` TenantID RawString `json:"tenantId"` LastHeartbeatAt *RawDateTime `json:"lastHeartbeatAt,omitempty"` + IsPaused RawBoolean `json:"isPaused"` IsActive RawBoolean `json:"isActive"` LastListenerEstablished *RawDateTime `json:"lastListenerEstablished,omitempty"` Name RawString `json:"name"` @@ -147649,6 +147657,11 @@ type workerQuery struct { // @optional LastHeartbeatAt workerQueryLastHeartbeatAtDateTime + // IsPaused + // + // @required + IsPaused workerQueryIsPausedBoolean + // IsActive // // @required @@ -149856,6 +149869,74 @@ func (r workerQueryLastHeartbeatAtDateTime) Field() workerPrismaFields { return workerFieldLastHeartbeatAt } +// base struct +type workerQueryIsPausedBoolean struct{} + +// Set the required value of IsPaused +func (r workerQueryIsPausedBoolean) Set(value bool) workerSetParam { + + return workerSetParam{ + data: builder.Field{ + Name: "isPaused", + Value: value, + }, + } + +} + +// Set the optional value of IsPaused dynamically +func (r workerQueryIsPausedBoolean) SetIfPresent(value *Boolean) workerSetParam { + if value == nil { + return workerSetParam{} + } + + return r.Set(*value) +} + +func (r workerQueryIsPausedBoolean) Equals(value bool) workerWithPrismaIsPausedEqualsParam { + + return workerWithPrismaIsPausedEqualsParam{ + data: builder.Field{ + Name: "isPaused", + Fields: []builder.Field{ + { + Name: "equals", + Value: value, + }, + }, + }, + } +} + +func (r workerQueryIsPausedBoolean) EqualsIfPresent(value *bool) workerWithPrismaIsPausedEqualsParam { + if value == nil { + return workerWithPrismaIsPausedEqualsParam{} + } + return r.Equals(*value) +} + +func (r workerQueryIsPausedBoolean) Order(direction SortOrder) workerDefaultParam { + return workerDefaultParam{ + data: builder.Field{ + Name: "isPaused", + Value: direction, + }, + } +} + +func (r workerQueryIsPausedBoolean) Cursor(cursor bool) workerCursorParam { + return workerCursorParam{ + data: builder.Field{ + Name: "isPaused", + Value: cursor, + }, + } +} + +func (r workerQueryIsPausedBoolean) Field() workerPrismaFields { + return workerFieldIsPaused +} + // base struct type workerQueryIsActiveBoolean struct{} @@ -218287,6 +218368,7 @@ var workerOutput = []builder.Output{ {Name: "deletedAt"}, {Name: "tenantId"}, {Name: "lastHeartbeatAt"}, + {Name: "isPaused"}, {Name: "isActive"}, {Name: "lastListenerEstablished"}, {Name: "name"}, @@ -219004,6 +219086,84 @@ func (p workerWithPrismaLastHeartbeatAtEqualsUniqueParam) lastHeartbeatAtField() func (workerWithPrismaLastHeartbeatAtEqualsUniqueParam) unique() {} func (workerWithPrismaLastHeartbeatAtEqualsUniqueParam) equals() {} +type WorkerWithPrismaIsPausedEqualsSetParam interface { + field() builder.Field + getQuery() builder.Query + equals() + workerModel() + isPausedField() +} + +type WorkerWithPrismaIsPausedSetParam interface { + field() builder.Field + getQuery() builder.Query + workerModel() + isPausedField() +} + +type workerWithPrismaIsPausedSetParam struct { + data builder.Field + query builder.Query +} + +func (p workerWithPrismaIsPausedSetParam) field() builder.Field { + return p.data +} + +func (p workerWithPrismaIsPausedSetParam) getQuery() builder.Query { + return p.query +} + +func (p workerWithPrismaIsPausedSetParam) workerModel() {} + +func (p workerWithPrismaIsPausedSetParam) isPausedField() {} + +type WorkerWithPrismaIsPausedWhereParam interface { + field() builder.Field + getQuery() builder.Query + workerModel() + isPausedField() +} + +type workerWithPrismaIsPausedEqualsParam struct { + data builder.Field + query builder.Query +} + +func (p workerWithPrismaIsPausedEqualsParam) field() builder.Field { + return p.data +} + +func (p workerWithPrismaIsPausedEqualsParam) getQuery() builder.Query { + return p.query +} + +func (p workerWithPrismaIsPausedEqualsParam) workerModel() {} + +func (p workerWithPrismaIsPausedEqualsParam) isPausedField() {} + +func (workerWithPrismaIsPausedSetParam) settable() {} +func (workerWithPrismaIsPausedEqualsParam) equals() {} + +type workerWithPrismaIsPausedEqualsUniqueParam struct { + data builder.Field + query builder.Query +} + +func (p workerWithPrismaIsPausedEqualsUniqueParam) field() builder.Field { + return p.data +} + +func (p workerWithPrismaIsPausedEqualsUniqueParam) getQuery() builder.Query { + return p.query +} + +func (p workerWithPrismaIsPausedEqualsUniqueParam) workerModel() {} +func (p workerWithPrismaIsPausedEqualsUniqueParam) isPausedField() {} + +func (workerWithPrismaIsPausedEqualsUniqueParam) unique() {} +func (workerWithPrismaIsPausedEqualsUniqueParam) equals() {} + type WorkerWithPrismaIsActiveEqualsSetParam interface { field() builder.Field getQuery() builder.Query diff --git a/pkg/repository/prisma/dbsqlc/get_group_key_runs.sql b/pkg/repository/prisma/dbsqlc/get_group_key_runs.sql index 2437f3efb1..05dff3e74d 100644 --- a/pkg/repository/prisma/dbsqlc/get_group_key_runs.sql +++ b/pkg/repository/prisma/dbsqlc/get_group_key_runs.sql @@ -55,6 +55,7 @@ WITH valid_workers AS ( w."tenantId" = @tenantId::uuid AND w."lastHeartbeatAt" > NOW() - INTERVAL '5 seconds' AND w."isActive" = true + AND w."isPaused" = false GROUP BY w."id" ), diff --git a/pkg/repository/prisma/dbsqlc/get_group_key_runs.sql.go b/pkg/repository/prisma/dbsqlc/get_group_key_runs.sql.go index a34d336f1a..3ff8601cc2 100644 --- a/pkg/repository/prisma/dbsqlc/get_group_key_runs.sql.go +++ b/pkg/repository/prisma/dbsqlc/get_group_key_runs.sql.go @@ -221,6 +221,7 @@ WITH valid_workers AS ( w."tenantId" = $1::uuid AND w."lastHeartbeatAt" > NOW() - INTERVAL '5 seconds' AND w."isActive" = true + AND w."isPaused" = false GROUP BY w."id" ), diff --git a/pkg/repository/prisma/dbsqlc/models.go b/pkg/repository/prisma/dbsqlc/models.go index 67ffc496c0..e016ebdb54 100644 --- a/pkg/repository/prisma/dbsqlc/models.go +++ b/pkg/repository/prisma/dbsqlc/models.go @@ -1040,6 +1040,7 @@ type Worker struct { MaxRuns int32 `json:"maxRuns"` IsActive bool `json:"isActive"` LastListenerEstablished pgtype.Timestamp `json:"lastListenerEstablished"` + IsPaused bool `json:"isPaused"` } type WorkerSemaphore struct { diff --git a/pkg/repository/prisma/dbsqlc/schema.sql b/pkg/repository/prisma/dbsqlc/schema.sql index c163068a18..8cd25efcc8 100644 --- a/pkg/repository/prisma/dbsqlc/schema.sql +++ b/pkg/repository/prisma/dbsqlc/schema.sql @@ -572,6 +572,7 @@ CREATE TABLE "Worker" ( "maxRuns" INTEGER NOT NULL DEFAULT 100, "isActive" BOOLEAN NOT NULL DEFAULT false, "lastListenerEstablished" TIMESTAMP(3), + "isPaused" BOOLEAN NOT NULL DEFAULT false, CONSTRAINT "Worker_pkey" PRIMARY KEY ("id") ); diff --git a/pkg/repository/prisma/dbsqlc/step_runs.sql b/pkg/repository/prisma/dbsqlc/step_runs.sql index c6f2335c6b..744eb6b484 100644 --- a/pkg/repository/prisma/dbsqlc/step_runs.sql +++ b/pkg/repository/prisma/dbsqlc/step_runs.sql @@ -316,6 +316,7 @@ WITH valid_workers AS ( AND w."lastHeartbeatAt" > NOW() - INTERVAL '5 seconds' -- necessary because isActive is set to false immediately when the stream closes AND w."isActive" = true + AND w."isPaused" = false GROUP BY w."id", w."maxRuns" HAVING @@ -405,6 +406,7 @@ WITH valid_workers AS ( AND w."lastHeartbeatAt" > NOW() - INTERVAL '5 seconds' -- necessary because isActive is set to false immediately when the stream closes AND w."isActive" = true + AND w."isPaused" = false GROUP BY w."id", w."maxRuns" HAVING @@ -524,6 +526,7 @@ WITH valid_workers AS ( AND w."dispatcherId" IS NOT NULL AND w."lastHeartbeatAt" > NOW() - INTERVAL '5 seconds' AND w."isActive" = true + AND w."isPaused" = false AND w."id" IN ( SELECT "_ActionToWorker"."B" FROM "_ActionToWorker" diff --git a/pkg/repository/prisma/dbsqlc/step_runs.sql.go b/pkg/repository/prisma/dbsqlc/step_runs.sql.go index fe1093e112..01d7d11365 100644 --- a/pkg/repository/prisma/dbsqlc/step_runs.sql.go +++ b/pkg/repository/prisma/dbsqlc/step_runs.sql.go @@ -21,6 +21,7 @@ WITH valid_workers AS ( AND w."dispatcherId" IS NOT NULL AND w."lastHeartbeatAt" > NOW() - INTERVAL '5 seconds' AND w."isActive" = true + AND w."isPaused" = false AND w."id" IN ( SELECT "_ActionToWorker"."B" FROM "_ActionToWorker" @@ -909,6 +910,7 @@ WITH valid_workers AS ( AND w."lastHeartbeatAt" > NOW() - INTERVAL '5 seconds' -- necessary because isActive is set to false immediately when the stream closes AND w."isActive" = true + AND w."isPaused" = false GROUP BY w."id", w."maxRuns" HAVING @@ -1019,6 +1021,7 @@ WITH valid_workers AS ( AND w."lastHeartbeatAt" > NOW() - INTERVAL '5 seconds' -- necessary because isActive is set to false immediately when the stream closes AND w."isActive" = true + AND w."isPaused" = false GROUP BY w."id", w."maxRuns" HAVING diff --git a/pkg/repository/prisma/dbsqlc/workers.sql b/pkg/repository/prisma/dbsqlc/workers.sql index e00696af47..4e37010ba7 100644 --- a/pkg/repository/prisma/dbsqlc/workers.sql +++ b/pkg/repository/prisma/dbsqlc/workers.sql @@ -83,7 +83,8 @@ SET "dispatcherId" = coalesce(sqlc.narg('dispatcherId')::uuid, "dispatcherId"), "maxRuns" = coalesce(sqlc.narg('maxRuns')::int, "maxRuns"), "lastHeartbeatAt" = coalesce(sqlc.narg('lastHeartbeatAt')::timestamp, "lastHeartbeatAt"), - "isActive" = coalesce(sqlc.narg('isActive')::boolean, "isActive") + "isActive" = coalesce(sqlc.narg('isActive')::boolean, "isActive"), + "isPaused" = coalesce(sqlc.narg('isPaused')::boolean, "isPaused") WHERE "id" = @id::uuid RETURNING *; diff --git a/pkg/repository/prisma/dbsqlc/workers.sql.go b/pkg/repository/prisma/dbsqlc/workers.sql.go index 04e1eb6177..9ce454b6be 100644 --- a/pkg/repository/prisma/dbsqlc/workers.sql.go +++ b/pkg/repository/prisma/dbsqlc/workers.sql.go @@ -28,7 +28,7 @@ INSERT INTO "Worker" ( $2::text, $3::uuid, $4::int -) RETURNING id, "createdAt", "updatedAt", "deletedAt", "tenantId", "lastHeartbeatAt", name, "dispatcherId", "maxRuns", "isActive", "lastListenerEstablished" +) RETURNING id, "createdAt", "updatedAt", "deletedAt", "tenantId", "lastHeartbeatAt", name, "dispatcherId", "maxRuns", "isActive", "lastListenerEstablished", "isPaused" ` type CreateWorkerParams struct { @@ -58,6 +58,7 @@ func (q *Queries) CreateWorker(ctx context.Context, db DBTX, arg CreateWorkerPar &i.MaxRuns, &i.IsActive, &i.LastListenerEstablished, + &i.IsPaused, ) return &i, err } @@ -67,7 +68,7 @@ DELETE FROM "Worker" WHERE "id" = $1::uuid -RETURNING id, "createdAt", "updatedAt", "deletedAt", "tenantId", "lastHeartbeatAt", name, "dispatcherId", "maxRuns", "isActive", "lastListenerEstablished" +RETURNING id, "createdAt", "updatedAt", "deletedAt", "tenantId", "lastHeartbeatAt", name, "dispatcherId", "maxRuns", "isActive", "lastListenerEstablished", "isPaused" ` func (q *Queries) DeleteWorker(ctx context.Context, db DBTX, id pgtype.UUID) (*Worker, error) { @@ -85,6 +86,7 @@ func (q *Queries) DeleteWorker(ctx context.Context, db DBTX, id pgtype.UUID) (*W &i.MaxRuns, &i.IsActive, &i.LastListenerEstablished, + &i.IsPaused, ) return &i, err } @@ -178,7 +180,7 @@ func (q *Queries) LinkServicesToWorker(ctx context.Context, db DBTX, arg LinkSer const listWorkersWithStepCount = `-- name: ListWorkersWithStepCount :many SELECT - workers.id, workers."createdAt", workers."updatedAt", workers."deletedAt", workers."tenantId", workers."lastHeartbeatAt", workers.name, workers."dispatcherId", workers."maxRuns", workers."isActive", workers."lastListenerEstablished", + workers.id, workers."createdAt", workers."updatedAt", workers."deletedAt", workers."tenantId", workers."lastHeartbeatAt", workers.name, workers."dispatcherId", workers."maxRuns", workers."isActive", workers."lastListenerEstablished", workers."isPaused", COUNT(runs."id") FILTER (WHERE runs."status" = 'RUNNING') AS "runningStepRuns", (SELECT COUNT(*) FROM "WorkerSemaphoreSlot" wss WHERE wss."workerId" = workers."id" AND wss."stepRunId" IS NOT NULL) AS "slots" FROM @@ -253,6 +255,7 @@ func (q *Queries) ListWorkersWithStepCount(ctx context.Context, db DBTX, arg Lis &i.Worker.MaxRuns, &i.Worker.IsActive, &i.Worker.LastListenerEstablished, + &i.Worker.IsPaused, &i.RunningStepRuns, &i.Slots, ); err != nil { @@ -306,10 +309,11 @@ SET "dispatcherId" = coalesce($1::uuid, "dispatcherId"), "maxRuns" = coalesce($2::int, "maxRuns"), "lastHeartbeatAt" = coalesce($3::timestamp, "lastHeartbeatAt"), - "isActive" = coalesce($4::boolean, "isActive") + "isActive" = coalesce($4::boolean, "isActive"), + "isPaused" = coalesce($5::boolean, "isPaused") WHERE - "id" = $5::uuid -RETURNING id, "createdAt", "updatedAt", "deletedAt", "tenantId", "lastHeartbeatAt", name, "dispatcherId", "maxRuns", "isActive", "lastListenerEstablished" + "id" = $6::uuid +RETURNING id, "createdAt", "updatedAt", "deletedAt", "tenantId", "lastHeartbeatAt", name, "dispatcherId", "maxRuns", "isActive", "lastListenerEstablished", "isPaused" ` type UpdateWorkerParams struct { @@ -317,6 +321,7 @@ type UpdateWorkerParams struct { MaxRuns pgtype.Int4 `json:"maxRuns"` LastHeartbeatAt pgtype.Timestamp `json:"lastHeartbeatAt"` IsActive pgtype.Bool `json:"isActive"` + IsPaused pgtype.Bool `json:"isPaused"` ID pgtype.UUID `json:"id"` } @@ -326,6 +331,7 @@ func (q *Queries) UpdateWorker(ctx context.Context, db DBTX, arg UpdateWorkerPar arg.MaxRuns, arg.LastHeartbeatAt, arg.IsActive, + arg.IsPaused, arg.ID, ) var i Worker @@ -341,6 +347,7 @@ func (q *Queries) UpdateWorker(ctx context.Context, db DBTX, arg UpdateWorkerPar &i.MaxRuns, &i.IsActive, &i.LastListenerEstablished, + &i.IsPaused, ) return &i, err } @@ -356,7 +363,7 @@ WHERE "lastListenerEstablished" IS NULL OR "lastListenerEstablished" <= $2::timestamp ) -RETURNING id, "createdAt", "updatedAt", "deletedAt", "tenantId", "lastHeartbeatAt", name, "dispatcherId", "maxRuns", "isActive", "lastListenerEstablished" +RETURNING id, "createdAt", "updatedAt", "deletedAt", "tenantId", "lastHeartbeatAt", name, "dispatcherId", "maxRuns", "isActive", "lastListenerEstablished", "isPaused" ` type UpdateWorkerActiveStatusParams struct { @@ -380,6 +387,7 @@ func (q *Queries) UpdateWorkerActiveStatus(ctx context.Context, db DBTX, arg Upd &i.MaxRuns, &i.IsActive, &i.LastListenerEstablished, + &i.IsPaused, ) return &i, err } @@ -390,7 +398,7 @@ SET "isActive" = $1::boolean WHERE "tenantId" = $2::uuid AND "name" = $3::text -RETURNING id, "createdAt", "updatedAt", "deletedAt", "tenantId", "lastHeartbeatAt", name, "dispatcherId", "maxRuns", "isActive", "lastListenerEstablished" +RETURNING id, "createdAt", "updatedAt", "deletedAt", "tenantId", "lastHeartbeatAt", name, "dispatcherId", "maxRuns", "isActive", "lastListenerEstablished", "isPaused" ` type UpdateWorkersByNameParams struct { @@ -420,6 +428,7 @@ func (q *Queries) UpdateWorkersByName(ctx context.Context, db DBTX, arg UpdateWo &i.MaxRuns, &i.IsActive, &i.LastListenerEstablished, + &i.IsPaused, ); err != nil { return nil, err } diff --git a/pkg/repository/prisma/worker.go b/pkg/repository/prisma/worker.go index 00e8c94f3a..2d7b3fadbc 100644 --- a/pkg/repository/prisma/worker.go +++ b/pkg/repository/prisma/worker.go @@ -125,6 +125,31 @@ func (r *workerAPIRepository) ListWorkers(tenantId string, opts *repository.List return workers, nil } +func (w *workerAPIRepository) UpdateWorker(tenantId, workerId string, opts repository.ApiUpdateWorkerOpts) (*dbsqlc.Worker, error) { + if err := w.v.Validate(opts); err != nil { + return nil, err + } + + updateParams := dbsqlc.UpdateWorkerParams{ + ID: sqlchelpers.UUIDFromStr(workerId), + } + + if opts.IsPaused != nil { + updateParams.IsPaused = pgtype.Bool{ + Bool: *opts.IsPaused, + Valid: true, + } + } + + worker, err := w.queries.UpdateWorker(context.Background(), w.pool, updateParams) + + if err != nil { + return nil, fmt.Errorf("could not update worker: %w", err) + } + + return worker, nil +} + type workerEngineRepository struct { pool *pgxpool.Pool v validator.Validator diff --git a/pkg/repository/worker.go b/pkg/repository/worker.go index 22f6831d93..31b2f3f192 100644 --- a/pkg/repository/worker.go +++ b/pkg/repository/worker.go @@ -52,6 +52,10 @@ type ListWorkersOpts struct { Assignable *bool } +type ApiUpdateWorkerOpts struct { + IsPaused *bool +} + type WorkerAPIRepository interface { // ListWorkers lists workers for the tenant ListWorkers(tenantId string, opts *ListWorkersOpts) ([]*dbsqlc.ListWorkersWithStepCountRow, error) @@ -61,6 +65,8 @@ type WorkerAPIRepository interface { // GetWorkerById returns a worker by its id. GetWorkerById(workerId string) (*db.WorkerModel, error) + + UpdateWorker(tenantId string, workerId string, opts ApiUpdateWorkerOpts) (*dbsqlc.Worker, error) } type WorkerEngineRepository interface { diff --git a/prisma/migrations/20240701144845_v0_35_0/migration.sql b/prisma/migrations/20240701144845_v0_35_0/migration.sql new file mode 100644 index 0000000000..08d93b198f --- /dev/null +++ b/prisma/migrations/20240701144845_v0_35_0/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Worker" ADD COLUMN "isPaused" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3b89ffbb52..0bbc598172 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1293,7 +1293,10 @@ model Worker { // the last heartbeat time lastHeartbeatAt DateTime? - // whether this worker is active or not + // whether the worker has been marked as paused + isPaused Boolean @default(false) + + // whether this worker GRPC connection is active or not isActive Boolean @default(false) lastListenerEstablished DateTime? diff --git a/sql/migrations/20240701144852_v0_35_0.sql b/sql/migrations/20240701144852_v0_35_0.sql new file mode 100644 index 0000000000..20854824ce --- /dev/null +++ b/sql/migrations/20240701144852_v0_35_0.sql @@ -0,0 +1,2 @@ +-- Modify "Worker" table +ALTER TABLE "Worker" ADD COLUMN "isPaused" boolean NOT NULL DEFAULT false; diff --git a/sql/migrations/atlas.sum b/sql/migrations/atlas.sum index aa8462d066..75769a4389 100644 --- a/sql/migrations/atlas.sum +++ b/sql/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:NK+ekkECiuadzun2bA8DNGD9zGTMM8+f44rPVLThpJs= +h1:LXQRtfBJrIn4LxC0e60IvzAFysclKxaKq++WZR4Bi7I= 20240115180414_init.sql h1:Ef3ZyjAHkmJPdGF/dEWCahbwgcg6uGJKnDxW2JCRi2k= 20240122014727_v0_6_0.sql h1:o/LdlteAeFgoHJ3e/M4Xnghqt9826IE/Y/h0q95Acuo= 20240126235456_v0_7_0.sql h1:KiVzt/hXgQ6esbdC6OMJOOWuYEXmy1yeCpmsVAHTFKs= @@ -34,3 +34,4 @@ h1:NK+ekkECiuadzun2bA8DNGD9zGTMM8+f44rPVLThpJs= 20240606145243_v0_31_0.sql h1:ALisDQv8IPGe6MiBSfE/Esdl5x4pzNHIVMavlsBXIPE= 20240625180548_v0.34.0.sql h1:77uSk0VF/jBvEPHCqWC4hmMQqUx4zVnMdTryGsIXt9s= 20240626204339_v0.34.2.sql h1:e2hArnEfcEYcBjEPxZW3axkl4CGt2lHa1oIA2r2fjfY= +20240701144852_v0_35_0.sql h1:q8pPeq4LZp7hxZZp4P08xctwAdQFKDEA9vbj1Ulbn7U= diff --git a/sql/schema/schema.sql b/sql/schema/schema.sql index c163068a18..8cd25efcc8 100644 --- a/sql/schema/schema.sql +++ b/sql/schema/schema.sql @@ -572,6 +572,7 @@ CREATE TABLE "Worker" ( "maxRuns" INTEGER NOT NULL DEFAULT 100, "isActive" BOOLEAN NOT NULL DEFAULT false, "lastListenerEstablished" TIMESTAMP(3), + "isPaused" BOOLEAN NOT NULL DEFAULT false, CONSTRAINT "Worker_pkey" PRIMARY KEY ("id") ); From 7350effd86e5a3b391a0f4a4b33317b4cceacad4 Mon Sep 17 00:00:00 2001 From: gabriel ruttner Date: Mon, 1 Jul 2024 11:39:48 -0400 Subject: [PATCH 2/3] feat: ui --- .../src/pages/main/workers/$worker/index.tsx | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/frontend/app/src/pages/main/workers/$worker/index.tsx b/frontend/app/src/pages/main/workers/$worker/index.tsx index 8e2dfcab6b..51fbfde88b 100644 --- a/frontend/app/src/pages/main/workers/$worker/index.tsx +++ b/frontend/app/src/pages/main/workers/$worker/index.tsx @@ -19,7 +19,13 @@ import { import RelativeDate from '@/components/molecules/relative-date'; import { useApiError } from '@/lib/hooks'; import queryClient from '@/query-client'; - +import { BiDotsVertical } from 'react-icons/bi'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; export const isHealthy = (worker?: Worker) => { const reasons = []; @@ -126,19 +132,35 @@ export default function ExpandedWorkflowRun() { {worker.name} - +

+ + + + + + + { + updateWorker.mutate({ + isPaused: worker.status === 'PAUSED' ? false : true, + }); + }} + > + {worker.status === 'PAUSED' ? 'Resume' : 'Pause'} Step Run + Assignment + + + +
-

First Connected: {worker.lastListenerEstablished && ( From 780e7ac2c2ccb0877c42fa58807e3d2e5082e4cc Mon Sep 17 00:00:00 2001 From: gabriel ruttner Date: Mon, 1 Jul 2024 13:08:33 -0400 Subject: [PATCH 3/3] fix: comment --- frontend/app/src/pages/main/workers/$worker/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app/src/pages/main/workers/$worker/index.tsx b/frontend/app/src/pages/main/workers/$worker/index.tsx index 51fbfde88b..71c357da84 100644 --- a/frontend/app/src/pages/main/workers/$worker/index.tsx +++ b/frontend/app/src/pages/main/workers/$worker/index.tsx @@ -109,7 +109,6 @@ export default function ExpandedWorkflowRun() { mutationFn: async (data: UpdateWorkerRequest) => (await api.workerUpdate(worker!.metadata.id, data)).data, onSuccess: async () => { - // Handle success logic here await queryClient.invalidateQueries({ queryKey: queries.workers.get(worker!.metadata.id).queryKey, });