diff --git a/addon/flamenco/manager/__init__.py b/addon/flamenco/manager/__init__.py
index ac5c0287..6a975b3e 100644
--- a/addon/flamenco/manager/__init__.py
+++ b/addon/flamenco/manager/__init__.py
@@ -10,7 +10,7 @@
"""
-__version__ = "781f1d93-dirty"
+__version__ = "c875745b-dirty"
# import ApiClient
from flamenco.manager.api_client import ApiClient
diff --git a/addon/flamenco/manager/api_client.py b/addon/flamenco/manager/api_client.py
index 0ae975ca..010385fe 100644
--- a/addon/flamenco/manager/api_client.py
+++ b/addon/flamenco/manager/api_client.py
@@ -76,7 +76,7 @@ class ApiClient(object):
self.default_headers[header_name] = header_value
self.cookie = cookie
# Set default User-Agent.
- self.user_agent = 'Flamenco/781f1d93-dirty (Blender add-on)'
+ self.user_agent = 'Flamenco/c875745b-dirty (Blender add-on)'
def __enter__(self):
return self
diff --git a/addon/flamenco/manager/configuration.py b/addon/flamenco/manager/configuration.py
index 2521393c..a5552b6b 100644
--- a/addon/flamenco/manager/configuration.py
+++ b/addon/flamenco/manager/configuration.py
@@ -404,7 +404,7 @@ conf = flamenco.manager.Configuration(
"OS: {env}\n"\
"Python Version: {pyversion}\n"\
"Version of the API: 1.0.0\n"\
- "SDK Package Version: 781f1d93-dirty".\
+ "SDK Package Version: c875745b-dirty".\
format(env=sys.platform, pyversion=sys.version)
def get_host_settings(self):
diff --git a/addon/flamenco/manager/docs/JobUpdate.md b/addon/flamenco/manager/docs/JobUpdate.md
new file mode 100644
index 00000000..4f7efe54
--- /dev/null
+++ b/addon/flamenco/manager/docs/JobUpdate.md
@@ -0,0 +1,15 @@
+# JobUpdate
+
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**id** | **str** | UUID of the Job |
+**updated** | **datetime** | Timestamp of last update |
+**status** | [**JobStatus**](JobStatus.md) | |
+**previous_status** | [**JobStatus**](JobStatus.md) | | [optional]
+**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/addon/flamenco/manager/model/job_update.py b/addon/flamenco/manager/model/job_update.py
new file mode 100644
index 00000000..0c67df2a
--- /dev/null
+++ b/addon/flamenco/manager/model/job_update.py
@@ -0,0 +1,283 @@
+"""
+ Flamenco manager
+
+ Render Farm manager API # noqa: E501
+
+ The version of the OpenAPI document: 1.0.0
+ Generated by: https://openapi-generator.tech
+"""
+
+
+import re # noqa: F401
+import sys # noqa: F401
+
+from flamenco.manager.model_utils import ( # noqa: F401
+ ApiTypeError,
+ ModelComposed,
+ ModelNormal,
+ ModelSimple,
+ cached_property,
+ change_keys_js_to_python,
+ convert_js_args_to_python_args,
+ date,
+ datetime,
+ file_type,
+ none_type,
+ validate_get_composed_info,
+ OpenApiModel
+)
+from flamenco.manager.exceptions import ApiAttributeError
+
+
+def lazy_import():
+ from flamenco.manager.model.job_status import JobStatus
+ globals()['JobStatus'] = JobStatus
+
+
+class JobUpdate(ModelNormal):
+ """NOTE: This class is auto generated by OpenAPI Generator.
+ Ref: https://openapi-generator.tech
+
+ Do not edit the class manually.
+
+ Attributes:
+ allowed_values (dict): The key is the tuple path to the attribute
+ and the for var_name this is (var_name,). The value is a dict
+ with a capitalized key describing the allowed value and an allowed
+ value. These dicts store the allowed enum values.
+ attribute_map (dict): The key is attribute name
+ and the value is json key in definition.
+ discriminator_value_class_map (dict): A dict to go from the discriminator
+ variable value to the discriminator class name.
+ validations (dict): The key is the tuple path to the attribute
+ and the for var_name this is (var_name,). The value is a dict
+ that stores validations for max_length, min_length, max_items,
+ min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
+ inclusive_minimum, and regex.
+ additional_properties_type (tuple): A tuple of classes accepted
+ as additional properties values.
+ """
+
+ allowed_values = {
+ }
+
+ validations = {
+ }
+
+ @cached_property
+ def additional_properties_type():
+ """
+ This must be a method because a model may have properties that are
+ of type self, this must run after the class is loaded
+ """
+ lazy_import()
+ return (bool, date, datetime, dict, float, int, list, str, none_type,) # noqa: E501
+
+ _nullable = False
+
+ @cached_property
+ def openapi_types():
+ """
+ This must be a method because a model may have properties that are
+ of type self, this must run after the class is loaded
+
+ Returns
+ openapi_types (dict): The key is attribute name
+ and the value is attribute type.
+ """
+ lazy_import()
+ return {
+ 'id': (str,), # noqa: E501
+ 'updated': (datetime,), # noqa: E501
+ 'status': (JobStatus,), # noqa: E501
+ 'previous_status': (JobStatus,), # noqa: E501
+ }
+
+ @cached_property
+ def discriminator():
+ return None
+
+
+ attribute_map = {
+ 'id': 'id', # noqa: E501
+ 'updated': 'updated', # noqa: E501
+ 'status': 'status', # noqa: E501
+ 'previous_status': 'previous_status', # noqa: E501
+ }
+
+ read_only_vars = {
+ }
+
+ _composed_schemas = {}
+
+ @classmethod
+ @convert_js_args_to_python_args
+ def _from_openapi_data(cls, id, updated, status, *args, **kwargs): # noqa: E501
+ """JobUpdate - a model defined in OpenAPI
+
+ Args:
+ id (str): UUID of the Job
+ updated (datetime): Timestamp of last update
+ status (JobStatus):
+
+ Keyword Args:
+ _check_type (bool): if True, values for parameters in openapi_types
+ will be type checked and a TypeError will be
+ raised if the wrong type is input.
+ Defaults to True
+ _path_to_item (tuple/list): This is a list of keys or values to
+ drill down to the model in received_data
+ when deserializing a response
+ _spec_property_naming (bool): True if the variable names in the input data
+ are serialized names, as specified in the OpenAPI document.
+ False if the variable names in the input data
+ are pythonic names, e.g. snake case (default)
+ _configuration (Configuration): the instance to use when
+ deserializing a file_type parameter.
+ If passed, type conversion is attempted
+ If omitted no type conversion is done.
+ _visited_composed_classes (tuple): This stores a tuple of
+ classes that we have traveled through so that
+ if we see that class again we will not use its
+ discriminator again.
+ When traveling through a discriminator, the
+ composed schema that is
+ is traveled through is added to this set.
+ For example if Animal has a discriminator
+ petType and we pass in "Dog", and the class Dog
+ allOf includes Animal, we move through Animal
+ once using the discriminator, and pick Dog.
+ Then in Dog, we will make an instance of the
+ Animal class but this time we won't travel
+ through its discriminator because we passed in
+ _visited_composed_classes = (Animal,)
+ previous_status (JobStatus): [optional] # noqa: E501
+ """
+
+ _check_type = kwargs.pop('_check_type', True)
+ _spec_property_naming = kwargs.pop('_spec_property_naming', False)
+ _path_to_item = kwargs.pop('_path_to_item', ())
+ _configuration = kwargs.pop('_configuration', None)
+ _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+ self = super(OpenApiModel, cls).__new__(cls)
+
+ if args:
+ raise ApiTypeError(
+ "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+ args,
+ self.__class__.__name__,
+ ),
+ path_to_item=_path_to_item,
+ valid_classes=(self.__class__,),
+ )
+
+ self._data_store = {}
+ self._check_type = _check_type
+ self._spec_property_naming = _spec_property_naming
+ self._path_to_item = _path_to_item
+ self._configuration = _configuration
+ self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+ self.id = id
+ self.updated = updated
+ self.status = status
+ for var_name, var_value in kwargs.items():
+ if var_name not in self.attribute_map and \
+ self._configuration is not None and \
+ self._configuration.discard_unknown_keys and \
+ self.additional_properties_type is None:
+ # discard variable.
+ continue
+ setattr(self, var_name, var_value)
+ return self
+
+ required_properties = set([
+ '_data_store',
+ '_check_type',
+ '_spec_property_naming',
+ '_path_to_item',
+ '_configuration',
+ '_visited_composed_classes',
+ ])
+
+ @convert_js_args_to_python_args
+ def __init__(self, id, updated, status, *args, **kwargs): # noqa: E501
+ """JobUpdate - a model defined in OpenAPI
+
+ Args:
+ id (str): UUID of the Job
+ updated (datetime): Timestamp of last update
+ status (JobStatus):
+
+ Keyword Args:
+ _check_type (bool): if True, values for parameters in openapi_types
+ will be type checked and a TypeError will be
+ raised if the wrong type is input.
+ Defaults to True
+ _path_to_item (tuple/list): This is a list of keys or values to
+ drill down to the model in received_data
+ when deserializing a response
+ _spec_property_naming (bool): True if the variable names in the input data
+ are serialized names, as specified in the OpenAPI document.
+ False if the variable names in the input data
+ are pythonic names, e.g. snake case (default)
+ _configuration (Configuration): the instance to use when
+ deserializing a file_type parameter.
+ If passed, type conversion is attempted
+ If omitted no type conversion is done.
+ _visited_composed_classes (tuple): This stores a tuple of
+ classes that we have traveled through so that
+ if we see that class again we will not use its
+ discriminator again.
+ When traveling through a discriminator, the
+ composed schema that is
+ is traveled through is added to this set.
+ For example if Animal has a discriminator
+ petType and we pass in "Dog", and the class Dog
+ allOf includes Animal, we move through Animal
+ once using the discriminator, and pick Dog.
+ Then in Dog, we will make an instance of the
+ Animal class but this time we won't travel
+ through its discriminator because we passed in
+ _visited_composed_classes = (Animal,)
+ previous_status (JobStatus): [optional] # noqa: E501
+ """
+
+ _check_type = kwargs.pop('_check_type', True)
+ _spec_property_naming = kwargs.pop('_spec_property_naming', False)
+ _path_to_item = kwargs.pop('_path_to_item', ())
+ _configuration = kwargs.pop('_configuration', None)
+ _visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
+
+ if args:
+ raise ApiTypeError(
+ "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
+ args,
+ self.__class__.__name__,
+ ),
+ path_to_item=_path_to_item,
+ valid_classes=(self.__class__,),
+ )
+
+ self._data_store = {}
+ self._check_type = _check_type
+ self._spec_property_naming = _spec_property_naming
+ self._path_to_item = _path_to_item
+ self._configuration = _configuration
+ self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
+
+ self.id = id
+ self.updated = updated
+ self.status = status
+ for var_name, var_value in kwargs.items():
+ if var_name not in self.attribute_map and \
+ self._configuration is not None and \
+ self._configuration.discard_unknown_keys and \
+ self.additional_properties_type is None:
+ # discard variable.
+ continue
+ setattr(self, var_name, var_value)
+ if var_name in self.read_only_vars:
+ raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
+ f"class with read only attributes.")
diff --git a/addon/flamenco/manager/models/__init__.py b/addon/flamenco/manager/models/__init__.py
index 6c5a0e30..a77f7a09 100644
--- a/addon/flamenco/manager/models/__init__.py
+++ b/addon/flamenco/manager/models/__init__.py
@@ -23,6 +23,7 @@ from flamenco.manager.model.job_all_of import JobAllOf
from flamenco.manager.model.job_metadata import JobMetadata
from flamenco.manager.model.job_settings import JobSettings
from flamenco.manager.model.job_status import JobStatus
+from flamenco.manager.model.job_update import JobUpdate
from flamenco.manager.model.jobs_query import JobsQuery
from flamenco.manager.model.jobs_query_result import JobsQueryResult
from flamenco.manager.model.manager_configuration import ManagerConfiguration
diff --git a/addon/flamenco/manager_README.md b/addon/flamenco/manager_README.md
index 99f7424c..87930192 100644
--- a/addon/flamenco/manager_README.md
+++ b/addon/flamenco/manager_README.md
@@ -4,7 +4,7 @@ Render Farm manager API
The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.0.0
-- Package version: 781f1d93-dirty
+- Package version: c875745b-dirty
- Build package: org.openapitools.codegen.languages.PythonClientCodegen
For more information, please visit [https://flamenco.io/](https://flamenco.io/)
@@ -101,6 +101,7 @@ Class | Method | HTTP request | Description
- [JobMetadata](flamenco/manager/docs/JobMetadata.md)
- [JobSettings](flamenco/manager/docs/JobSettings.md)
- [JobStatus](flamenco/manager/docs/JobStatus.md)
+ - [JobUpdate](flamenco/manager/docs/JobUpdate.md)
- [JobsQuery](flamenco/manager/docs/JobsQuery.md)
- [JobsQueryResult](flamenco/manager/docs/JobsQueryResult.md)
- [ManagerConfiguration](flamenco/manager/docs/ManagerConfiguration.md)
diff --git a/cmd/flamenco-manager/main.go b/cmd/flamenco-manager/main.go
index 2e55c159..5baa3aec 100644
--- a/cmd/flamenco-manager/main.go
+++ b/cmd/flamenco-manager/main.go
@@ -94,8 +94,8 @@ func main() {
//
// go persist.PeriodicMaintenanceLoop(mainCtx)
- flamenco := buildFlamencoAPI(configService, persist)
webUpdater := webupdates.New()
+ flamenco := buildFlamencoAPI(configService, persist, webUpdater)
e := buildWebService(flamenco, persist, ssdp, webUpdater)
installSignalHandler(mainCtxCancel)
@@ -132,14 +132,14 @@ func main() {
log.Info().Msg("shutdown complete")
}
-func buildFlamencoAPI(configService *config.Service, persist *persistence.DB) api.ServerInterface {
+func buildFlamencoAPI(configService *config.Service, persist *persistence.DB, webUpdater *webupdates.BiDirComms) api.ServerInterface {
timeService := clock.New()
compiler, err := job_compilers.Load(timeService)
if err != nil {
log.Fatal().Err(err).Msg("error loading job compilers")
}
logStorage := task_logs.NewStorage(configService.Get().TaskLogsPath)
- taskStateMachine := task_state_machine.NewStateMachine(persist)
+ taskStateMachine := task_state_machine.NewStateMachine(persist, webUpdater)
shamanServer := shaman.NewServer(configService.Get().Shaman, nil)
flamenco := api_impl.NewFlamenco(compiler, persist, logStorage, configService, taskStateMachine, shamanServer)
return flamenco
diff --git a/internal/manager/api_impl/api_impl.go b/internal/manager/api_impl/api_impl.go
index 8c8fdf04..c36c7b1b 100644
--- a/internal/manager/api_impl/api_impl.go
+++ b/internal/manager/api_impl/api_impl.go
@@ -11,6 +11,7 @@ import (
"git.blender.org/flamenco/internal/manager/job_compilers"
"git.blender.org/flamenco/internal/manager/persistence"
"git.blender.org/flamenco/internal/manager/task_state_machine"
+ "git.blender.org/flamenco/internal/manager/webupdates"
"git.blender.org/flamenco/pkg/api"
"git.blender.org/flamenco/pkg/shaman"
"github.com/labstack/echo/v4"
@@ -66,6 +67,14 @@ type TaskStateMachine interface {
// TaskStateMachine should be a subset of task_state_machine.StateMachine.
var _ TaskStateMachine = (*task_state_machine.StateMachine)(nil)
+type ChangeBroadcaster interface {
+ // BroadcastNewJob sends a 'new job' notification to all SocketIO clients.
+ BroadcastNewJob(jobUpdate api.JobUpdate)
+}
+
+// ChangeBroadcaster should be a subset of webupdates.BiDirComms.
+var _ ChangeBroadcaster = (*webupdates.BiDirComms)(nil)
+
type JobCompiler interface {
ListJobTypes() api.AvailableJobTypes
Compile(ctx context.Context, job api.SubmittedJob) (*job_compilers.AuthoredJob, error)
diff --git a/internal/manager/task_state_machine/mocks/interfaces_mock.gen.go b/internal/manager/task_state_machine/mocks/interfaces_mock.gen.go
index def73e23..7e930865 100644
--- a/internal/manager/task_state_machine/mocks/interfaces_mock.gen.go
+++ b/internal/manager/task_state_machine/mocks/interfaces_mock.gen.go
@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: git.blender.org/flamenco/internal/manager/task_state_machine (interfaces: PersistenceService)
+// Source: git.blender.org/flamenco/internal/manager/task_state_machine (interfaces: PersistenceService,ChangeBroadcaster)
// Package mocks is a generated GoMock package.
package mocks
@@ -122,3 +122,38 @@ func (mr *MockPersistenceServiceMockRecorder) UpdateJobsTaskStatusesConditional(
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateJobsTaskStatusesConditional", reflect.TypeOf((*MockPersistenceService)(nil).UpdateJobsTaskStatusesConditional), arg0, arg1, arg2, arg3, arg4)
}
+
+// MockChangeBroadcaster is a mock of ChangeBroadcaster interface.
+type MockChangeBroadcaster struct {
+ ctrl *gomock.Controller
+ recorder *MockChangeBroadcasterMockRecorder
+}
+
+// MockChangeBroadcasterMockRecorder is the mock recorder for MockChangeBroadcaster.
+type MockChangeBroadcasterMockRecorder struct {
+ mock *MockChangeBroadcaster
+}
+
+// NewMockChangeBroadcaster creates a new mock instance.
+func NewMockChangeBroadcaster(ctrl *gomock.Controller) *MockChangeBroadcaster {
+ mock := &MockChangeBroadcaster{ctrl: ctrl}
+ mock.recorder = &MockChangeBroadcasterMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockChangeBroadcaster) EXPECT() *MockChangeBroadcasterMockRecorder {
+ return m.recorder
+}
+
+// BroadcastJobUpdate mocks base method.
+func (m *MockChangeBroadcaster) BroadcastJobUpdate(arg0 api.JobUpdate) {
+ m.ctrl.T.Helper()
+ m.ctrl.Call(m, "BroadcastJobUpdate", arg0)
+}
+
+// BroadcastJobUpdate indicates an expected call of BroadcastJobUpdate.
+func (mr *MockChangeBroadcasterMockRecorder) BroadcastJobUpdate(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastJobUpdate", reflect.TypeOf((*MockChangeBroadcaster)(nil).BroadcastJobUpdate), arg0)
+}
diff --git a/internal/manager/task_state_machine/task_state_machine.go b/internal/manager/task_state_machine/task_state_machine.go
index 6f655e14..700e1754 100644
--- a/internal/manager/task_state_machine/task_state_machine.go
+++ b/internal/manager/task_state_machine/task_state_machine.go
@@ -10,6 +10,7 @@ import (
"github.com/rs/zerolog/log"
"git.blender.org/flamenco/internal/manager/persistence"
+ "git.blender.org/flamenco/internal/manager/webupdates"
"git.blender.org/flamenco/pkg/api"
)
@@ -19,11 +20,12 @@ const taskFailJobPercentage = 10 // Integer from 0 to 100.
// StateMachine handles task and job status changes.
type StateMachine struct {
- persist PersistenceService
+ persist PersistenceService
+ broadcaster ChangeBroadcaster
}
// Generate mock implementations of these interfaces.
-//go:generate go run github.com/golang/mock/mockgen -destination mocks/interfaces_mock.gen.go -package mocks git.blender.org/flamenco/internal/manager/task_state_machine PersistenceService
+//go:generate go run github.com/golang/mock/mockgen -destination mocks/interfaces_mock.gen.go -package mocks git.blender.org/flamenco/internal/manager/task_state_machine PersistenceService,ChangeBroadcaster
type PersistenceService interface {
SaveTask(ctx context.Context, task *persistence.Task) error
@@ -45,9 +47,18 @@ type PersistenceService interface {
// PersistenceService should be a subset of persistence.DB
var _ PersistenceService = (*persistence.DB)(nil)
-func NewStateMachine(persist PersistenceService) *StateMachine {
+type ChangeBroadcaster interface {
+ // BroadcastJobUpdate sends the job update to clients.
+ BroadcastJobUpdate(jobUpdate api.JobUpdate)
+}
+
+// ChangeBroadcaster should be a subset of webupdates.BiDirComms
+var _ ChangeBroadcaster = (*webupdates.BiDirComms)(nil)
+
+func NewStateMachine(persist PersistenceService, broadcaster ChangeBroadcaster) *StateMachine {
return &StateMachine{
- persist: persist,
+ persist: persist,
+ broadcaster: broadcaster,
}
}
@@ -244,6 +255,15 @@ func (sm *StateMachine) JobStatusChange(ctx context.Context, job *persistence.Jo
if err != nil {
return fmt.Errorf("updating job's tasks after job status change: %w", err)
}
+
+ // Broadcast this change to the SocketIO clients.
+ jobUpdate := api.JobUpdate{
+ Id: job.UUID,
+ Updated: job.UpdatedAt,
+ PreviousStatus: &oldJobStatus,
+ Status: job.Status,
+ }
+ sm.broadcaster.BroadcastJobUpdate(jobUpdate)
}
return nil
diff --git a/internal/manager/task_state_machine/task_state_machine_test.go b/internal/manager/task_state_machine/task_state_machine_test.go
index c38aa620..0fa99788 100644
--- a/internal/manager/task_state_machine/task_state_machine_test.go
+++ b/internal/manager/task_state_machine/task_state_machine_test.go
@@ -18,7 +18,8 @@ import (
)
type StateMachineMocks struct {
- persist *mocks.MockPersistenceService
+ persist *mocks.MockPersistenceService
+ broadcaster *mocks.MockChangeBroadcaster
}
// In the comments below, "T" indicates the performed task status change, and
@@ -32,6 +33,8 @@ func TestTaskStatusChangeQueuedToActive(t *testing.T) {
task := taskWithStatus(api.JobStatusQueued, api.TaskStatusQueued)
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusActive)
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusActive)
+ mocks.expectBroadcastJobChange(task.Job, api.JobStatusQueued, api.JobStatusActive)
+
assert.NoError(t, sm.TaskStatusChange(ctx, task, api.TaskStatusActive))
}
@@ -81,6 +84,8 @@ func TestTaskStatusChangeActiveToCompleted(t *testing.T) {
mocks.expectSaveTaskWithStatus(t, task3, api.TaskStatusCompleted)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, task.Job, api.TaskStatusCompleted).Return(3, 3, nil) // 3 of 3 complete.
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusCompleted)
+ mocks.expectBroadcastJobChange(task.Job, api.JobStatusActive, api.JobStatusCompleted)
+
assert.NoError(t, sm.TaskStatusChange(ctx, task3, api.TaskStatusCompleted))
}
@@ -93,6 +98,8 @@ func TestTaskStatusChangeQueuedToFailed(t *testing.T) {
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusFailed)
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusActive)
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, task.Job, api.TaskStatusFailed).Return(1, 100, nil) // 1 out of 100 failed.
+ mocks.expectBroadcastJobChange(task.Job, api.JobStatusQueued, api.JobStatusActive)
+
assert.NoError(t, sm.TaskStatusChange(ctx, task, api.TaskStatusFailed))
}
@@ -104,6 +111,8 @@ func TestTaskStatusChangeActiveToFailedFailJob(t *testing.T) {
task := taskWithStatus(api.JobStatusActive, api.TaskStatusActive)
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusFailed)
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusFailed)
+ mocks.expectBroadcastJobChange(task.Job, api.JobStatusActive, api.JobStatusFailed)
+
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, task.Job, api.TaskStatusFailed).Return(10, 100, nil) // 10 out of 100 failed.
// Expect failure of the job to trigger cancellation of remaining tasks.
@@ -127,6 +136,8 @@ func TestTaskStatusChangeRequeueOnCompletedJob(t *testing.T) {
task := taskWithStatus(api.JobStatusCompleted, api.TaskStatusCompleted)
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusQueued)
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusRequeued)
+ mocks.expectBroadcastJobChange(task.Job, api.JobStatusCompleted, api.JobStatusRequeued)
+ mocks.expectBroadcastJobChange(task.Job, api.JobStatusRequeued, api.JobStatusQueued)
// Expect queueing of the job to trigger queueing of all its tasks, if those tasks were all completed before.
// 2 out of 3 completed, because one was just queued.
@@ -156,6 +167,8 @@ func TestTaskStatusChangeCancelSingleTask(t *testing.T) {
mocks.expectSaveTaskWithStatus(t, task2, api.TaskStatusCanceled)
mocks.persist.EXPECT().JobHasTasksInStatus(ctx, job, api.TaskStatusCancelRequested).Return(false, nil)
mocks.expectSaveJobWithStatus(t, job, api.JobStatusCanceled)
+ mocks.expectBroadcastJobChange(task.Job, api.JobStatusCancelRequested, api.JobStatusCanceled)
+
assert.NoError(t, sm.TaskStatusChange(ctx, task2, api.TaskStatusCanceled))
}
@@ -196,6 +209,10 @@ func TestJobRequeueWithSomeCompletedTasks(t *testing.T) {
"Queued because job transitioned status from \"active\" to \"requeued\"",
)
mocks.expectSaveJobWithStatus(t, job, api.JobStatusQueued)
+
+ mocks.expectBroadcastJobChange(job, api.JobStatusActive, api.JobStatusRequeued)
+ mocks.expectBroadcastJobChange(job, api.JobStatusRequeued, api.JobStatusQueued)
+
assert.NoError(t, sm.JobStatusChange(ctx, job, api.JobStatusRequeued))
}
@@ -224,14 +241,18 @@ func TestJobRequeueWithAllCompletedTasks(t *testing.T) {
Return(0, 3, nil). // By now all tasks are queued.
After(call3)
+ mocks.expectBroadcastJobChange(job, api.JobStatusCompleted, api.JobStatusRequeued)
+ mocks.expectBroadcastJobChange(job, api.JobStatusRequeued, api.JobStatusQueued)
+
assert.NoError(t, sm.JobStatusChange(ctx, job, api.JobStatusRequeued))
}
func mockedTaskStateMachine(mockCtrl *gomock.Controller) (*StateMachine, *StateMachineMocks) {
mocks := StateMachineMocks{
- persist: mocks.NewMockPersistenceService(mockCtrl),
+ persist: mocks.NewMockPersistenceService(mockCtrl),
+ broadcaster: mocks.NewMockChangeBroadcaster(mockCtrl),
}
- sm := NewStateMachine(mocks.persist)
+ sm := NewStateMachine(mocks.persist, mocks.broadcaster)
return sm, &mocks
}
@@ -261,6 +282,19 @@ func (m *StateMachineMocks) expectSaveJobWithStatus(
})
}
+func (m *StateMachineMocks) expectBroadcastJobChange(
+ job *persistence.Job,
+ fromStatus, toStatus api.JobStatus,
+) *gomock.Call {
+ expectUpdate := api.JobUpdate{
+ Id: job.UUID,
+ Updated: job.UpdatedAt,
+ PreviousStatus: &fromStatus,
+ Status: toStatus,
+ }
+ return m.broadcaster.EXPECT().BroadcastJobUpdate(expectUpdate)
+}
+
/* taskWithStatus() creates a task of a certain status, with a job of a certain status. */
func taskWithStatus(jobStatus api.JobStatus, taskStatus api.TaskStatus) *persistence.Task {
job := persistence.Job{
diff --git a/internal/manager/webupdates/chatrooms.go b/internal/manager/webupdates/chatrooms.go
new file mode 100644
index 00000000..96dec1a5
--- /dev/null
+++ b/internal/manager/webupdates/chatrooms.go
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+package webupdates
+
+type SocketIORoomName string
+
+const (
+ // Predefined SocketIO rooms.
+ SocketIORoomChat SocketIORoomName = "Chat" // For chat messages.
+ SocketIORoomJobs SocketIORoomName = "Jobs" // For job updates.
+)
+
+type SocketIOEventType string
+
+const (
+ // Predefined SocketIO event types.
+ SIOEventChatMessageRcv SocketIOEventType = "/chat" // clients send messages here
+ SIOEventChatMessageSend SocketIOEventType = "/message" // messages are broadcasted here
+ SIOEventJobUpdate SocketIOEventType = "/jobs" // sends api.JobUpdate
+)
diff --git a/internal/manager/webupdates/job_updates.go b/internal/manager/webupdates/job_updates.go
new file mode 100644
index 00000000..297c6488
--- /dev/null
+++ b/internal/manager/webupdates/job_updates.go
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+package webupdates
+
+import (
+ "github.com/rs/zerolog/log"
+
+ "git.blender.org/flamenco/pkg/api"
+)
+
+// BroadcastJobUpdate sends the job update to clients.
+func (b *BiDirComms) BroadcastJobUpdate(jobUpdate api.JobUpdate) {
+ log.Debug().Interface("jobUpdate", jobUpdate).Msg("socketIO: broadcasting job update")
+ b.sockserv.BroadcastTo(string(SocketIORoomJobs), "/jobs", jobUpdate)
+}
+
+// BroadcastNewJob sends a "new job" notification to clients.
+func (b *BiDirComms) BroadcastNewJob(jobUpdate api.JobUpdate) {
+ if jobUpdate.PreviousStatus != nil {
+ log.Warn().Interface("jobUpdate", jobUpdate).Msg("socketIO: new jobs should not have a previous state")
+ jobUpdate.PreviousStatus = nil
+ }
+
+ log.Debug().Interface("jobUpdate", jobUpdate).Msg("socketIO: broadcasting new job")
+ b.sockserv.BroadcastTo(string(SocketIORoomJobs), "/jobs", jobUpdate)
+}
diff --git a/internal/manager/webupdates/webupdates.go b/internal/manager/webupdates/webupdates.go
index f0b494e0..760f8962 100644
--- a/internal/manager/webupdates/webupdates.go
+++ b/internal/manager/webupdates/webupdates.go
@@ -37,7 +37,7 @@ func socketIOServer() *gosocketio.Server {
// socket connection
err = sio.On(gosocketio.OnConnection, func(c *gosocketio.Channel) {
log.Debug().Str("clientID", c.Id()).Msg("socketIO: connected")
- if err := c.Join("Room"); err != nil {
+ if err := c.Join(string(SocketIORoomChat)); err != nil {
log.Warn().Err(err).Str("clientID", c.Id()).Msg("socketIO: unable to make client join broadcast message room")
}
})
@@ -48,7 +48,7 @@ func socketIOServer() *gosocketio.Server {
// socket disconnection
err = sio.On(gosocketio.OnDisconnection, func(c *gosocketio.Channel) {
log.Debug().Str("clientID", c.Id()).Msg("socketIO: disconnected")
- if err := c.Leave("Room"); err != nil {
+ if err := c.Leave(string(SocketIORoomChat)); err != nil {
log.Warn().Err(err).Str("clientID", c.Id()).Msg("socketIO: unable to make client leave broadcast message room")
}
})
@@ -64,12 +64,12 @@ func socketIOServer() *gosocketio.Server {
}
// chat socket
- err = sio.On("/chat", func(c *gosocketio.Channel, message Message) string {
+ err = sio.On(string(SIOEventChatMessageRcv), func(c *gosocketio.Channel, message Message) string {
log.Info().Str("clientID", c.Id()).
Str("text", message.Text).
Str("name", message.Name).
Msg("socketIO: message received")
- c.BroadcastTo("Room", "/message", message.Text)
+ c.BroadcastTo(string(SocketIORoomChat), string(SIOEventChatMessageSend), message)
return "message sent successfully."
})
if err != nil {
diff --git a/pkg/api/flamenco-manager.yaml b/pkg/api/flamenco-manager.yaml
index 15048ea4..74b0dec6 100644
--- a/pkg/api/flamenco-manager.yaml
+++ b/pkg/api/flamenco-manager.yaml
@@ -892,6 +892,25 @@ components:
"status": {$ref: "#/components/schemas/ShamanFileStatus"}
required: [status]
+ # SocketIO API. These types are not used in any HTTP operation defined in
+ # the 'paths' section of this document, so some code generators may choose
+ # to skip these.
+
+ JobUpdate:
+ type: object
+ properties:
+ "id":
+ type: string
+ format: uuid
+ description: UUID of the Job
+ "updated":
+ type: string
+ format: date-time
+ description: Timestamp of last update
+ "status": {$ref: "#/components/schemas/JobStatus"}
+ "previous_status": {$ref: "#/components/schemas/JobStatus"}
+ required: [id, updated, status]
+
securitySchemes:
worker_auth:
description: Username is the worker ID, password is the secret given at worker registration.
diff --git a/pkg/api/generate.go b/pkg/api/generate.go
index 6b7ad8dd..960708ef 100644
--- a/pkg/api/generate.go
+++ b/pkg/api/generate.go
@@ -1,8 +1,7 @@
-//go:generate oapi-codegen -generate types -o openapi_types.gen.go -package api flamenco-manager.yaml
-//go:generate oapi-codegen -generate server -o openapi_server.gen.go -package api flamenco-manager.yaml
-//go:generate oapi-codegen -generate spec -o openapi_spec.gen.go -package api flamenco-manager.yaml
-//go:generate oapi-codegen -generate client -o openapi_client.gen.go -package api flamenco-manager.yaml
-
+// SPDX-License-Identifier: GPL-3.0-or-later
package api
-// SPDX-License-Identifier: GPL-3.0-or-later
+//go:generate oapi-codegen -generate types,skip-prune -o openapi_types.gen.go -package api flamenco-manager.yaml
+//go:generate oapi-codegen -generate server,skip-prune -o openapi_server.gen.go -package api flamenco-manager.yaml
+//go:generate oapi-codegen -generate spec,skip-prune -o openapi_spec.gen.go -package api flamenco-manager.yaml
+//go:generate oapi-codegen -generate client,skip-prune -o openapi_client.gen.go -package api flamenco-manager.yaml
diff --git a/pkg/api/openapi_spec.gen.go b/pkg/api/openapi_spec.gen.go
index 6f538abe..50a22a37 100644
--- a/pkg/api/openapi_spec.gen.go
+++ b/pkg/api/openapi_spec.gen.go
@@ -18,100 +18,101 @@ import (
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "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",
+ "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",
}
// GetSwagger returns the content of the embedded swagger specification file
diff --git a/pkg/api/openapi_types.gen.go b/pkg/api/openapi_types.gen.go
index c9d4390a..0f5227b7 100644
--- a/pkg/api/openapi_types.gen.go
+++ b/pkg/api/openapi_types.gen.go
@@ -225,6 +225,17 @@ type JobSettings struct {
// JobStatus defines model for JobStatus.
type JobStatus string
+// JobUpdate defines model for JobUpdate.
+type JobUpdate struct {
+ // UUID of the Job
+ Id string `json:"id"`
+ PreviousStatus *JobStatus `json:"previous_status,omitempty"`
+ Status JobStatus `json:"status"`
+
+ // Timestamp of last update
+ Updated time.Time `json:"updated"`
+}
+
// JobsQuery defines model for JobsQuery.
type JobsQuery struct {
Limit *int `json:"limit,omitempty"`
diff --git a/web/app/src/App.vue b/web/app/src/App.vue
index 78e3b468..88af3b9f 100644
--- a/web/app/src/App.vue
+++ b/web/app/src/App.vue
@@ -5,46 +5,45 @@
+ />
+
diff --git a/web/app/src/main.js b/web/app/src/main.js
index 80574918..d9a96edd 100644
--- a/web/app/src/main.js
+++ b/web/app/src/main.js
@@ -10,34 +10,27 @@ import {
BButton,
} from "bootstrap-vue";
+import URLs from './urls'
+
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
-let url = new URL(window.location);
-url.port = "8080";
-const flamencoAPIURL = url.href;
-url.protocol = "ws:";
-const websocketURL = url.href;
-console.log("Flamenco API:", flamencoAPIURL);
-console.log("Websocket :", websocketURL);
+// let flamencoManager = require('flamenco-manager');
+// let apiClient = new flamencoManager.ApiClient(URLs.api);
-let flamencoManager = require('flamenco-manager');
-let apiClient = new flamencoManager.ApiClient(flamencoAPIURL);
+// let query = new flamencoManager.JobsQuery();
+// // query.status_in = ["active"];
+// query.metadata = {project: "Heist"};
-let query = new flamencoManager.JobsQuery();
-// query.status_in = ["active"];
-query.metadata = {project: "Heist"};
-
-let JobsApi = new flamencoManager.JobsApi(apiClient);
-JobsApi.queryJobs(query).then(function(data) {
- console.log('API called successfully.');
- console.log(data);
-}, function(error) {
- console.error(error);
-});
+// let JobsApi = new flamencoManager.JobsApi(apiClient);
+// JobsApi.queryJobs(query).then(function(data) {
+// console.log('API called successfully.');
+// console.log(data);
+// }, function(error) {
+// console.error(error);
+// });
Vue.config.productionTip = false
-Vue.config.serverUrl = websocketURL;
Vue.use(FormInputPlugin);
Vue.use(NavbarPlugin);
@@ -47,8 +40,7 @@ Vue.component("b-input-group", BInputGroup);
Vue.component("b-button", BButton);
Vue.use(IconsPlugin);
-var vueApp = new Vue({
- render: h => h(App),
-});
+var vueApp = new Vue(App);
+vueApp.websocketURL = URLs.ws;
vueApp.$mount("#app");
diff --git a/web/app/src/urls.js b/web/app/src/urls.js
new file mode 100644
index 00000000..eb59cc14
--- /dev/null
+++ b/web/app/src/urls.js
@@ -0,0 +1,16 @@
+let url = new URL(window.location);
+url.port = "8080";
+const flamencoAPIURL = url.href;
+
+url.protocol = "ws:";
+const websocketURL = url.href;
+
+const URLs = {
+ api: flamencoAPIURL,
+ ws: websocketURL,
+};
+
+console.log("Flamenco API:", URLs.api);
+console.log("Websocket :", URLs.ws);
+
+export default URLs;
diff --git a/web/manager-api/README.md b/web/manager-api/README.md
index 8d6d9c5c..ac78ee6e 100644
--- a/web/manager-api/README.md
+++ b/web/manager-api/README.md
@@ -152,6 +152,7 @@ Class | Method | HTTP request | Description
- [flamencoManager.Job](docs/Job.md)
- [flamencoManager.JobAllOf](docs/JobAllOf.md)
- [flamencoManager.JobStatus](docs/JobStatus.md)
+ - [flamencoManager.JobUpdate](docs/JobUpdate.md)
- [flamencoManager.JobsQuery](docs/JobsQuery.md)
- [flamencoManager.JobsQueryResult](docs/JobsQueryResult.md)
- [flamencoManager.ManagerConfiguration](docs/ManagerConfiguration.md)
diff --git a/web/manager-api/docs/JobUpdate.md b/web/manager-api/docs/JobUpdate.md
new file mode 100644
index 00000000..e8b0ed29
--- /dev/null
+++ b/web/manager-api/docs/JobUpdate.md
@@ -0,0 +1,12 @@
+# flamencoManager.JobUpdate
+
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**id** | **String** | UUID of the Job |
+**updated** | **Date** | Timestamp of last update |
+**status** | [**JobStatus**](JobStatus.md) | |
+**previous_status** | [**JobStatus**](JobStatus.md) | | [optional]
+
+
diff --git a/web/manager-api/src/ApiClient.js b/web/manager-api/src/ApiClient.js
index 84a07e58..a825aca7 100644
--- a/web/manager-api/src/ApiClient.js
+++ b/web/manager-api/src/ApiClient.js
@@ -55,7 +55,7 @@ class ApiClient {
* @default {}
*/
this.defaultHeaders = {
- 'User-Agent': 'Flamenco/c00cf8b0-dirty / webbrowser'
+ 'User-Agent': 'Flamenco/c875745b-dirty / webbrowser'
};
/**
diff --git a/web/manager-api/src/index.js b/web/manager-api/src/index.js
index f4026665..4ff6bb2b 100644
--- a/web/manager-api/src/index.js
+++ b/web/manager-api/src/index.js
@@ -25,6 +25,7 @@ import FlamencoVersion from './model/FlamencoVersion';
import Job from './model/Job';
import JobAllOf from './model/JobAllOf';
import JobStatus from './model/JobStatus';
+import JobUpdate from './model/JobUpdate';
import JobsQuery from './model/JobsQuery';
import JobsQueryResult from './model/JobsQueryResult';
import ManagerConfiguration from './model/ManagerConfiguration';
@@ -162,6 +163,12 @@ export {
*/
JobStatus,
+ /**
+ * The JobUpdate model constructor.
+ * @property {module:model/JobUpdate}
+ */
+ JobUpdate,
+
/**
* The JobsQuery model constructor.
* @property {module:model/JobsQuery}
diff --git a/web/manager-api/src/model/JobUpdate.js b/web/manager-api/src/model/JobUpdate.js
new file mode 100644
index 00000000..e7eae895
--- /dev/null
+++ b/web/manager-api/src/model/JobUpdate.js
@@ -0,0 +1,104 @@
+/**
+ * Flamenco manager
+ * Render Farm manager API
+ *
+ * The version of the OpenAPI document: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ *
+ */
+
+import ApiClient from '../ApiClient';
+import JobStatus from './JobStatus';
+
+/**
+ * The JobUpdate model module.
+ * @module model/JobUpdate
+ * @version 0.0.0
+ */
+class JobUpdate {
+ /**
+ * Constructs a new JobUpdate
.
+ * @alias module:model/JobUpdate
+ * @param id {String} UUID of the Job
+ * @param updated {Date} Timestamp of last update
+ * @param status {module:model/JobStatus}
+ */
+ constructor(id, updated, status) {
+
+ JobUpdate.initialize(this, id, updated, status);
+ }
+
+ /**
+ * Initializes the fields of this object.
+ * This method is used by the constructors of any subclasses, in order to implement multiple inheritance (mix-ins).
+ * Only for internal use.
+ */
+ static initialize(obj, id, updated, status) {
+ obj['id'] = id;
+ obj['updated'] = updated;
+ obj['status'] = status;
+ }
+
+ /**
+ * Constructs a JobUpdate
from a plain JavaScript object, optionally creating a new instance.
+ * Copies all relevant properties from data
to obj
if supplied or a new instance if not.
+ * @param {Object} data The plain JavaScript object bearing properties of interest.
+ * @param {module:model/JobUpdate} obj Optional instance to populate.
+ * @return {module:model/JobUpdate} The populated JobUpdate
instance.
+ */
+ static constructFromObject(data, obj) {
+ if (data) {
+ obj = obj || new JobUpdate();
+
+ if (data.hasOwnProperty('id')) {
+ obj['id'] = ApiClient.convertToType(data['id'], 'String');
+ }
+ if (data.hasOwnProperty('updated')) {
+ obj['updated'] = ApiClient.convertToType(data['updated'], 'Date');
+ }
+ if (data.hasOwnProperty('status')) {
+ obj['status'] = JobStatus.constructFromObject(data['status']);
+ }
+ if (data.hasOwnProperty('previous_status')) {
+ obj['previous_status'] = JobStatus.constructFromObject(data['previous_status']);
+ }
+ }
+ return obj;
+ }
+
+
+}
+
+/**
+ * UUID of the Job
+ * @member {String} id
+ */
+JobUpdate.prototype['id'] = undefined;
+
+/**
+ * Timestamp of last update
+ * @member {Date} updated
+ */
+JobUpdate.prototype['updated'] = undefined;
+
+/**
+ * @member {module:model/JobStatus} status
+ */
+JobUpdate.prototype['status'] = undefined;
+
+/**
+ * @member {module:model/JobStatus} previous_status
+ */
+JobUpdate.prototype['previous_status'] = undefined;
+
+
+
+
+
+
+export default JobUpdate;
+