diff --git a/addon/flamenco/manager/api/worker_api.py b/addon/flamenco/manager/api/worker_api.py
index e81067c9..df717f6a 100644
--- a/addon/flamenco/manager/api/worker_api.py
+++ b/addon/flamenco/manager/api/worker_api.py
@@ -26,6 +26,7 @@ from flamenco.manager.model.error import Error
from flamenco.manager.model.may_keep_running import MayKeepRunning
from flamenco.manager.model.registered_worker import RegisteredWorker
from flamenco.manager.model.security_error import SecurityError
+from flamenco.manager.model.task_progress_update import TaskProgressUpdate
from flamenco.manager.model.task_update import TaskUpdate
from flamenco.manager.model.worker_registration import WorkerRegistration
from flamenco.manager.model.worker_sign_on import WorkerSignOn
@@ -344,6 +345,64 @@ class WorkerApi(object):
},
api_client=api_client
)
+ self.task_progress_update_endpoint = _Endpoint(
+ settings={
+ 'response_type': None,
+ 'auth': [
+ 'worker_auth'
+ ],
+ 'endpoint_path': '/api/v3/worker/task/{task_id}/progress',
+ 'operation_id': 'task_progress_update',
+ 'http_method': 'POST',
+ 'servers': None,
+ },
+ params_map={
+ 'all': [
+ 'task_id',
+ 'task_progress_update',
+ ],
+ 'required': [
+ 'task_id',
+ 'task_progress_update',
+ ],
+ 'nullable': [
+ ],
+ 'enum': [
+ ],
+ 'validation': [
+ ]
+ },
+ root_map={
+ 'validations': {
+ },
+ 'allowed_values': {
+ },
+ 'openapi_types': {
+ 'task_id':
+ (str,),
+ 'task_progress_update':
+ (TaskProgressUpdate,),
+ },
+ 'attribute_map': {
+ 'task_id': 'task_id',
+ },
+ 'location_map': {
+ 'task_id': 'path',
+ 'task_progress_update': 'body',
+ },
+ 'collection_format_map': {
+ }
+ },
+ headers_map={
+ 'accept': [
+ 'application/json'
+ ],
+ 'content_type': [
+ 'application/json'
+ ]
+ },
+ api_client=api_client
+ )
self.task_update_endpoint = _Endpoint(
settings={
'response_type': None,
@@ -955,6 +1014,87 @@ class WorkerApi(object):
body
return self.task_output_produced_endpoint.call_with_http_info(**kwargs)
+ def task_progress_update(
+ self,
+ task_id,
+ task_progress_update,
+ **kwargs
+ ):
+ """Update the progress of the task. # noqa: E501
+
+ This method makes a synchronous HTTP request by default. To make an
+ asynchronous HTTP request, please pass async_req=True
+
+ >>> thread = api.task_progress_update(task_id, task_progress_update, async_req=True)
+ >>> result = thread.get()
+
+ Args:
+ task_id (str):
+ task_progress_update (TaskProgressUpdate): Task progress information
+
+ Keyword Args:
+ _return_http_data_only (bool): response data without head status
+ code and headers. Default is True.
+ _preload_content (bool): if False, the urllib3.HTTPResponse object
+ will be returned without reading/decoding response data.
+ Default is True.
+ _request_timeout (int/float/tuple): timeout setting for this request. If
+ one number provided, it will be total request timeout. It can also
+ be a pair (tuple) of (connection, read) timeouts.
+ Default is None.
+ _check_input_type (bool): specifies if type checking
+ should be done one the data sent to the server.
+ Default is True.
+ _check_return_type (bool): specifies if type checking
+ should be done one the data received from the server.
+ Default is True.
+ _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)
+ _content_type (str/None): force body content-type.
+ Default is None and content-type will be predicted by allowed
+ content-types and body.
+ _host_index (int/None): specifies the index of the server
+ that we want to use.
+ Default is read from the configuration.
+ async_req (bool): execute request asynchronously
+
+ Returns:
+ None
+ If the method is called asynchronously, returns the request
+ thread.
+ """
+ kwargs['async_req'] = kwargs.get(
+ 'async_req', False
+ )
+ kwargs['_return_http_data_only'] = kwargs.get(
+ '_return_http_data_only', True
+ )
+ kwargs['_preload_content'] = kwargs.get(
+ '_preload_content', True
+ )
+ kwargs['_request_timeout'] = kwargs.get(
+ '_request_timeout', None
+ )
+ kwargs['_check_input_type'] = kwargs.get(
+ '_check_input_type', True
+ )
+ kwargs['_check_return_type'] = kwargs.get(
+ '_check_return_type', True
+ )
+ kwargs['_spec_property_naming'] = kwargs.get(
+ '_spec_property_naming', False
+ )
+ kwargs['_content_type'] = kwargs.get(
+ '_content_type')
+ kwargs['_host_index'] = kwargs.get('_host_index')
+ kwargs['task_id'] = \
+ task_id
+ kwargs['task_progress_update'] = \
+ task_progress_update
+ return self.task_progress_update_endpoint.call_with_http_info(**kwargs)
+
def task_update(
self,
task_id,
diff --git a/addon/flamenco/manager/api/worker_mgt_api.py b/addon/flamenco/manager/api/worker_mgt_api.py
index 52beace5..bea95e86 100644
--- a/addon/flamenco/manager/api/worker_mgt_api.py
+++ b/addon/flamenco/manager/api/worker_mgt_api.py
@@ -44,7 +44,7 @@ class WorkerMgtApi(object):
self.api_client = api_client
self.create_worker_cluster_endpoint = _Endpoint(
settings={
- 'response_type': None,
+ 'response_type': (WorkerCluster,),
'auth': [],
'endpoint_path': '/api/v3/worker-mgt/clusters',
'operation_id': 'create_worker_cluster',
@@ -691,7 +691,7 @@ class WorkerMgtApi(object):
async_req (bool): execute request asynchronously
Returns:
- None
+ WorkerCluster
If the method is called asynchronously, returns the request
thread.
"""
diff --git a/addon/flamenco/manager/docs/SocketIOTaskProgressUpdate.md b/addon/flamenco/manager/docs/SocketIOTaskProgressUpdate.md
new file mode 100644
index 00000000..b267492d
--- /dev/null
+++ b/addon/flamenco/manager/docs/SocketIOTaskProgressUpdate.md
@@ -0,0 +1,15 @@
+# SocketIOTaskProgressUpdate
+
+Task progress, sent to a SocketIO room when progress of a task changes.
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**id** | **str** | UUID of the Task |
+**job_id** | **str** | |
+**progress** | **int** | Indicates the percentage of the task that's been completed. |
+**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/docs/TaskProgressUpdate.md b/addon/flamenco/manager/docs/TaskProgressUpdate.md
new file mode 100644
index 00000000..0c461753
--- /dev/null
+++ b/addon/flamenco/manager/docs/TaskProgressUpdate.md
@@ -0,0 +1,13 @@
+# TaskProgressUpdate
+
+TaskProgressUpdate is sent by a Worker to update the progress of a task it's executing.
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**progress** | **int** | Indicates the percentage of the task that's been completed. |
+**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/docs/WorkerApi.md b/addon/flamenco/manager/docs/WorkerApi.md
index 0fbb0caf..440911d4 100644
--- a/addon/flamenco/manager/docs/WorkerApi.md
+++ b/addon/flamenco/manager/docs/WorkerApi.md
@@ -10,6 +10,7 @@ Method | HTTP request | Description
[**sign_off**](WorkerApi.md#sign_off) | **POST** /api/v3/worker/sign-off | Mark the worker as offline
[**sign_on**](WorkerApi.md#sign_on) | **POST** /api/v3/worker/sign-on | Authenticate & sign in the worker.
[**task_output_produced**](WorkerApi.md#task_output_produced) | **POST** /api/v3/worker/task/{task_id}/output-produced | Store the most recently rendered frame here. Note that it is up to the Worker to ensure this is in a format that's digestable by the Manager. Currently only PNG and JPEG support is planned.
+[**task_progress_update**](WorkerApi.md#task_progress_update) | **POST** /api/v3/worker/task/{task_id}/progress | Update the progress of the task.
[**task_update**](WorkerApi.md#task_update) | **POST** /api/v3/worker/task/{task_id} | Update the task, typically to indicate progress, completion, or failure.
[**worker_state**](WorkerApi.md#worker_state) | **GET** /api/v3/worker/state |
[**worker_state_changed**](WorkerApi.md#worker_state_changed) | **POST** /api/v3/worker/state-changed | Worker changed state. This could be as acknowledgement of a Manager-requested state change, or in response to worker-local signals.
@@ -485,6 +486,88 @@ void (empty response body)
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+# **task_progress_update**
+> task_progress_update(task_id, task_progress_update)
+
+Update the progress of the task.
+
+### Example
+
+* Basic Authentication (worker_auth):
+
+```python
+import time
+import flamenco.manager
+from flamenco.manager.api import worker_api
+from flamenco.manager.model.error import Error
+from flamenco.manager.model.task_progress_update import TaskProgressUpdate
+from pprint import pprint
+# Defining the host is optional and defaults to http://localhost
+# See configuration.py for a list of all supported configuration parameters.
+configuration = flamenco.manager.Configuration(
+ host = "http://localhost"
+)
+
+# The client must configure the authentication and authorization parameters
+# in accordance with the API server security policy.
+# Examples for each auth method are provided below, use the example that
+# satisfies your auth use case.
+
+# Configure HTTP basic authorization: worker_auth
+configuration = flamenco.manager.Configuration(
+ username = 'YOUR_USERNAME',
+ password = 'YOUR_PASSWORD'
+)
+
+# Enter a context with an instance of the API client
+with flamenco.manager.ApiClient(configuration) as api_client:
+ # Create an instance of the API class
+ api_instance = worker_api.WorkerApi(api_client)
+ task_id = "task_id_example" # str |
+ task_progress_update = TaskProgressUpdate(
+ progress=0,
+ ) # TaskProgressUpdate | Task progress information
+
+ # example passing only required values which don't have defaults set
+ try:
+ # Update the progress of the task.
+ api_instance.task_progress_update(task_id, task_progress_update)
+ except flamenco.manager.ApiException as e:
+ print("Exception when calling WorkerApi->task_progress_update: %s\n" % e)
+```
+
+
+### Parameters
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+ **task_id** | **str**| |
+ **task_progress_update** | [**TaskProgressUpdate**](TaskProgressUpdate.md)| Task progress information |
+
+### Return type
+
+void (empty response body)
+
+### Authorization
+
+[worker_auth](../README.md#worker_auth)
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **Accept**: application/json
+
+
+### HTTP response details
+
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**204** | The progress update was accepted | - |
+**409** | The task is assigned to another worker, so the progress update was not accepted. | - |
+**0** | unexpected error | - |
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
# **task_update**
> task_update(task_id, task_update)
diff --git a/addon/flamenco/manager/docs/WorkerMgtApi.md b/addon/flamenco/manager/docs/WorkerMgtApi.md
index d108ca47..a6111337 100644
--- a/addon/flamenco/manager/docs/WorkerMgtApi.md
+++ b/addon/flamenco/manager/docs/WorkerMgtApi.md
@@ -19,7 +19,7 @@ Method | HTTP request | Description
# **create_worker_cluster**
-> create_worker_cluster(worker_cluster)
+> WorkerCluster create_worker_cluster(worker_cluster)
Create a new worker cluster.
@@ -53,7 +53,8 @@ with flamenco.manager.ApiClient() as api_client:
# example passing only required values which don't have defaults set
try:
# Create a new worker cluster.
- api_instance.create_worker_cluster(worker_cluster)
+ api_response = api_instance.create_worker_cluster(worker_cluster)
+ pprint(api_response)
except flamenco.manager.ApiException as e:
print("Exception when calling WorkerMgtApi->create_worker_cluster: %s\n" % e)
```
@@ -67,7 +68,7 @@ Name | Type | Description | Notes
### Return type
-void (empty response body)
+[**WorkerCluster**](WorkerCluster.md)
### Authorization
@@ -83,7 +84,7 @@ No authorization required
| Status code | Description | Response headers |
|-------------|-------------|------------------|
-**204** | The cluster was created. | - |
+**200** | The cluster was created. The created cluster is returned, so that the caller can know its UUID. | - |
**0** | Error message | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
diff --git a/addon/flamenco/manager/model/socket_io_task_progress_update.py b/addon/flamenco/manager/model/socket_io_task_progress_update.py
new file mode 100644
index 00000000..6f98caf1
--- /dev/null
+++ b/addon/flamenco/manager/model/socket_io_task_progress_update.py
@@ -0,0 +1,277 @@
+"""
+ 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
+
+
+
+class SocketIOTaskProgressUpdate(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 = {
+ ('progress',): {
+ 'inclusive_maximum': 100,
+ 'inclusive_minimum': 0,
+ },
+ }
+
+ @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
+ """
+ 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.
+ """
+ return {
+ 'id': (str,), # noqa: E501
+ 'job_id': (str,), # noqa: E501
+ 'progress': (int,), # noqa: E501
+ }
+
+ @cached_property
+ def discriminator():
+ return None
+
+
+ attribute_map = {
+ 'id': 'id', # noqa: E501
+ 'job_id': 'job_id', # noqa: E501
+ 'progress': 'progress', # noqa: E501
+ }
+
+ read_only_vars = {
+ }
+
+ _composed_schemas = {}
+
+ @classmethod
+ @convert_js_args_to_python_args
+ def _from_openapi_data(cls, id, job_id, progress, *args, **kwargs): # noqa: E501
+ """SocketIOTaskProgressUpdate - a model defined in OpenAPI
+
+ Args:
+ id (str): UUID of the Task
+ job_id (str):
+ progress (int): Indicates the percentage of the task that's been completed.
+
+ 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,)
+ """
+
+ _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.job_id = job_id
+ self.progress = progress
+ 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, job_id, progress, *args, **kwargs): # noqa: E501
+ """SocketIOTaskProgressUpdate - a model defined in OpenAPI
+
+ Args:
+ id (str): UUID of the Task
+ job_id (str):
+ progress (int): Indicates the percentage of the task that's been completed.
+
+ 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,)
+ """
+
+ _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.job_id = job_id
+ self.progress = progress
+ 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/model/task_progress_update.py b/addon/flamenco/manager/model/task_progress_update.py
new file mode 100644
index 00000000..494cc977
--- /dev/null
+++ b/addon/flamenco/manager/model/task_progress_update.py
@@ -0,0 +1,265 @@
+"""
+ 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
+
+
+
+class TaskProgressUpdate(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 = {
+ ('progress',): {
+ 'inclusive_maximum': 100,
+ 'inclusive_minimum': 0,
+ },
+ }
+
+ @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
+ """
+ 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.
+ """
+ return {
+ 'progress': (int,), # noqa: E501
+ }
+
+ @cached_property
+ def discriminator():
+ return None
+
+
+ attribute_map = {
+ 'progress': 'progress', # noqa: E501
+ }
+
+ read_only_vars = {
+ }
+
+ _composed_schemas = {}
+
+ @classmethod
+ @convert_js_args_to_python_args
+ def _from_openapi_data(cls, progress, *args, **kwargs): # noqa: E501
+ """TaskProgressUpdate - a model defined in OpenAPI
+
+ Args:
+ progress (int): Indicates the percentage of the task that's been completed.
+
+ 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,)
+ """
+
+ _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.progress = progress
+ 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, progress, *args, **kwargs): # noqa: E501
+ """TaskProgressUpdate - a model defined in OpenAPI
+
+ Args:
+ progress (int): Indicates the percentage of the task that's been completed.
+
+ 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,)
+ """
+
+ _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.progress = progress
+ 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 e4fe3a14..cdaf080a 100644
--- a/addon/flamenco/manager/models/__init__.py
+++ b/addon/flamenco/manager/models/__init__.py
@@ -62,11 +62,13 @@ from flamenco.manager.model.socket_io_subscription import SocketIOSubscription
from flamenco.manager.model.socket_io_subscription_operation import SocketIOSubscriptionOperation
from flamenco.manager.model.socket_io_subscription_type import SocketIOSubscriptionType
from flamenco.manager.model.socket_io_task_log_update import SocketIOTaskLogUpdate
+from flamenco.manager.model.socket_io_task_progress_update import SocketIOTaskProgressUpdate
from flamenco.manager.model.socket_io_task_update import SocketIOTaskUpdate
from flamenco.manager.model.socket_io_worker_update import SocketIOWorkerUpdate
from flamenco.manager.model.submitted_job import SubmittedJob
from flamenco.manager.model.task import Task
from flamenco.manager.model.task_log_info import TaskLogInfo
+from flamenco.manager.model.task_progress_update import TaskProgressUpdate
from flamenco.manager.model.task_status import TaskStatus
from flamenco.manager.model.task_status_change import TaskStatusChange
from flamenco.manager.model.task_summary import TaskSummary
diff --git a/addon/flamenco/manager_README.md b/addon/flamenco/manager_README.md
index b8086d84..d7fd7920 100644
--- a/addon/flamenco/manager_README.md
+++ b/addon/flamenco/manager_README.md
@@ -113,6 +113,7 @@ Class | Method | HTTP request | Description
*WorkerApi* | [**sign_off**](flamenco/manager/docs/WorkerApi.md#sign_off) | **POST** /api/v3/worker/sign-off | Mark the worker as offline
*WorkerApi* | [**sign_on**](flamenco/manager/docs/WorkerApi.md#sign_on) | **POST** /api/v3/worker/sign-on | Authenticate & sign in the worker.
*WorkerApi* | [**task_output_produced**](flamenco/manager/docs/WorkerApi.md#task_output_produced) | **POST** /api/v3/worker/task/{task_id}/output-produced | Store the most recently rendered frame here. Note that it is up to the Worker to ensure this is in a format that's digestable by the Manager. Currently only PNG and JPEG support is planned.
+*WorkerApi* | [**task_progress_update**](flamenco/manager/docs/WorkerApi.md#task_progress_update) | **POST** /api/v3/worker/task/{task_id}/progress | Update the progress of the task.
*WorkerApi* | [**task_update**](flamenco/manager/docs/WorkerApi.md#task_update) | **POST** /api/v3/worker/task/{task_id} | Update the task, typically to indicate progress, completion, or failure.
*WorkerApi* | [**worker_state**](flamenco/manager/docs/WorkerApi.md#worker_state) | **GET** /api/v3/worker/state |
*WorkerApi* | [**worker_state_changed**](flamenco/manager/docs/WorkerApi.md#worker_state_changed) | **POST** /api/v3/worker/state-changed | Worker changed state. This could be as acknowledgement of a Manager-requested state change, or in response to worker-local signals.
@@ -185,11 +186,13 @@ Class | Method | HTTP request | Description
- [SocketIOSubscriptionOperation](flamenco/manager/docs/SocketIOSubscriptionOperation.md)
- [SocketIOSubscriptionType](flamenco/manager/docs/SocketIOSubscriptionType.md)
- [SocketIOTaskLogUpdate](flamenco/manager/docs/SocketIOTaskLogUpdate.md)
+ - [SocketIOTaskProgressUpdate](flamenco/manager/docs/SocketIOTaskProgressUpdate.md)
- [SocketIOTaskUpdate](flamenco/manager/docs/SocketIOTaskUpdate.md)
- [SocketIOWorkerUpdate](flamenco/manager/docs/SocketIOWorkerUpdate.md)
- [SubmittedJob](flamenco/manager/docs/SubmittedJob.md)
- [Task](flamenco/manager/docs/Task.md)
- [TaskLogInfo](flamenco/manager/docs/TaskLogInfo.md)
+ - [TaskProgressUpdate](flamenco/manager/docs/TaskProgressUpdate.md)
- [TaskStatus](flamenco/manager/docs/TaskStatus.md)
- [TaskStatusChange](flamenco/manager/docs/TaskStatusChange.md)
- [TaskSummary](flamenco/manager/docs/TaskSummary.md)
diff --git a/internal/manager/api_impl/workers.go b/internal/manager/api_impl/workers.go
index 99dbe7a4..2deb18dc 100644
--- a/internal/manager/api_impl/workers.go
+++ b/internal/manager/api_impl/workers.go
@@ -373,6 +373,58 @@ func (f *Flamenco) ScheduleTask(e echo.Context) error {
return e.JSON(http.StatusOK, customisedTask)
}
+// TODO: 1) Broadcast the udpates to the frontend client
+// TODO: 2) Write tests for the following function
+func (f *Flamenco) TaskProgressUpdate(e echo.Context, taskID string) error {
+ logger := requestLogger(e)
+ worker := requestWorkerOrPanic(e)
+
+ if !uuid.IsValid(taskID) {
+ logger.Debug().Msg("Invalid task ID received")
+ return sendAPIError(e, http.StatusBadRequest, "Task ID not valid")
+ }
+
+ logger = logger.With().Str("taskID", taskID).Logger()
+
+ // Fetch the task, to see if this worker is even allowed to send udpates.
+ ctx := e.Request().Context()
+ dbTask, err := f.persist.FetchTask(ctx, taskID)
+ if err != nil {
+ logger.Warn().Err(err).Msg("cannot fetch task")
+ if errors.Is(err, persistence.ErrTaskNotFound) {
+ return sendAPIError(e, http.StatusNotFound, "task %+v not found", taskID)
+ }
+ return sendAPIError(e, http.StatusInternalServerError, "error fetching task")
+ }
+
+ if dbTask == nil {
+ panic("task could not be fetched, but database gave no error either")
+ }
+
+ // Decode the request body.
+ var taskProgressUpdate api.TaskProgressUpdate
+ if err := e.Bind(&taskProgressUpdate); err != nil {
+ logger.Warn().Err(err).Msg("bad request received")
+ return sendAPIError(e, http.StatusBadRequest, "invalid format")
+ }
+ if dbTask.WorkerID == nil {
+ logger.Warn().Msg("worker trying to update task that's not assigned to any worker")
+ return sendAPIError(e, http.StatusConflict, "task %+v is not assigned to any worker, so also not to you", taskID)
+ }
+ if *dbTask.WorkerID != worker.ID {
+ logger.Warn().Msg("worker trying to update task that's assigned to another worker")
+ return sendAPIError(e, http.StatusConflict, "task %+v is not assigned to you", taskID)
+ }
+ // for testing ..............................
+
+ fmt.Println("----------------------------")
+ fmt.Print("Progress: ")
+ fmt.Println(taskProgressUpdate.Progress)
+ fmt.Println("----------------------------")
+
+ //...........................................
+ return e.NoContent(http.StatusNoContent)
+}
func (f *Flamenco) TaskOutputProduced(e echo.Context, taskID string) error {
ctx := e.Request().Context()
filesize := e.Request().ContentLength
diff --git a/internal/manager/job_compilers/js_globals.go b/internal/manager/job_compilers/js_globals.go
index de9dddf9..ff3f1950 100644
--- a/internal/manager/job_compilers/js_globals.go
+++ b/internal/manager/job_compilers/js_globals.go
@@ -111,6 +111,11 @@ func jsFrameChunker(frameRange string, chunkSize int) ([]string, error) {
return chunks, nil
}
+func jsCountChunkSize(chunk string) (int, error) {
+ frames, err := frameRangeExplode(chunk)
+ return len(frames), err
+}
+
// Given a range of frames, return an array containing each frame number.
func frameRangeExplode(frameRange string) ([]int, error) {
// Store as map to avoid duplicate frames.
diff --git a/internal/manager/job_compilers/js_globals_test.go b/internal/manager/job_compilers/js_globals_test.go
index 9ce7eb06..f6103935 100644
--- a/internal/manager/job_compilers/js_globals_test.go
+++ b/internal/manager/job_compilers/js_globals_test.go
@@ -56,3 +56,38 @@ func TestFrameRangeExplode(t *testing.T) {
20, 21, 22, 23, 24, 25, 40,
}, frames)
}
+
+func TestJSCountChunkSize(t *testing.T) {
+ tests := []struct {
+ name string
+ chunk string
+ want int
+ wantErr bool
+ }{
+ // Bad cases.
+ {"empty", "", 0, true},
+ {"negative", "-5", 0, true},
+ {"space", " ", 0, true},
+ {"no-comma", "5 10", 0, true},
+ {"no-numbers", "start-end", 0, true},
+ // Good cases.
+ {"single", "4", 1, false},
+ {"multiple", "4,5,10", 3, false},
+ {"onerange", "1-4", 4, false},
+ {"tworange", "1-4,10-12", 7, false},
+ {"overlap", "1-6,3-12", 12, false},
+ {"space-around", " 1-5\t", 5, false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := jsCountChunkSize(tt.chunk)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("jsCountChunkSize() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("jsCountChunkSize() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/internal/manager/job_compilers/scripts.go b/internal/manager/job_compilers/scripts.go
index efba10e9..fec29d44 100644
--- a/internal/manager/job_compilers/scripts.go
+++ b/internal/manager/job_compilers/scripts.go
@@ -140,6 +140,7 @@ func newGojaVM(registry *require.Registry) *goja.Runtime {
mustSet("alert", jsAlert)
mustSet("frameChunker", jsFrameChunker)
mustSet("formatTimestampLocal", jsFormatTimestampLocal)
+ mustSet("countChunkSize", jsCountChunkSize)
// Pre-import some useful modules.
registry.Enable(vm)
diff --git a/internal/manager/job_compilers/scripts/simple_blender_render.js b/internal/manager/job_compilers/scripts/simple_blender_render.js
index d980991d..75aec593 100644
--- a/internal/manager/job_compilers/scripts/simple_blender_render.js
+++ b/internal/manager/job_compilers/scripts/simple_blender_render.js
@@ -95,12 +95,14 @@ function authorRenderTasks(settings, renderDir, renderOutput) {
let renderTasks = [];
let chunks = frameChunker(settings.frames, settings.chunk_size);
for (let chunk of chunks) {
+ const numFrames = countChunkSize(chunk)
const task = author.Task(`render-${chunk}`, "blender");
const command = author.Command("blender-render", {
exe: "{blender}",
exeArgs: "{blenderArgs}",
argsBefore: [],
blendfile: settings.blendfile,
+ numFrames: numFrames,
args: [
"--render-output", path.join(renderDir, path.basename(renderOutput)),
"--render-format", settings.format,
diff --git a/internal/worker/command_blender.go b/internal/worker/command_blender.go
index 5b2cab2b..f319c158 100644
--- a/internal/worker/command_blender.go
+++ b/internal/worker/command_blender.go
@@ -7,6 +7,7 @@ package worker
import (
"context"
"fmt"
+ "math"
"os/exec"
"regexp"
"sync"
@@ -20,6 +21,10 @@ import (
)
var regexpFileSaved = regexp.MustCompile("Saved: '(.*)'")
+var regexpFrameNumber = regexp.MustCompile("Fra:[0-9]+")
+var prevFrame string
+var renderedNumFrames int
+var totalNumFrames float64
type BlenderParameters struct {
exe string // Expansion of `{blender}`: the executable path defined by the Manager.
@@ -27,6 +32,7 @@ type BlenderParameters struct {
argsBefore []string // Additional CLI arguments defined by the job compiler script, to go before the blend file name.
blendfile string // Path of the file to open.
args []string // Additional CLI arguments defined by the job compiler script, to go after the blend file name.
+ numFrames float64 // Additional CLI argument defined by the job compiler script, to define the total number of frame that are needed to be rendered.
}
// cmdBlender executes the "blender-render" command.
@@ -46,6 +52,8 @@ func (ce *CommandExecutor) cmdBlenderRender(ctx context.Context, logger zerolog.
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
+ prevFrame = ""
+ renderedNumFrames = 0
defer wg.Done()
for line := range lineChannel {
ce.processLineBlender(ctx, logger, taskID, line)
@@ -138,6 +146,12 @@ func cmdBlenderRenderParams(logger zerolog.Logger, cmd api.Command) (BlenderPara
// Ignore the `ok` return value, as a missing exeArgs key is fine:
parameters.exeArgs, _ = cmdParameter[string](cmd, "exeArgs")
+ // Ignore the `ok` return value, as a missing numFrames key is fine:
+ parameters.numFrames, _ = cmdParameter[float64](cmd, "numFrames")
+ if parameters.numFrames != 0 {
+ totalNumFrames = parameters.numFrames
+ }
+
if parameters.argsBefore, ok = cmdParameterAsStrings(cmd, "argsBefore"); !ok {
logger.Warn().Interface("command", cmd).Msg("invalid 'argsBefore' parameter")
return parameters, NewParameterInvalidError("argsBefore", cmd, "cannot convert to list of strings")
@@ -170,6 +184,22 @@ func cmdBlenderRenderParams(logger zerolog.Logger, cmd api.Command) (BlenderPara
func (ce *CommandExecutor) processLineBlender(ctx context.Context, logger zerolog.Logger, taskID string, line string) {
// TODO: check for "Warning: Unable to open" and other indicators of missing
// files. Flamenco v2 updated the task.Activity field for such situations.
+ renderedFrameNumber := regexpFrameNumber.FindString(line)
+ if renderedFrameNumber != "" && renderedFrameNumber != prevFrame {
+ renderedNumFrames++
+ prevFrame = renderedFrameNumber
+ var progress = int(math.Ceil(float64(renderedNumFrames) / totalNumFrames * 100))
+ // for checking the out of progress
+ fmt.Println("---------------------------")
+ fmt.Println(prevFrame)
+ fmt.Println(progress)
+ fmt.Println("---------------------------")
+
+ err := ce.listener.UpdateTaskProgress(ctx, taskID, progress)
+ if err != nil {
+ logger.Warn().Err(err).Msg("error send progress udpate to manager.")
+ }
+ }
match := regexpFileSaved.FindStringSubmatch(line)
if len(match) < 2 {
diff --git a/internal/worker/command_exe.go b/internal/worker/command_exe.go
index 0224a64e..6c36973e 100644
--- a/internal/worker/command_exe.go
+++ b/internal/worker/command_exe.go
@@ -39,6 +39,8 @@ type CommandListener interface {
LogProduced(ctx context.Context, taskID string, logLines ...string) error
// OutputProduced tells the Manager there has been some output (most commonly a rendered frame or video).
OutputProduced(ctx context.Context, taskID string, outputLocation string) error
+ // UpdateTaskProgress sends the progress update of the task to manager
+ UpdateTaskProgress(ctx context.Context, taskID string, progress int) error
}
// TimeService is a service that operates on time.
diff --git a/internal/worker/listener.go b/internal/worker/listener.go
index f4b57538..4731d367 100644
--- a/internal/worker/listener.go
+++ b/internal/worker/listener.go
@@ -6,6 +6,7 @@ import (
"context"
"errors"
"fmt"
+ "net/http"
"strings"
"github.com/rs/zerolog/log"
@@ -91,9 +92,35 @@ func (l *Listener) OutputProduced(ctx context.Context, taskID string, outputLoca
return nil
}
+func (l *Listener) UpdateTaskProgress(ctx context.Context, taskID string, progress int) error {
+ return l.sendProgressUpdate(ctx, taskID, api.TaskProgressUpdateJSONRequestBody{
+ Progress: progress,
+ })
+}
+
func (l *Listener) sendTaskUpdate(ctx context.Context, taskID string, update api.TaskUpdateJSONRequestBody) error {
if ctx.Err() != nil {
return ctx.Err()
}
return l.buffer.SendTaskUpdate(ctx, taskID, update)
}
+
+func (l *Listener) sendProgressUpdate(ctx context.Context, taskID string, progress api.TaskProgressUpdateJSONRequestBody) error {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ resp, err := l.client.TaskProgressUpdateWithResponse(ctx, taskID, progress)
+
+ if err != nil {
+ log.Warn().Err(err).Str("task", taskID).Msg("Error communicating with the Manager, unable to send progress update")
+ return fmt.Errorf("%v", err)
+ }
+
+ switch resp.StatusCode() {
+ case http.StatusNoContent:
+ return nil
+ default:
+ return fmt.Errorf("unknown error from Manager, code %d: %v",
+ resp.StatusCode(), resp.JSONDefault)
+ }
+}
diff --git a/internal/worker/mocks/client.gen.go b/internal/worker/mocks/client.gen.go
index 7ef5594e..3751dd6a 100644
--- a/internal/worker/mocks/client.gen.go
+++ b/internal/worker/mocks/client.gen.go
@@ -1376,6 +1376,46 @@ func (mr *MockFlamencoClientMockRecorder) TaskOutputProducedWithBodyWithResponse
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TaskOutputProducedWithBodyWithResponse", reflect.TypeOf((*MockFlamencoClient)(nil).TaskOutputProducedWithBodyWithResponse), varargs...)
}
+// TaskProgressUpdateWithBodyWithResponse mocks base method.
+func (m *MockFlamencoClient) TaskProgressUpdateWithBodyWithResponse(arg0 context.Context, arg1, arg2 string, arg3 io.Reader, arg4 ...api.RequestEditorFn) (*api.TaskProgressUpdateResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2, arg3}
+ for _, a := range arg4 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "TaskProgressUpdateWithBodyWithResponse", varargs...)
+ ret0, _ := ret[0].(*api.TaskProgressUpdateResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// TaskProgressUpdateWithBodyWithResponse indicates an expected call of TaskProgressUpdateWithBodyWithResponse.
+func (mr *MockFlamencoClientMockRecorder) TaskProgressUpdateWithBodyWithResponse(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TaskProgressUpdateWithBodyWithResponse", reflect.TypeOf((*MockFlamencoClient)(nil).TaskProgressUpdateWithBodyWithResponse), varargs...)
+}
+
+// TaskProgressUpdateWithResponse mocks base method.
+func (m *MockFlamencoClient) TaskProgressUpdateWithResponse(arg0 context.Context, arg1 string, arg2 api.TaskProgressUpdateJSONRequestBody, arg3 ...api.RequestEditorFn) (*api.TaskProgressUpdateResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{arg0, arg1, arg2}
+ for _, a := range arg3 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "TaskProgressUpdateWithResponse", varargs...)
+ ret0, _ := ret[0].(*api.TaskProgressUpdateResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// TaskProgressUpdateWithResponse indicates an expected call of TaskProgressUpdateWithResponse.
+func (mr *MockFlamencoClientMockRecorder) TaskProgressUpdateWithResponse(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TaskProgressUpdateWithResponse", reflect.TypeOf((*MockFlamencoClient)(nil).TaskProgressUpdateWithResponse), varargs...)
+}
+
// TaskUpdateWithBodyWithResponse mocks base method.
func (m *MockFlamencoClient) TaskUpdateWithBodyWithResponse(arg0 context.Context, arg1, arg2 string, arg3 io.Reader, arg4 ...api.RequestEditorFn) (*api.TaskUpdateResponse, error) {
m.ctrl.T.Helper()
diff --git a/internal/worker/mocks/command_listener.gen.go b/internal/worker/mocks/command_listener.gen.go
index 1581422e..ac418a44 100644
--- a/internal/worker/mocks/command_listener.gen.go
+++ b/internal/worker/mocks/command_listener.gen.go
@@ -66,3 +66,17 @@ func (mr *MockCommandListenerMockRecorder) OutputProduced(arg0, arg1, arg2 inter
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OutputProduced", reflect.TypeOf((*MockCommandListener)(nil).OutputProduced), arg0, arg1, arg2)
}
+
+// UpdateTaskProgress mocks base method.
+func (m *MockCommandListener) UpdateTaskProgress(arg0 context.Context, arg1 string, arg2 int) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpdateTaskProgress", arg0, arg1, arg2)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// UpdateTaskProgress indicates an expected call of UpdateTaskProgress.
+func (mr *MockCommandListenerMockRecorder) UpdateTaskProgress(arg0, arg1, arg2 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTaskProgress", reflect.TypeOf((*MockCommandListener)(nil).UpdateTaskProgress), arg0, arg1, arg2)
+}
diff --git a/pkg/api/flamenco-openapi.yaml b/pkg/api/flamenco-openapi.yaml
index b09395fa..153453c5 100644
--- a/pkg/api/flamenco-openapi.yaml
+++ b/pkg/api/flamenco-openapi.yaml
@@ -454,6 +454,37 @@ paths:
schema:
$ref: "#/components/schemas/Error"
+ /api/v3/worker/task/{task_id}/progress:
+ summary: Workers send the progress of the task, in the form of integer percentage value here.
+ post:
+ operationId: taskProgressUpdate
+ summary: Update the progress of the task.
+ security: [{ worker_auth: [] }]
+ tags: [worker]
+ parameters:
+ - name: task_id
+ in: path
+ required: true
+ schema: { type: string, format: uuid }
+ requestBody:
+ description: Task progress information
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/TaskProgressUpdate"
+ responses:
+ "204":
+ description: The progress update was accepted
+ "409":
+ description: The task is assigned to another worker, so the progress update was not accepted.
+ default:
+ description: unexpected error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+
# Worker Management
/api/v3/worker-mgt/workers:
summary: Obtain list of Workers known to the Manager.
@@ -1581,6 +1612,20 @@ components:
type: string
description: Log lines for this task, will be appended to logs sent earlier.
+ TaskProgressUpdate:
+ type: object
+ description: >
+ TaskProgressUpdate is sent by a Worker to update the progress of a task
+ it's executing.
+ properties:
+ "progress":
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: >
+ Indicates the percentage of the task that's been completed.
+ required: ["progress"]
+
MayKeepRunning:
type: object
description: Indicates whether the worker may keep running the task.
@@ -2233,6 +2278,24 @@ components:
"activity": { type: string }
required: [id, job_id, name, updated, status, activity]
+ SocketIOTaskProgressUpdate:
+ type: object
+ description: >
+ Task progress, sent to a SocketIO room when progress of a task changes.
+ properties:
+ "id":
+ type: string
+ format: uuid
+ description: UUID of the Task
+ "job_id": { type: string, format: uuid }
+ "progress":
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: >
+ Indicates the percentage of the task that's been completed.
+ required: [id, job_id, progress]
+
SocketIOTaskLogUpdate:
type: object
description: >
diff --git a/pkg/api/openapi_client.gen.go b/pkg/api/openapi_client.gen.go
index b93f11cc..ab357ae4 100644
--- a/pkg/api/openapi_client.gen.go
+++ b/pkg/api/openapi_client.gen.go
@@ -292,6 +292,11 @@ type ClientInterface interface {
// TaskOutputProduced request with any body
TaskOutputProducedWithBody(ctx context.Context, taskId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // TaskProgressUpdate request with any body
+ TaskProgressUpdateWithBody(ctx context.Context, taskId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ TaskProgressUpdate(ctx context.Context, taskId string, body TaskProgressUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
}
func (c *Client) GetConfiguration(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
@@ -1182,6 +1187,30 @@ func (c *Client) TaskOutputProducedWithBody(ctx context.Context, taskId string,
return c.Client.Do(req)
}
+func (c *Client) TaskProgressUpdateWithBody(ctx context.Context, taskId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewTaskProgressUpdateRequestWithBody(c.Server, taskId, 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) TaskProgressUpdate(ctx context.Context, taskId string, body TaskProgressUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewTaskProgressUpdateRequest(c.Server, taskId, 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)
+}
+
// NewGetConfigurationRequest generates requests for GetConfiguration
func NewGetConfigurationRequest(server string) (*http.Request, error) {
var err error
@@ -3150,6 +3179,53 @@ func NewTaskOutputProducedRequestWithBody(server string, taskId string, contentT
return req, nil
}
+// NewTaskProgressUpdateRequest calls the generic TaskProgressUpdate builder with application/json body
+func NewTaskProgressUpdateRequest(server string, taskId string, body TaskProgressUpdateJSONRequestBody) (*http.Request, error) {
+ var bodyReader io.Reader
+ buf, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ bodyReader = bytes.NewReader(buf)
+ return NewTaskProgressUpdateRequestWithBody(server, taskId, "application/json", bodyReader)
+}
+
+// NewTaskProgressUpdateRequestWithBody generates requests for TaskProgressUpdate with any type of body
+func NewTaskProgressUpdateRequestWithBody(server string, taskId string, contentType string, body io.Reader) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "task_id", runtime.ParamLocationPath, taskId)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/api/v3/worker/task/%s/progress", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", contentType)
+
+ return req, nil
+}
+
func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error {
for _, r := range c.RequestEditors {
if err := r(ctx, req); err != nil {
@@ -3393,6 +3469,11 @@ type ClientWithResponsesInterface interface {
// TaskOutputProduced request with any body
TaskOutputProducedWithBodyWithResponse(ctx context.Context, taskId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*TaskOutputProducedResponse, error)
+
+ // TaskProgressUpdate request with any body
+ TaskProgressUpdateWithBodyWithResponse(ctx context.Context, taskId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*TaskProgressUpdateResponse, error)
+
+ TaskProgressUpdateWithResponse(ctx context.Context, taskId string, body TaskProgressUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*TaskProgressUpdateResponse, error)
}
type GetConfigurationResponse struct {
@@ -4209,6 +4290,7 @@ func (r FetchWorkerClustersResponse) StatusCode() int {
type CreateWorkerClusterResponse struct {
Body []byte
HTTPResponse *http.Response
+ JSON200 *WorkerCluster
JSONDefault *Error
}
@@ -4591,6 +4673,28 @@ func (r TaskOutputProducedResponse) StatusCode() int {
return 0
}
+type TaskProgressUpdateResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSONDefault *Error
+}
+
+// Status returns HTTPResponse.Status
+func (r TaskProgressUpdateResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r TaskProgressUpdateResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
// GetConfigurationWithResponse request returning *GetConfigurationResponse
func (c *ClientWithResponses) GetConfigurationWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetConfigurationResponse, error) {
rsp, err := c.GetConfiguration(ctx, reqEditors...)
@@ -5236,6 +5340,23 @@ func (c *ClientWithResponses) TaskOutputProducedWithBodyWithResponse(ctx context
return ParseTaskOutputProducedResponse(rsp)
}
+// TaskProgressUpdateWithBodyWithResponse request with arbitrary body returning *TaskProgressUpdateResponse
+func (c *ClientWithResponses) TaskProgressUpdateWithBodyWithResponse(ctx context.Context, taskId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*TaskProgressUpdateResponse, error) {
+ rsp, err := c.TaskProgressUpdateWithBody(ctx, taskId, contentType, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseTaskProgressUpdateResponse(rsp)
+}
+
+func (c *ClientWithResponses) TaskProgressUpdateWithResponse(ctx context.Context, taskId string, body TaskProgressUpdateJSONRequestBody, reqEditors ...RequestEditorFn) (*TaskProgressUpdateResponse, error) {
+ rsp, err := c.TaskProgressUpdate(ctx, taskId, body, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseTaskProgressUpdateResponse(rsp)
+}
+
// ParseGetConfigurationResponse parses an HTTP response from a GetConfigurationWithResponse call
func ParseGetConfigurationResponse(rsp *http.Response) (*GetConfigurationResponse, error) {
bodyBytes, err := ioutil.ReadAll(rsp.Body)
@@ -6307,6 +6428,13 @@ func ParseCreateWorkerClusterResponse(rsp *http.Response) (*CreateWorkerClusterR
}
switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest WorkerCluster
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true:
var dest Error
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
@@ -6811,3 +6939,29 @@ func ParseTaskOutputProducedResponse(rsp *http.Response) (*TaskOutputProducedRes
return response, nil
}
+
+// ParseTaskProgressUpdateResponse parses an HTTP response from a TaskProgressUpdateWithResponse call
+func ParseTaskProgressUpdateResponse(rsp *http.Response) (*TaskProgressUpdateResponse, error) {
+ bodyBytes, err := ioutil.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &TaskProgressUpdateResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true:
+ var dest Error
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSONDefault = &dest
+
+ }
+
+ return response, nil
+}
diff --git a/pkg/api/openapi_server.gen.go b/pkg/api/openapi_server.gen.go
index d314ecbf..0521a9ee 100644
--- a/pkg/api/openapi_server.gen.go
+++ b/pkg/api/openapi_server.gen.go
@@ -173,6 +173,9 @@ type ServerInterface interface {
// Store the most recently rendered frame here. Note that it is up to the Worker to ensure this is in a format that's digestable by the Manager. Currently only PNG and JPEG support is planned.
// (POST /api/v3/worker/task/{task_id}/output-produced)
TaskOutputProduced(ctx echo.Context, taskId string) error
+ // Update the progress of the task.
+ // (POST /api/v3/worker/task/{task_id}/progress)
+ TaskProgressUpdate(ctx echo.Context, taskId string) error
}
// ServerInterfaceWrapper converts echo contexts to parameters.
@@ -950,6 +953,24 @@ func (w *ServerInterfaceWrapper) TaskOutputProduced(ctx echo.Context) error {
return err
}
+// TaskProgressUpdate converts echo context to params.
+func (w *ServerInterfaceWrapper) TaskProgressUpdate(ctx echo.Context) error {
+ var err error
+ // ------------- Path parameter "task_id" -------------
+ var taskId string
+
+ err = runtime.BindStyledParameterWithLocation("simple", false, "task_id", runtime.ParamLocationPath, ctx.Param("task_id"), &taskId)
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter task_id: %s", err))
+ }
+
+ ctx.Set(Worker_authScopes, []string{""})
+
+ // Invoke the callback with all the unmarshalled arguments
+ err = w.Handler.TaskProgressUpdate(ctx, taskId)
+ return err
+}
+
// This is a simple interface which specifies echo.Route addition functions which
// are present on both echo.Echo and echo.Group, since we want to allow using
// either of them for path registration
@@ -1031,5 +1052,6 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
router.POST(baseURL+"/api/v3/worker/task/:task_id", wrapper.TaskUpdate)
router.GET(baseURL+"/api/v3/worker/task/:task_id/may-i-run", wrapper.MayWorkerRun)
router.POST(baseURL+"/api/v3/worker/task/:task_id/output-produced", wrapper.TaskOutputProduced)
+ router.POST(baseURL+"/api/v3/worker/task/:task_id/progress", wrapper.TaskProgressUpdate)
}
diff --git a/pkg/api/openapi_spec.gen.go b/pkg/api/openapi_spec.gen.go
index c15a6b05..f62018b1 100644
--- a/pkg/api/openapi_spec.gen.go
+++ b/pkg/api/openapi_spec.gen.go
@@ -18,216 +18,220 @@ 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 7e10b218..6bb181f6 100644
--- a/pkg/api/openapi_types.gen.go
+++ b/pkg/api/openapi_types.gen.go
@@ -578,6 +578,16 @@ type SocketIOTaskLogUpdate struct {
TaskId string `json:"task_id"`
}
+// Task progress, sent to a SocketIO room when progress of a task changes.
+type SocketIOTaskProgressUpdate struct {
+ // UUID of the Task
+ Id string `json:"id"`
+ JobId string `json:"job_id"`
+
+ // Indicates the percentage of the task that's been completed.
+ Progress int `json:"progress"`
+}
+
// Subset of a Task, sent over SocketIO when a task changes. For new tasks, `previous_status` will be excluded.
type SocketIOTaskUpdate struct {
Activity string `json:"activity"`
@@ -680,6 +690,12 @@ type TaskLogInfo struct {
Url string `json:"url"`
}
+// TaskProgressUpdate is sent by a Worker to update the progress of a task it's executing.
+type TaskProgressUpdate struct {
+ // Indicates the percentage of the task that's been completed.
+ Progress int `json:"progress"`
+}
+
// TaskStatus defines model for TaskStatus.
type TaskStatus string
@@ -901,6 +917,9 @@ type WorkerStateChangedJSONBody WorkerStateChanged
// TaskUpdateJSONBody defines parameters for TaskUpdate.
type TaskUpdateJSONBody TaskUpdate
+// TaskProgressUpdateJSONBody defines parameters for TaskProgressUpdate.
+type TaskProgressUpdateJSONBody TaskProgressUpdate
+
// CheckBlenderExePathJSONRequestBody defines body for CheckBlenderExePath for application/json ContentType.
type CheckBlenderExePathJSONRequestBody CheckBlenderExePathJSONBody
@@ -964,6 +983,9 @@ type WorkerStateChangedJSONRequestBody WorkerStateChangedJSONBody
// TaskUpdateJSONRequestBody defines body for TaskUpdate for application/json ContentType.
type TaskUpdateJSONRequestBody TaskUpdateJSONBody
+// TaskProgressUpdateJSONRequestBody defines body for TaskProgressUpdate for application/json ContentType.
+type TaskProgressUpdateJSONRequestBody TaskProgressUpdateJSONBody
+
// Getter for additional properties for JobMetadata. Returns the specified
// element and whether it was found
func (a JobMetadata) Get(fieldName string) (value string, found bool) {
diff --git a/web/app/src/manager-api/index.js b/web/app/src/manager-api/index.js
index b25813cc..340942fd 100644
--- a/web/app/src/manager-api/index.js
+++ b/web/app/src/manager-api/index.js
@@ -61,11 +61,13 @@ import SocketIOSubscription from './model/SocketIOSubscription';
import SocketIOSubscriptionOperation from './model/SocketIOSubscriptionOperation';
import SocketIOSubscriptionType from './model/SocketIOSubscriptionType';
import SocketIOTaskLogUpdate from './model/SocketIOTaskLogUpdate';
+import SocketIOTaskProgressUpdate from './model/SocketIOTaskProgressUpdate';
import SocketIOTaskUpdate from './model/SocketIOTaskUpdate';
import SocketIOWorkerUpdate from './model/SocketIOWorkerUpdate';
import SubmittedJob from './model/SubmittedJob';
import Task from './model/Task';
import TaskLogInfo from './model/TaskLogInfo';
+import TaskProgressUpdate from './model/TaskProgressUpdate';
import TaskStatus from './model/TaskStatus';
import TaskStatusChange from './model/TaskStatusChange';
import TaskSummary from './model/TaskSummary';
@@ -420,6 +422,12 @@ export {
*/
SocketIOTaskLogUpdate,
+ /**
+ * The SocketIOTaskProgressUpdate model constructor.
+ * @property {module:model/SocketIOTaskProgressUpdate}
+ */
+ SocketIOTaskProgressUpdate,
+
/**
* The SocketIOTaskUpdate model constructor.
* @property {module:model/SocketIOTaskUpdate}
@@ -450,6 +458,12 @@ export {
*/
TaskLogInfo,
+ /**
+ * The TaskProgressUpdate model constructor.
+ * @property {module:model/TaskProgressUpdate}
+ */
+ TaskProgressUpdate,
+
/**
* The TaskStatus model constructor.
* @property {module:model/TaskStatus}
diff --git a/web/app/src/manager-api/manager/WorkerApi.js b/web/app/src/manager-api/manager/WorkerApi.js
index 9c001e7f..0652666e 100644
--- a/web/app/src/manager-api/manager/WorkerApi.js
+++ b/web/app/src/manager-api/manager/WorkerApi.js
@@ -18,6 +18,7 @@ import Error from '../model/Error';
import MayKeepRunning from '../model/MayKeepRunning';
import RegisteredWorker from '../model/RegisteredWorker';
import SecurityError from '../model/SecurityError';
+import TaskProgressUpdate from '../model/TaskProgressUpdate';
import TaskUpdate from '../model/TaskUpdate';
import WorkerRegistration from '../model/WorkerRegistration';
import WorkerSignOn from '../model/WorkerSignOn';
@@ -310,6 +311,58 @@ export default class WorkerApi {
}
+ /**
+ * Update the progress of the task.
+ * @param {String} taskId
+ * @param {module:model/TaskProgressUpdate} taskProgressUpdate Task progress information
+ * @return {Promise} a {@link https://www.promisejs.org/|Promise}, with an object containing HTTP response
+ */
+ taskProgressUpdateWithHttpInfo(taskId, taskProgressUpdate) {
+ let postBody = taskProgressUpdate;
+ // verify the required parameter 'taskId' is set
+ if (taskId === undefined || taskId === null) {
+ throw new Error("Missing the required parameter 'taskId' when calling taskProgressUpdate");
+ }
+ // verify the required parameter 'taskProgressUpdate' is set
+ if (taskProgressUpdate === undefined || taskProgressUpdate === null) {
+ throw new Error("Missing the required parameter 'taskProgressUpdate' when calling taskProgressUpdate");
+ }
+
+ let pathParams = {
+ 'task_id': taskId
+ };
+ let queryParams = {
+ };
+ let headerParams = {
+ };
+ let formParams = {
+ };
+
+ let authNames = ['worker_auth'];
+ let contentTypes = ['application/json'];
+ let accepts = ['application/json'];
+ let returnType = null;
+ return this.apiClient.callApi(
+ '/api/v3/worker/task/{task_id}/progress', 'POST',
+ pathParams, queryParams, headerParams, formParams, postBody,
+ authNames, contentTypes, accepts, returnType, null
+ );
+ }
+
+ /**
+ * Update the progress of the task.
+ * @param {String} taskId
+ * @param {module:model/TaskProgressUpdate} taskProgressUpdate Task progress information
+ * @return {Promise} a {@link https://www.promisejs.org/|Promise}
+ */
+ taskProgressUpdate(taskId, taskProgressUpdate) {
+ return this.taskProgressUpdateWithHttpInfo(taskId, taskProgressUpdate)
+ .then(function(response_and_data) {
+ return response_and_data.data;
+ });
+ }
+
+
/**
* Update the task, typically to indicate progress, completion, or failure.
* @param {String} taskId
diff --git a/web/app/src/manager-api/manager/WorkerMgtApi.js b/web/app/src/manager-api/manager/WorkerMgtApi.js
index 08c0ccaf..b86e7e5c 100644
--- a/web/app/src/manager-api/manager/WorkerMgtApi.js
+++ b/web/app/src/manager-api/manager/WorkerMgtApi.js
@@ -45,7 +45,7 @@ export default class WorkerMgtApi {
/**
* Create a new worker cluster.
* @param {module:model/WorkerCluster} workerCluster The worker cluster.
- * @return {Promise} a {@link https://www.promisejs.org/|Promise}, with an object containing HTTP response
+ * @return {Promise} a {@link https://www.promisejs.org/|Promise}, with an object containing data of type {@link module:model/WorkerCluster} and HTTP response
*/
createWorkerClusterWithHttpInfo(workerCluster) {
let postBody = workerCluster;
@@ -66,7 +66,7 @@ export default class WorkerMgtApi {
let authNames = [];
let contentTypes = ['application/json'];
let accepts = ['application/json'];
- let returnType = null;
+ let returnType = WorkerCluster;
return this.apiClient.callApi(
'/api/v3/worker-mgt/clusters', 'POST',
pathParams, queryParams, headerParams, formParams, postBody,
@@ -77,7 +77,7 @@ export default class WorkerMgtApi {
/**
* Create a new worker cluster.
* @param {module:model/WorkerCluster} workerCluster The worker cluster.
- * @return {Promise} a {@link https://www.promisejs.org/|Promise}
+ * @return {Promise} a {@link https://www.promisejs.org/|Promise}, with data of type {@link module:model/WorkerCluster}
*/
createWorkerCluster(workerCluster) {
return this.createWorkerClusterWithHttpInfo(workerCluster)
diff --git a/web/app/src/manager-api/model/SocketIOTaskProgressUpdate.js b/web/app/src/manager-api/model/SocketIOTaskProgressUpdate.js
new file mode 100644
index 00000000..0000db05
--- /dev/null
+++ b/web/app/src/manager-api/model/SocketIOTaskProgressUpdate.js
@@ -0,0 +1,96 @@
+/**
+ * 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';
+
+/**
+ * The SocketIOTaskProgressUpdate model module.
+ * @module model/SocketIOTaskProgressUpdate
+ * @version 0.0.0
+ */
+class SocketIOTaskProgressUpdate {
+ /**
+ * Constructs a new SocketIOTaskProgressUpdate
.
+ * Task progress, sent to a SocketIO room when progress of a task changes.
+ * @alias module:model/SocketIOTaskProgressUpdate
+ * @param id {String} UUID of the Task
+ * @param jobId {String}
+ * @param progress {Number} Indicates the percentage of the task that's been completed.
+ */
+ constructor(id, jobId, progress) {
+
+ SocketIOTaskProgressUpdate.initialize(this, id, jobId, progress);
+ }
+
+ /**
+ * 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, jobId, progress) {
+ obj['id'] = id;
+ obj['job_id'] = jobId;
+ obj['progress'] = progress;
+ }
+
+ /**
+ * Constructs a SocketIOTaskProgressUpdate
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/SocketIOTaskProgressUpdate} obj Optional instance to populate.
+ * @return {module:model/SocketIOTaskProgressUpdate} The populated SocketIOTaskProgressUpdate
instance.
+ */
+ static constructFromObject(data, obj) {
+ if (data) {
+ obj = obj || new SocketIOTaskProgressUpdate();
+
+ if (data.hasOwnProperty('id')) {
+ obj['id'] = ApiClient.convertToType(data['id'], 'String');
+ }
+ if (data.hasOwnProperty('job_id')) {
+ obj['job_id'] = ApiClient.convertToType(data['job_id'], 'String');
+ }
+ if (data.hasOwnProperty('progress')) {
+ obj['progress'] = ApiClient.convertToType(data['progress'], 'Number');
+ }
+ }
+ return obj;
+ }
+
+
+}
+
+/**
+ * UUID of the Task
+ * @member {String} id
+ */
+SocketIOTaskProgressUpdate.prototype['id'] = undefined;
+
+/**
+ * @member {String} job_id
+ */
+SocketIOTaskProgressUpdate.prototype['job_id'] = undefined;
+
+/**
+ * Indicates the percentage of the task that's been completed.
+ * @member {Number} progress
+ */
+SocketIOTaskProgressUpdate.prototype['progress'] = undefined;
+
+
+
+
+
+
+export default SocketIOTaskProgressUpdate;
+
diff --git a/web/app/src/manager-api/model/TaskProgressUpdate.js b/web/app/src/manager-api/model/TaskProgressUpdate.js
new file mode 100644
index 00000000..ac5b38dc
--- /dev/null
+++ b/web/app/src/manager-api/model/TaskProgressUpdate.js
@@ -0,0 +1,75 @@
+/**
+ * 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';
+
+/**
+ * The TaskProgressUpdate model module.
+ * @module model/TaskProgressUpdate
+ * @version 0.0.0
+ */
+class TaskProgressUpdate {
+ /**
+ * Constructs a new TaskProgressUpdate
.
+ * TaskProgressUpdate is sent by a Worker to update the progress of a task it's executing.
+ * @alias module:model/TaskProgressUpdate
+ * @param progress {Number} Indicates the percentage of the task that's been completed.
+ */
+ constructor(progress) {
+
+ TaskProgressUpdate.initialize(this, progress);
+ }
+
+ /**
+ * 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, progress) {
+ obj['progress'] = progress;
+ }
+
+ /**
+ * Constructs a TaskProgressUpdate
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/TaskProgressUpdate} obj Optional instance to populate.
+ * @return {module:model/TaskProgressUpdate} The populated TaskProgressUpdate
instance.
+ */
+ static constructFromObject(data, obj) {
+ if (data) {
+ obj = obj || new TaskProgressUpdate();
+
+ if (data.hasOwnProperty('progress')) {
+ obj['progress'] = ApiClient.convertToType(data['progress'], 'Number');
+ }
+ }
+ return obj;
+ }
+
+
+}
+
+/**
+ * Indicates the percentage of the task that's been completed.
+ * @member {Number} progress
+ */
+TaskProgressUpdate.prototype['progress'] = undefined;
+
+
+
+
+
+
+export default TaskProgressUpdate;
+