diff --git a/internal/manager/api_impl/worker_mgt.go b/internal/manager/api_impl/worker_mgt.go index cdd0b072..c7c9952a 100644 --- a/internal/manager/api_impl/worker_mgt.go +++ b/internal/manager/api_impl/worker_mgt.go @@ -7,6 +7,7 @@ import ( "net/http" "git.blender.org/flamenco/internal/manager/persistence" + "git.blender.org/flamenco/internal/manager/webupdates" "git.blender.org/flamenco/internal/uuid" "git.blender.org/flamenco/pkg/api" "github.com/labstack/echo/v4" @@ -54,6 +55,54 @@ func (f *Flamenco) FetchWorker(e echo.Context, workerUUID string) error { return e.JSON(http.StatusOK, apiWorker) } +func (f *Flamenco) RequestWorkerStatusChange(e echo.Context, workerUUID string) error { + logger := requestLogger(e) + logger = logger.With().Str("worker", workerUUID).Logger() + + if !uuid.IsValid(workerUUID) { + return sendAPIError(e, http.StatusBadRequest, "not a valid UUID") + } + + // Decode the request body. + var change api.WorkerStatusChangeRequest + if err := e.Bind(&change); err != nil { + logger.Warn().Err(err).Msg("bad request received") + return sendAPIError(e, http.StatusBadRequest, "invalid format") + } + + // Fetch the worker. + dbWorker, err := f.persist.FetchWorker(e.Request().Context(), workerUUID) + if errors.Is(err, persistence.ErrWorkerNotFound) { + logger.Debug().Msg("non-existent worker requested") + return sendAPIError(e, http.StatusNotFound, "worker %q not found", workerUUID) + } + if err != nil { + logger.Error().Err(err).Msg("error fetching worker") + return sendAPIError(e, http.StatusInternalServerError, "error fetching worker: %v", err) + } + + // Store the status change. + logger = logger.With(). + Str("status", string(dbWorker.Status)). + Str("requested", string(change.StatusRequested)). + Bool("lazy", change.IsLazy). + Logger() + + logger.Info().Msg("worker status change requested") + dbWorker.StatusRequested = change.StatusRequested + dbWorker.LazyStatusRequest = change.IsLazy + if err := f.persist.SaveWorker(e.Request().Context(), dbWorker); err != nil { + logger.Error().Err(err).Msg("error saving worker after status change request") + return sendAPIError(e, http.StatusInternalServerError, "error saving worker: %v", err) + } + + // Broadcast the change. + update := webupdates.NewWorkerUpdate(dbWorker) + f.broadcaster.BroadcastWorkerUpdate(update) + + return e.NoContent(http.StatusNoContent) +} + func workerSummary(w persistence.Worker) api.WorkerSummary { summary := api.WorkerSummary{ Id: w.UUID, @@ -63,6 +112,7 @@ func workerSummary(w persistence.Worker) api.WorkerSummary { } if w.StatusRequested != "" { summary.StatusRequested = &w.StatusRequested + summary.LazyStatusRequest = &w.LazyStatusRequest } return summary } @@ -82,6 +132,7 @@ func workerDBtoAPI(dbWorker *persistence.Worker) api.Worker { if dbWorker.StatusRequested != "" { apiWorker.StatusRequested = &dbWorker.StatusRequested + apiWorker.LazyStatusRequest = &dbWorker.LazyStatusRequest } return apiWorker diff --git a/internal/manager/api_impl/worker_mgt_test.go b/internal/manager/api_impl/worker_mgt_test.go index a9bb3004..56e7c901 100644 --- a/internal/manager/api_impl/worker_mgt_test.go +++ b/internal/manager/api_impl/worker_mgt_test.go @@ -121,3 +121,41 @@ func TestFetchWorker(t *testing.T) { SupportedTaskTypes: []string{"blender", "ffmpeg", "file-management", "misc"}, }) } + +func TestRequestWorkerStatusChange(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mf := newMockedFlamenco(mockCtrl) + worker := testWorker() + workerUUID := worker.UUID + prevStatus := worker.Status + + mf.persistence.EXPECT().FetchWorker(gomock.Any(), workerUUID).Return(&worker, nil) + + requestStatus := api.WorkerStatusAsleep + savedWorker := worker + savedWorker.StatusRequested = requestStatus + savedWorker.LazyStatusRequest = true + mf.persistence.EXPECT().SaveWorker(gomock.Any(), &savedWorker).Return(nil) + + // Expect a broadcast of the change + mf.broadcaster.EXPECT().BroadcastWorkerUpdate(api.SocketIOWorkerUpdate{ + Id: worker.UUID, + Nickname: worker.Name, + PreviousStatus: &prevStatus, + Status: prevStatus, + StatusRequested: &requestStatus, + LazyStatusRequest: ptr(true), + Updated: worker.UpdatedAt, + Version: worker.Software, + }) + + echo := mf.prepareMockedJSONRequest(api.WorkerStatusChangeRequest{ + StatusRequested: requestStatus, + IsLazy: true, + }) + err := mf.flamenco.RequestWorkerStatusChange(echo, workerUUID) + assert.NoError(t, err) + assertResponseEmpty(t, echo) +} diff --git a/internal/manager/persistence/workers.go b/internal/manager/persistence/workers.go index c04b6938..32d1cae2 100644 --- a/internal/manager/persistence/workers.go +++ b/internal/manager/persistence/workers.go @@ -18,11 +18,13 @@ type Worker struct { Secret string `gorm:"type:varchar(255);default:''"` Name string `gorm:"type:varchar(64);default:''"` - Address string `gorm:"type:varchar(39);default:'';index"` // 39 = max length of IPv6 address. - Platform string `gorm:"type:varchar(16);default:''"` - Software string `gorm:"type:varchar(32);default:''"` - Status api.WorkerStatus `gorm:"type:varchar(16);default:''"` - StatusRequested api.WorkerStatus `gorm:"type:varchar(16);default:''"` + Address string `gorm:"type:varchar(39);default:'';index"` // 39 = max length of IPv6 address. + Platform string `gorm:"type:varchar(16);default:''"` + Software string `gorm:"type:varchar(32);default:''"` + Status api.WorkerStatus `gorm:"type:varchar(16);default:''"` + + StatusRequested api.WorkerStatus `gorm:"type:varchar(16);default:''"` + LazyStatusRequest bool `gorm:"type:smallint;default:0"` SupportedTaskTypes string `gorm:"type:varchar(255);default:''"` // comma-separated list of task types. } diff --git a/internal/manager/webupdates/worker_updates.go b/internal/manager/webupdates/worker_updates.go index 03928e88..85f10b7c 100644 --- a/internal/manager/webupdates/worker_updates.go +++ b/internal/manager/webupdates/worker_updates.go @@ -20,6 +20,12 @@ func NewWorkerUpdate(worker *persistence.Worker) api.SocketIOWorkerUpdate { Version: worker.Software, Updated: worker.UpdatedAt, } + + if worker.StatusRequested != "" { + workerUpdate.StatusRequested = &worker.StatusRequested + workerUpdate.LazyStatusRequest = &worker.LazyStatusRequest + } + return workerUpdate } diff --git a/web/app/src/assets/base.css b/web/app/src/assets/base.css index 5d0c4c50..eb17f2e2 100644 --- a/web/app/src/assets/base.css +++ b/web/app/src/assets/base.css @@ -465,4 +465,8 @@ section.footer-popup .tabulator .tabulator-tableholder .tabulator-placeholder .t span.state-transition-arrow { font-weight: bold; font-size: 110%; + color: var(--color-accent); +} +span.state-transition-arrow.lazy { + color: var(--color-text-muted); } diff --git a/web/app/src/components/workers/WorkerDetails.vue b/web/app/src/components/workers/WorkerDetails.vue index 27dea0ad..f56c8b36 100644 --- a/web/app/src/components/workers/WorkerDetails.vue +++ b/web/app/src/components/workers/WorkerDetails.vue @@ -10,11 +10,7 @@
{{ workerData.nickname }}
Status
-
{{ workerData.status }} - -
+
Version
{{ workerData.version }}
@@ -39,6 +35,7 @@ import * as datetime from "@/datetime"; import { WorkerMgtApi } from '@/manager-api'; import { apiClient } from '@/stores/api-query-count'; +import { workerStatus } from "../../statusindicator"; export default { props: [ @@ -48,12 +45,19 @@ export default { return { datetime: datetime, // So that the template can access it. api: new WorkerMgtApi(apiClient), + workerStatusHTML: "", }; }, mounted() { // Allow testing from the JS console: window.workerDetailsVue = this; }, + watch: { + workerData(newData) { + this.workerStatusHTML = workerStatus(newData); + console.log("new worker data; status=", this.workerStatusHTML); + }, + }, computed: { hasWorkerData() { return !!this.workerData && !!this.workerData.id; diff --git a/web/app/src/components/workers/WorkersTable.vue b/web/app/src/components/workers/WorkersTable.vue index d010aee3..d21376ec 100644 --- a/web/app/src/components/workers/WorkersTable.vue +++ b/web/app/src/components/workers/WorkersTable.vue @@ -14,7 +14,7 @@