From 7a1ae6928a003c234ccccdeaeb8120ab08d98c03 Mon Sep 17 00:00:00 2001 From: Nitin Rawat Date: Thu, 9 Feb 2023 08:48:44 +0530 Subject: [PATCH 1/8] OAPI: add endpoint to get task progress update from workers Add an endpoint for the Manager, to allow Workers, to send progress update of the task. Define the schema of the request body to hit this endpoint. Issue: #103268 --- pkg/api/flamenco-openapi.yaml | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/pkg/api/flamenco-openapi.yaml b/pkg/api/flamenco-openapi.yaml index 9f612097..d431dd24 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: + applicaton/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. @@ -1463,6 +1494,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. -- 2.30.2 From 2053b25ecff1131f83b58ba016bd91c985329d8f Mon Sep 17 00:00:00 2001 From: Nitin Rawat Date: Thu, 9 Feb 2023 08:53:04 +0530 Subject: [PATCH 2/8] OAPI: add `SocketIOTaskProgressUpdate` for broadcasting progress updates `SocketIOTaskProgressUpdate` objects are meant to be broadcast to SocketIO clients (i.e. the web interface). They are sent to the job specific room, just like task updates. --- pkg/api/flamenco-openapi.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/api/flamenco-openapi.yaml b/pkg/api/flamenco-openapi.yaml index d431dd24..21f8673a 100644 --- a/pkg/api/flamenco-openapi.yaml +++ b/pkg/api/flamenco-openapi.yaml @@ -2153,6 +2153,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: > -- 2.30.2 From 168c786707058daa46b455dc46b73372e918a6a2 Mon Sep 17 00:00:00 2001 From: Nitin Rawat Date: Thu, 9 Feb 2023 09:48:20 +0530 Subject: [PATCH 3/8] OAPI: regenerate code --- addon/flamenco/manager/api/worker_api.py | 140 ++++++ .../docs/SocketIOTaskProgressUpdate.md | 15 + .../manager/docs/TaskProgressUpdate.md | 13 + addon/flamenco/manager/docs/WorkerApi.md | 83 ++++ .../model/socket_io_task_progress_update.py | 277 ++++++++++++ .../manager/model/task_progress_update.py | 265 ++++++++++++ addon/flamenco/manager/models/__init__.py | 2 + addon/flamenco/manager_README.md | 3 + internal/worker/mocks/client.gen.go | 20 + pkg/api/openapi_client.gen.go | 111 +++++ pkg/api/openapi_server.gen.go | 22 + pkg/api/openapi_spec.gen.go | 401 +++++++++--------- pkg/api/openapi_types.gen.go | 16 + web/app/src/manager-api/index.js | 14 + web/app/src/manager-api/manager/WorkerApi.js | 53 +++ .../model/SocketIOTaskProgressUpdate.js | 96 +++++ .../manager-api/model/TaskProgressUpdate.js | 75 ++++ 17 files changed, 1407 insertions(+), 199 deletions(-) create mode 100644 addon/flamenco/manager/docs/SocketIOTaskProgressUpdate.md create mode 100644 addon/flamenco/manager/docs/TaskProgressUpdate.md create mode 100644 addon/flamenco/manager/model/socket_io_task_progress_update.py create mode 100644 addon/flamenco/manager/model/task_progress_update.py create mode 100644 web/app/src/manager-api/model/SocketIOTaskProgressUpdate.js create mode 100644 web/app/src/manager-api/model/TaskProgressUpdate.js diff --git a/addon/flamenco/manager/api/worker_api.py b/addon/flamenco/manager/api/worker_api.py index e81067c9..5d831321 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': [ + 'applicaton/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/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..b9bc6910 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**: applicaton/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/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 32587b61..a021afa1 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 ddf66d81..825b3ce8 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. @@ -179,11 +180,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/worker/mocks/client.gen.go b/internal/worker/mocks/client.gen.go index c4505056..fbd068d6 100644 --- a/internal/worker/mocks/client.gen.go +++ b/internal/worker/mocks/client.gen.go @@ -1236,6 +1236,26 @@ 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...) +} + // 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/pkg/api/openapi_client.gen.go b/pkg/api/openapi_client.gen.go index 0b6d994a..e9cddb1e 100644 --- a/pkg/api/openapi_client.gen.go +++ b/pkg/api/openapi_client.gen.go @@ -268,6 +268,9 @@ 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) } func (c *Client) GetConfiguration(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -1050,6 +1053,18 @@ 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) +} + // NewGetConfigurationRequest generates requests for GetConfiguration func NewGetConfigurationRequest(server string) (*http.Request, error) { var err error @@ -2789,6 +2804,42 @@ func NewTaskOutputProducedRequestWithBody(server string, taskId string, contentT return req, nil } +// 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 { @@ -3008,6 +3059,9 @@ 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) } type GetConfigurationResponse struct { @@ -4074,6 +4128,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...) @@ -4641,6 +4717,15 @@ 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) +} + // ParseGetConfigurationResponse parses an HTTP response from a GetConfigurationWithResponse call func ParseGetConfigurationResponse(rsp *http.Response) (*GetConfigurationResponse, error) { bodyBytes, err := ioutil.ReadAll(rsp.Body) @@ -6060,3 +6145,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 a88f5824..c5239039 100644 --- a/pkg/api/openapi_server.gen.go +++ b/pkg/api/openapi_server.gen.go @@ -155,6 +155,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. @@ -850,6 +853,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 @@ -925,5 +946,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 26f38f78..f1876a97 100644 --- a/pkg/api/openapi_spec.gen.go +++ b/pkg/api/openapi_spec.gen.go @@ -18,205 +18,208 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "VFOJ53Cpgh9/+/j/AgAA///jSOnmIvYAAA==", } // 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 74b6fb34..f2b3a0aa 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"` @@ -677,6 +687,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 diff --git a/web/app/src/manager-api/index.js b/web/app/src/manager-api/index.js index 125bd02f..36291321 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'; @@ -417,6 +419,12 @@ export { */ SocketIOTaskLogUpdate, + /** + * The SocketIOTaskProgressUpdate model constructor. + * @property {module:model/SocketIOTaskProgressUpdate} + */ + SocketIOTaskProgressUpdate, + /** * The SocketIOTaskUpdate model constructor. * @property {module:model/SocketIOTaskUpdate} @@ -447,6 +455,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..308d62c9 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 = ['applicaton/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/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; + -- 2.30.2 From bd86d0ff60bd2ffc6f6b887a67bf66d2b05ca434 Mon Sep 17 00:00:00 2001 From: Nitin Rawat Date: Thu, 9 Feb 2023 18:01:17 +0530 Subject: [PATCH 4/8] Worker: Extending blender render command Blender render command is being extended in order to accept a frameRange property which is optional. FrameRange property will help in the calculation of the progress of a task. Issue: 103268 --- internal/manager/api_impl/workers.go | 4 +++ internal/manager/job_compilers/js_globals.go | 5 +++ .../manager/job_compilers/js_globals_test.go | 35 +++++++++++++++++++ internal/manager/job_compilers/scripts.go | 1 + .../scripts/simple_blender_render.js | 2 ++ internal/worker/command_blender.go | 4 +++ 6 files changed, 51 insertions(+) diff --git a/internal/manager/api_impl/workers.go b/internal/manager/api_impl/workers.go index 0f599824..fe4b62cf 100644 --- a/internal/manager/api_impl/workers.go +++ b/internal/manager/api_impl/workers.go @@ -369,6 +369,10 @@ func (f *Flamenco) ScheduleTask(e echo.Context) error { return e.JSON(http.StatusOK, customisedTask) } +// TODO: Complete the immplementation for the below function +func (f *Flamenco) TaskProgressUpdate(e echo.Context, taskID string) error { + return fmt.Errorf("") +} 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..2018ee07 100644 --- a/internal/worker/command_blender.go +++ b/internal/worker/command_blender.go @@ -27,6 +27,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. @@ -138,6 +139,9 @@ 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.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") -- 2.30.2 From 4ca61cc457ab93b5f814deb46ba2c3b57817dc9d Mon Sep 17 00:00:00 2001 From: Nitin Rawat Date: Mon, 6 Mar 2023 21:07:16 +0530 Subject: [PATCH 5/8] OAPI: fix a typo --- pkg/api/flamenco-openapi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/flamenco-openapi.yaml b/pkg/api/flamenco-openapi.yaml index 21f8673a..4cf5731c 100644 --- a/pkg/api/flamenco-openapi.yaml +++ b/pkg/api/flamenco-openapi.yaml @@ -470,7 +470,7 @@ paths: description: Task progress information required: true content: - applicaton/json: + application/json: schema: $ref: "#/components/schemas/TaskProgressUpdate" responses: -- 2.30.2 From deef71e2070ea32ee1d3e59f2a44d7b893e829e9 Mon Sep 17 00:00:00 2001 From: Nitin Rawat Date: Mon, 6 Mar 2023 21:12:51 +0530 Subject: [PATCH 6/8] OAPI: regenerate code --- addon/flamenco/manager/api/worker_api.py | 2 +- addon/flamenco/manager/docs/WorkerApi.md | 2 +- internal/worker/mocks/client.gen.go | 20 +++++++++++ pkg/api/openapi_client.gen.go | 35 ++++++++++++++++++++ pkg/api/openapi_spec.gen.go | 10 +++--- pkg/api/openapi_types.gen.go | 6 ++++ web/app/src/manager-api/manager/WorkerApi.js | 2 +- 7 files changed, 69 insertions(+), 8 deletions(-) diff --git a/addon/flamenco/manager/api/worker_api.py b/addon/flamenco/manager/api/worker_api.py index 5d831321..df717f6a 100644 --- a/addon/flamenco/manager/api/worker_api.py +++ b/addon/flamenco/manager/api/worker_api.py @@ -398,7 +398,7 @@ class WorkerApi(object): 'application/json' ], 'content_type': [ - 'applicaton/json' + 'application/json' ] }, api_client=api_client diff --git a/addon/flamenco/manager/docs/WorkerApi.md b/addon/flamenco/manager/docs/WorkerApi.md index b9bc6910..440911d4 100644 --- a/addon/flamenco/manager/docs/WorkerApi.md +++ b/addon/flamenco/manager/docs/WorkerApi.md @@ -554,7 +554,7 @@ void (empty response body) ### HTTP request headers - - **Content-Type**: applicaton/json + - **Content-Type**: application/json - **Accept**: application/json diff --git a/internal/worker/mocks/client.gen.go b/internal/worker/mocks/client.gen.go index fbd068d6..cb22d059 100644 --- a/internal/worker/mocks/client.gen.go +++ b/internal/worker/mocks/client.gen.go @@ -1256,6 +1256,26 @@ func (mr *MockFlamencoClientMockRecorder) TaskProgressUpdateWithBodyWithResponse 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/pkg/api/openapi_client.gen.go b/pkg/api/openapi_client.gen.go index e9cddb1e..e04cc1fb 100644 --- a/pkg/api/openapi_client.gen.go +++ b/pkg/api/openapi_client.gen.go @@ -271,6 +271,8 @@ type ClientInterface interface { // 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) { @@ -1065,6 +1067,18 @@ func (c *Client) TaskProgressUpdateWithBody(ctx context.Context, taskId string, 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 @@ -2804,6 +2818,17 @@ 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 @@ -3062,6 +3087,8 @@ type ClientWithResponsesInterface interface { // 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 { @@ -4726,6 +4753,14 @@ func (c *ClientWithResponses) TaskProgressUpdateWithBodyWithResponse(ctx context 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) diff --git a/pkg/api/openapi_spec.gen.go b/pkg/api/openapi_spec.gen.go index f1876a97..5c6fc988 100644 --- a/pkg/api/openapi_spec.gen.go +++ b/pkg/api/openapi_spec.gen.go @@ -215,11 +215,11 @@ var swaggerSpec = []string{ "lhIHgpH7BBNw3EoPb36lx3QFgflQUp4qV4v7wf2Ht+FG0FVZSmUP6hXLOSWnq9J5zADFCGKUFyYnIa+k", "7tASR9c8OPj+dqr/+0Q35JRAOiS07V2Rqb3YrqKGy5swcyWNKZiru/GHkjwwocUCeiG1IYplmOYTaoTA", "flEeiNJaOACnKn2kSu0IYUJjkQ8MNgPp3Z2y/fKeJjmfMY2dTltnTJ6FNCOIwzn+5UeA88/HP/xIHCrZ", - "QcuCCpGOg1kn8Jh5tZgIygu9Vyp2xdnSkyWusDKKp/YEqf+2YpCXnNYziWP31l3UIK6lQLQ21KdIeOh8", - "HlUijJbQKT6XSpGa42vQLcK+GpHMu12h9ChDb82wJ2wfuExiUjJlKYql9Ve0qJi/UrAFdeVxHxur7w0i", - "u277DI+acVudxiOe+IbLAwGy3STcn+XEex4AJf5eMcUtRa+7+wxbpZzHjQpUOjHo0+OjZjuU2OosF4tK", - "oAYHyb2ppqKNmIjEBI7AvgprItAZtLcZGTaCsNuwyKlk4VfUmQz8+Ik0c0zdC7OA6FXnHToIhhYt7+Qk", - "VFOJ53Cpgh9/+/j/AgAA///jSOnmIvYAAA==", + "QcuCCpGOg1kn8Jh5tZgIygu9Vyp2xdnSkyWusDKKp/YEqf+2YpCXnNYziWP31tejQbR21KdJePB8Hl0i", + "jJZQKj6XTpGa42tQLsK+GqHMu92h9ChDb86wJ2wfuFRiUjJlSYol9le0qJi/U7AFdeWRHzur7w0iw277", + "DI+agVudziOe+obbAxGy3Szcn+XEux4AJf5eMcUtSa/b+wxbtZzHjRJUOjHo0+OjZj+U2OwsF4tKoAoH", + "2b2prqKNoIjEBI7CvgprItAatLcbGXaCsNuwyKlk4VfUmQwc+Yk8c8zdC7OA7FUnHjoIhh4t7+QklFOJ", + "53C5gh9/+/j/AgAA//9+ZK/OI/YAAA==", } // 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 f2b3a0aa..d00558b4 100644 --- a/pkg/api/openapi_types.gen.go +++ b/pkg/api/openapi_types.gen.go @@ -883,6 +883,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 @@ -937,6 +940,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/manager/WorkerApi.js b/web/app/src/manager-api/manager/WorkerApi.js index 308d62c9..0652666e 100644 --- a/web/app/src/manager-api/manager/WorkerApi.js +++ b/web/app/src/manager-api/manager/WorkerApi.js @@ -339,7 +339,7 @@ export default class WorkerApi { }; let authNames = ['worker_auth']; - let contentTypes = ['applicaton/json']; + let contentTypes = ['application/json']; let accepts = ['application/json']; let returnType = null; return this.apiClient.callApi( -- 2.30.2 From 218e476e78103c3923e5f8d034e4698855b713e6 Mon Sep 17 00:00:00 2001 From: Nitin Rawat Date: Tue, 7 Mar 2023 11:19:18 +0530 Subject: [PATCH 7/8] Implemented the functionality to calcualte progress now we are able to calculate progress based on the number of frames rendered on worker and function to handle progressUpdate request from worker is also implemented. --- internal/manager/api_impl/workers.go | 52 ++++++++++++++++++- internal/worker/command_blender.go | 26 ++++++++++ internal/worker/command_exe.go | 5 +- internal/worker/listener.go | 27 ++++++++++ internal/worker/mocks/command_listener.gen.go | 14 +++++ 5 files changed, 121 insertions(+), 3 deletions(-) diff --git a/internal/manager/api_impl/workers.go b/internal/manager/api_impl/workers.go index fe4b62cf..2c740886 100644 --- a/internal/manager/api_impl/workers.go +++ b/internal/manager/api_impl/workers.go @@ -369,9 +369,57 @@ func (f *Flamenco) ScheduleTask(e echo.Context) error { return e.JSON(http.StatusOK, customisedTask) } -// TODO: Complete the immplementation for the below function +// 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 { - return fmt.Errorf("") + 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() diff --git a/internal/worker/command_blender.go b/internal/worker/command_blender.go index 2018ee07..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. @@ -47,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) @@ -141,6 +148,9 @@ func cmdBlenderRenderParams(logger zerolog.Logger, cmd api.Command) (BlenderPara // 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") @@ -174,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 8d7fd3a2..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. @@ -47,8 +49,9 @@ type TimeService interface { Now() time.Time } -//go:generate go run github.com/golang/mock/mockgen -destination mocks/cli_runner.gen.go -package mocks git.blender.org/flamenco/internal/worker CommandLineRunner // CommandLineRunner is an interface around exec.CommandContext(). +// +//go:generate go run github.com/golang/mock/mockgen -destination mocks/cli_runner.gen.go -package mocks git.blender.org/flamenco/internal/worker CommandLineRunner type CommandLineRunner interface { CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd RunWithTextOutput( 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/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) +} -- 2.30.2 From a25f82ea96ceb2b74feaddd54de7d9e3dc017966 Mon Sep 17 00:00:00 2001 From: Nitin Rawat Date: Sat, 15 Apr 2023 14:49:40 +0530 Subject: [PATCH 8/8] Undo changes caused by auto-formatting. --- internal/worker/command_exe.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/worker/command_exe.go b/internal/worker/command_exe.go index 6c36973e..0768f70b 100644 --- a/internal/worker/command_exe.go +++ b/internal/worker/command_exe.go @@ -49,9 +49,8 @@ type TimeService interface { Now() time.Time } -// CommandLineRunner is an interface around exec.CommandContext(). -// //go:generate go run github.com/golang/mock/mockgen -destination mocks/cli_runner.gen.go -package mocks git.blender.org/flamenco/internal/worker CommandLineRunner +// CommandLineRunner is an interface around exec.CommandContext(). type CommandLineRunner interface { CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd RunWithTextOutput( -- 2.30.2