diff --git a/.github/workflows/start-minor-release.yml b/.github/workflows/start-minor-release.yml new file mode 100644 index 00000000000..af06d7ece7d --- /dev/null +++ b/.github/workflows/start-minor-release.yml @@ -0,0 +1,62 @@ +--- +name: "Start minor release" + +on: # yamllint disable-line rule:truthy + workflow_dispatch: + inputs: + version: + description: "The Determined minor version to release. E.g. 0.38.0. This will create a new release branch and make commits on main." + required: true + +jobs: + start-minor-release: + name: "Start minor release" + env: + GH_TOKEN: ${{ secrets.DETERMINED_TOKEN }} + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: "Validate version" + shell: bash {0} + run: | + grep -E -o '[0-9]+\.[0-9]+\.0' <<<'${{ github.event.inputs.version }}' + + ret=$? + if [[ $ret != 0 ]]; then + echo '::error::Version string must match <[0-9]+\.[0-9]+\.0>. Got: <${{ github.event.inputs.version }}>' + exit $ret + fi + + - name: Configure git username and e-mail" + run: | + git config user.name github-actions + git config user.email \ + 41898282+github-actions[bot]@users.noreply.github.com + + - name: "Setup Go" + uses: actions/setup-go@v5 + with: + go-version: "1.22.0" + + - name: "Install protobuf dependencies" + run: "make get-deps-proto" + + - name: "Create release branch" + run: | + echo 'Creating branch: release-${{ github.event.inputs.version }}' + git checkout -b release-${{ github.event.inputs.version }} + + echo 'Pushing release branch' + git push -u origin release-${{ github.event.inputs.version }} + + - name: "Switch back to main" + run: "git checkout main" + + - name: "Publish changes to main" + run: | + ./tools/scripts/lock-api-state.sh + ./tools/scripts/lock-published-urls.sh + git push origin main diff --git a/docs/reference/experiment-config-reference.rst b/docs/reference/experiment-config-reference.rst index 4374e0e399a..5ba662831e6 100644 --- a/docs/reference/experiment-config-reference.rst +++ b/docs/reference/experiment-config-reference.rst @@ -308,38 +308,65 @@ Optional. Defines actions and labels in response to trial logs matching specifie language syntax). For more information about the syntax, you can visit this `RE2 reference page `__. Each log policy can have the following fields: -- ``name``: Optional. A name for the log policy. If provided, this name will be displayed as a - label in the UI when the log policy matches. +- ``name``: Required. The name of the log policy, displayed as a label in the WebUI when a log + policy match occurs. -- ``pattern``: Required. The regex pattern to match in the logs. +- ``pattern``: Optional. Defines a regex pattern to match log entries. If not specified, this + policy is disabled. - ``action``: Optional. The action to take when the pattern is matched. Actions include: - - ``exclude_node``: Excludes a failed trial's restart attempts from being scheduled on nodes - with matching error logs. - - ``cancel_retries``: Prevents a trial from restarting if it reports a matching log. + - ``exclude_node``: Excludes a failed trial's restart attempts (due to its ``max_restarts`` + policy) from being scheduled on nodes with matched error logs. This is useful for bypassing + nodes with hardware issues, such as uncorrectable GPU ECC errors. + + .. note:: + + This option is not supported on PBS systems. + + For the agent resource manager, if a trial becomes unschedulable due to enough node + exclusions, and ``launch_error`` in the master config is set to true (default), the trial will + fail. + + - ``cancel_retries``: Prevents a trial from restarting if a log matches the pattern, even if the + trial has remaining max_restarts. This avoids using resources retrying a trial that encounters + failures unlikely to be resolved by retrying, such as CUDA memory issues. Example configuration: .. code:: yaml log_policies: - - name: "ECC Error" - pattern: ".*uncorrectable ECC error encountered.*" - action: - type: exclude_node - - name: "CUDA OOM" - pattern: ".*CUDA out of memory.*" - action: - type: cancel_retries - -When a log policy matches, its name (if provided) will be displayed as a label in the WebUI, -allowing for easy identification of specific issues or events during a run. + - name: ECC Error + pattern: ".*uncorrectable ECC error encountered.*" + action: exclude_node + - name: CUDA OOM + pattern: ".*CUDA out of memory.*" + action: cancel_retries + +When a log policy matches, its name appears as a label in the WebUI, making it easy to identify +specific issues during a run. These labels are shown in both the run table and run detail views. These settings may also be specified at the cluster or resource pool level through task container defaults. -To find out more about log management, visit :ref:`Log Management `. +Default policies: + +.. code:: yaml + + log_policies: + - name: CUDA OOM + pattern: ".*CUDA out of memory.*" + - name: ECC Error + pattern: ".*uncorrectable ECC error encountered.*" + +To disable showing labels from the default policies: + +.. code:: yaml + + log_policies: + - name: CUDA OOM + - name: ECC Error .. _log-retention-days: diff --git a/docs/release-notes/log-search-improvement.rst b/docs/release-notes/log-search-improvement.rst index c24773c49ec..5ce8d46fff1 100644 --- a/docs/release-notes/log-search-improvement.rst +++ b/docs/release-notes/log-search-improvement.rst @@ -6,4 +6,4 @@ search result will take users directly to the relevant position in the log, allowing them to easily view logs both before and after the matched entry. Additionally, add support for regex-based searches, providing more flexible log filtering. For more details, refer to - :ref:`log_policies `. + :ref:`WebUI `. diff --git a/docs/release-notes/log-signal.rst b/docs/release-notes/log-signal.rst new file mode 100644 index 00000000000..743b0c6c56b --- /dev/null +++ b/docs/release-notes/log-signal.rst @@ -0,0 +1,10 @@ +:orphan: + +**New Features** + +- Experiments: Add a ``name`` field to ``log_policies``. When a log policy matches, its name shows + as a label in the WebUI, making it easy to spot specific issues during a run. Labels appear in + both the run table and run detail views. + + In addition, there is a new format: ``name`` is required, and ``action`` is now a plain string. + For more details, refer to :ref:`log_policies `. diff --git a/docs/release-notes/unsupport-aurora-postgres-reminder.rst b/docs/release-notes/unsupport-aurora-postgres-reminder.rst new file mode 100644 index 00000000000..f028a726e00 --- /dev/null +++ b/docs/release-notes/unsupport-aurora-postgres-reminder.rst @@ -0,0 +1,19 @@ +:orphan: + +**Deprecations** + +- Cluster: A reminder that Amazon Aurora V1 will reach End of Life at the end of 2024. It is no + longer supported as the default persistent storage for AWS Determined deployments. We recommend + that users migrate to Amazon RDS for PostgreSQL. For more information, visit the `migration + instructions `_. + +- Cluster: After Amazon Aurora V1 reaches End of Life, support for Amazon Aurora V1 in ``det deploy + aws`` will be removed. Future deployments will default to the ``simple-rds`` type, which uses + Amazon RDS for PostgreSQL. Changes to the deployment code will ensure this transition to the new + default. + +- Database: As a follow-up to the earlier notice, PostgreSQL 12 will reach End of Life on November + 14, 2024. Instances still using PostgreSQL 12 or earlier should upgrade to PostgreSQL 13 or later + to maintain compatibility. The application will log a warning if it detects a connection to any + PostgreSQL version older than 12, and this warning will be updated to include PostgreSQL 12 once + it is End of Life. diff --git a/docs/setup-cluster/aws/aws-spot.rst b/docs/setup-cluster/aws/aws-spot.rst index 6ab280fc7aa..da329bc8103 100644 --- a/docs/setup-cluster/aws/aws-spot.rst +++ b/docs/setup-cluster/aws/aws-spot.rst @@ -4,7 +4,7 @@ Use Spot Instances #################### -This document describes how to use AWS spot instances with Determined. Spot instances can be much +This guide describes how to use AWS spot instances with Determined. Spot instances can be much cheaper than on-demand instances (up to 90% cheaper, but more often 70-80%) but they are unreliable, so software that runs on spot instances must be fault tolerant. Unfortunately, deep learning code is often not written with fault tolerance in mind, preventing many practitioners from using spot diff --git a/docs/setup-cluster/aws/install-on-aws.rst b/docs/setup-cluster/aws/install-on-aws.rst index d8bc7038756..74c1f92da84 100644 --- a/docs/setup-cluster/aws/install-on-aws.rst +++ b/docs/setup-cluster/aws/install-on-aws.rst @@ -23,12 +23,18 @@ CloudFormation `__ to automatically depl Determined cluster. CloudFormation builds the necessary components for Determined into a single CloudFormation stack. +.. important:: + + **Amazon Aurora V1 is no longer supported**. We recommend migrating to **Amazon RDS for + PostgreSQL 14**. Deployments using ``det deploy aws`` will default to the ``simple-rds`` + deployment type, which uses Amazon RDS. + Requirements ============ -- Either AWS credentials or an IAM role with permissions to access AWS CloudFormation APIs. See the - `AWS Documentation `__ - for information on how to use AWS credentials. +- AWS credentials or an IAM role with permissions to access AWS CloudFormation APIs. See the `AWS + Documentation `__ for + information on how to use AWS credentials. - An `AWS EC2 Keypair `__. diff --git a/docs/setup-cluster/checklists/postgresql.rst b/docs/setup-cluster/checklists/postgresql.rst index cde700b0509..67d209e8cd6 100644 --- a/docs/setup-cluster/checklists/postgresql.rst +++ b/docs/setup-cluster/checklists/postgresql.rst @@ -6,6 +6,10 @@ Determined uses a PostgreSQL database to store experiment and trial metadata. +.. note:: + + We recommend installing the latest available version of PostgreSQL. + .. note:: If you are using an existing PostgreSQL installation, we recommend confirming that @@ -22,7 +26,7 @@ Determined uses a PostgreSQL database to store experiment and trial metadata. GPUs, ensure that the :ref:`NVIDIA Container Toolkit ` on each one is working as expected. -#. Pull the official Docker image for PostgreSQL. PostgreSQL version 10 and later is supported. +#. Pull the official Docker image for the latest PostgreSQL version. .. code:: @@ -75,7 +79,7 @@ Determined uses a PostgreSQL database to store experiment and trial metadata. Install PostgreSQL using ``apt`` or ``yum`` =========================================== -#. Install PostgreSQL 10 or greater. +#. Install PostgreSQL. **Debian Distributions** @@ -83,13 +87,13 @@ Install PostgreSQL using ``apt`` or ``yum`` .. code:: - sudo apt install postgresql-10 + sudo apt install postgresql **Red Hat Distributions** On Red Hat distributions, you'll need to configure the PostgreSQL yum repository as described in the `Red Hat Linux documentation `_. Then, - install version 10: + install PostgreSQL: .. code:: diff --git a/docs/setup-cluster/on-prem/options/docker.rst b/docs/setup-cluster/on-prem/options/docker.rst index 2ecf417d02a..4e612cec486 100644 --- a/docs/setup-cluster/on-prem/options/docker.rst +++ b/docs/setup-cluster/on-prem/options/docker.rst @@ -14,7 +14,7 @@ This user guide provides step-by-step instructions for installing Determined usi GPUs, ensure that the :ref:`NVIDIA Container Toolkit ` on each one is working as expected. -#. Pull the official Docker image for PostgreSQL. PostgreSQL version 10 and later is supported. +#. Pull the official Docker image for the latest PostgreSQL version. .. code:: diff --git a/docs/setup-cluster/on-prem/options/linux-packages.rst b/docs/setup-cluster/on-prem/options/linux-packages.rst index 1194c11ad93..62a6e5eb9af 100644 --- a/docs/setup-cluster/on-prem/options/linux-packages.rst +++ b/docs/setup-cluster/on-prem/options/linux-packages.rst @@ -34,7 +34,7 @@ Docker container or your Linux distribution's package and service. Run PostgreSQL in Docker ------------------------ -#. Pull the official Docker image for PostgreSQL. PostgreSQL version 10 and later is supported. +#. Pull the official Docker image for the latest PostgreSQL version. .. code:: @@ -65,7 +65,7 @@ Run PostgreSQL in Docker Install PostgreSQL using ``apt`` or ``yum`` ------------------------------------------- -#. Install PostgreSQL. Version 10 and later is supported. +#. Install PostgreSQL. **Debian Distributions** diff --git a/docs/setup-cluster/slurm/hpc-environment-requirements.rst b/docs/setup-cluster/slurm/hpc-environment-requirements.rst index f28d644fa5c..f7b7a46bde5 100644 --- a/docs/setup-cluster/slurm/hpc-environment-requirements.rst +++ b/docs/setup-cluster/slurm/hpc-environment-requirements.rst @@ -91,7 +91,7 @@ The following software components are required: | Podman | >= 4.0.0 | Admin | +------------------------+----------------------------------+------------------+ | PostgreSQL | 10 (RHEL 8), 13 (RHEL 9), 14 | Admin | -| | (Ubuntu 22.04) or newer | | +| | (Ubuntu 22.04) or later | | +------------------------+----------------------------------+------------------+ | HPC client packages | Same as login nodes | Admin | +------------------------+----------------------------------+------------------+ @@ -109,7 +109,8 @@ The following software components are required: Database Requirements ===================== -The solution requires PostgreSQL 10 or newer, which will be installed on the admin node. The +The solution requires PostgreSQL 13 or later, which will be installed on the admin node. We +recommend using the latest available version of PostgreSQL for optimal support and security. The required disk space for the database is estimated as follows: - 200 GB on small systems (less than 15 workers) or big systems if the experiment logs are sent to diff --git a/docs/tools/webui-if.rst b/docs/tools/webui-if.rst index 733180a123e..5ab5187cc33 100644 --- a/docs/tools/webui-if.rst +++ b/docs/tools/webui-if.rst @@ -241,3 +241,17 @@ Clear the message with the following command: .. code:: bash det master cluster-message clear + +**************************** + Viewing Log Search Results +**************************** + +To perform a log search: + +#. Navigate to your run in the WebUI. +#. In the Logs tab, start typing in the search box to open the search pane. +#. To use regex search, click the "Regex" checkbox in the search pane. +#. Click on a search result to view it in context, with logs before and after visible. +#. Scroll up and down to fetch new logs. + +Note: Search results are not auto-updating. You may need to refresh to see new logs. diff --git a/docs/tutorials/_index.rst b/docs/tutorials/_index.rst index 724736f35c6..ab695aecfce 100644 --- a/docs/tutorials/_index.rst +++ b/docs/tutorials/_index.rst @@ -46,7 +46,6 @@ Examples let you build off of an existing model that already runs on Determined. :hidden: Quickstart for Model Developers - Managing Logs and Log Policies Get Started with Detached Mode Viewing Epoch-Based Metrics in the WebUI Using Pachyderm to Create a Batch Inferencing Pipeline diff --git a/docs/tutorials/log-management.rst b/docs/tutorials/log-management.rst deleted file mode 100644 index 3eb95cfea69..00000000000 --- a/docs/tutorials/log-management.rst +++ /dev/null @@ -1,52 +0,0 @@ -.. _log-management: - -################ - Log Management -################ - -This guide covers two log management features: Log Search and Log Signal. - -************ - Log Search -************ - -To perform a log search: - -#. Navigate to your run in the WebUI. -#. In the Logs tab, start typing in the search box to open the search pane. -#. To use regex search, click the "Regex" checkbox in the search pane. -#. Click on a search result to view it in context, with logs before and after visible. -#. Scroll up and down to fetch new logs. - -Note: Search results are not auto-updating. You may need to refresh to see new logs. - -************ - Log Signal -************ - -Log Signal allows you to configure log policies in the master configuration to display labels in the -UI when specific patterns are matched in the logs. - -To set up a log policy: - -#. In the master configuration file, under ``task_container_defaults > log_policies``, define your - log policies. -#. Each policy can have a ``name``, ``pattern``, and ``action``. -#. When a log matching the pattern is encountered, the ``name`` will be displayed as a label in the - run table and run detail views. - -Example configuration: - -.. code:: yaml - - log_policies: - - name: "CUDA OOM" - pattern: ".*CUDA out of memory.*" - action: - type: cancel_retries - -This will display a "CUDA OOM" label in the UI when a CUDA out of memory error is encountered in the -logs. - -For more detailed information on configuring log policies, refer to the :ref:`experiment -configuration reference `. diff --git a/e2e_tests/tests/cluster/test_log_policies.py b/e2e_tests/tests/cluster/test_log_policies.py index a810bc43d59..a3abe1087cc 100644 --- a/e2e_tests/tests/cluster/test_log_policies.py +++ b/e2e_tests/tests/cluster/test_log_policies.py @@ -4,6 +4,7 @@ from determined.experimental import client from tests import api_utils from tests import experiment as exp +from tests.cluster import utils from tests.experiment import noop @@ -152,3 +153,33 @@ def test_log_policy_exclude_slurm(should_match: bool) -> None: ) # Job fails to start up the second restart since all nodes are excluded. else: assert times_ran == 2 + + +@pytest.mark.e2e_cpu +@pytest.mark.parametrize("should_match", [True, False]) +def test_log_policy_matched(should_match: bool) -> None: + sess = api_utils.user_session() + regex = r"executing.*action.*exit.*code.*7" + if not should_match: + regex = r"(.*) this should not match (.*)" + + expected_policy = "Test" + config = { + "log_policies": [{"name": expected_policy, "pattern": regex}], + } + + exp_ref = noop.create_experiment(sess, [noop.Exit(7)], config=config) + assert exp_ref.wait(interval=0.01) == client.ExperimentState.ERROR + + searchRes = utils.get_run_by_exp_id(sess, exp_ref.id) + runPolicyMatched = searchRes.runs[0].logPolicyMatched + + trialRes = bindings.get_GetTrial(sess, trialId=searchRes.runs[0].id) + trialPolicyMatched = trialRes.trial.logPolicyMatched + + if should_match: + assert runPolicyMatched == expected_policy + assert trialPolicyMatched == expected_policy + else: + assert runPolicyMatched is None + assert trialPolicyMatched is None diff --git a/e2e_tests/tests/cluster/utils.py b/e2e_tests/tests/cluster/utils.py index de38408e182..9bef663f01f 100644 --- a/e2e_tests/tests/cluster/utils.py +++ b/e2e_tests/tests/cluster/utils.py @@ -11,6 +11,7 @@ from typing_extensions import Literal # noqa:I2041 from determined.common import api +from determined.common.api import bindings from tests import command from tests import config as conf from tests import detproc @@ -197,3 +198,30 @@ def set_master_port(config: str) -> None: lc = conf.load_config(config_path=config) port = get_master_port(lc) conf.MASTER_PORT = port + + +def get_run_by_exp_id(sess: api.Session, exp_id: int) -> bindings.v1SearchRunsResponse: + return bindings.post_SearchRuns( + sess, + body=bindings.v1SearchRunsRequest( + limit=1, + filter="""{ + "filterGroup": { + "children": [ + { + "columnName": "experimentId", + "kind": "field", + "location": "LOCATION_TYPE_RUN", + "operator": "=", + "type": "COLUMN_TYPE_NUMBER", + "value": %s + } + ], + "conjunction": "and", + "kind": "group" + }, + "showArchived": false + }""" + % exp_id, + ), + ) diff --git a/harness/determined/common/api/bindings.py b/harness/determined/common/api/bindings.py index 9121c2a8937..662ce773fc2 100644 --- a/harness/determined/common/api/bindings.py +++ b/harness/determined/common/api/bindings.py @@ -726,8 +726,8 @@ class trialv1Trial(Printable): checkpointCount: "typing.Optional[int]" = None endTime: "typing.Optional[str]" = None latestValidation: "typing.Optional[v1MetricsWorkload]" = None + logPolicyMatched: "typing.Optional[str]" = None logRetentionDays: "typing.Optional[int]" = None - logSignal: "typing.Optional[str]" = None metadata: "typing.Optional[typing.Dict[str, typing.Any]]" = None runnerState: "typing.Optional[str]" = None searcherMetricValue: "typing.Optional[float]" = None @@ -753,8 +753,8 @@ def __init__( checkpointCount: "typing.Union[int, None, Unset]" = _unset, endTime: "typing.Union[str, None, Unset]" = _unset, latestValidation: "typing.Union[v1MetricsWorkload, None, Unset]" = _unset, + logPolicyMatched: "typing.Union[str, None, Unset]" = _unset, logRetentionDays: "typing.Union[int, None, Unset]" = _unset, - logSignal: "typing.Union[str, None, Unset]" = _unset, metadata: "typing.Union[typing.Dict[str, typing.Any], None, Unset]" = _unset, runnerState: "typing.Union[str, None, Unset]" = _unset, searcherMetricValue: "typing.Union[float, None, Unset]" = _unset, @@ -782,10 +782,10 @@ def __init__( self.endTime = endTime if not isinstance(latestValidation, Unset): self.latestValidation = latestValidation + if not isinstance(logPolicyMatched, Unset): + self.logPolicyMatched = logPolicyMatched if not isinstance(logRetentionDays, Unset): self.logRetentionDays = logRetentionDays - if not isinstance(logSignal, Unset): - self.logSignal = logSignal if not isinstance(metadata, Unset): self.metadata = metadata if not isinstance(runnerState, Unset): @@ -826,10 +826,10 @@ def from_json(cls, obj: Json) -> "trialv1Trial": kwargs["endTime"] = obj["endTime"] if "latestValidation" in obj: kwargs["latestValidation"] = v1MetricsWorkload.from_json(obj["latestValidation"]) if obj["latestValidation"] is not None else None + if "logPolicyMatched" in obj: + kwargs["logPolicyMatched"] = obj["logPolicyMatched"] if "logRetentionDays" in obj: kwargs["logRetentionDays"] = obj["logRetentionDays"] - if "logSignal" in obj: - kwargs["logSignal"] = obj["logSignal"] if "metadata" in obj: kwargs["metadata"] = obj["metadata"] if "runnerState" in obj: @@ -870,10 +870,10 @@ def to_json(self, omit_unset: bool = False) -> typing.Dict[str, typing.Any]: out["endTime"] = self.endTime if not omit_unset or "latestValidation" in vars(self): out["latestValidation"] = None if self.latestValidation is None else self.latestValidation.to_json(omit_unset) + if not omit_unset or "logPolicyMatched" in vars(self): + out["logPolicyMatched"] = self.logPolicyMatched if not omit_unset or "logRetentionDays" in vars(self): out["logRetentionDays"] = self.logRetentionDays - if not omit_unset or "logSignal" in vars(self): - out["logSignal"] = self.logSignal if not omit_unset or "metadata" in vars(self): out["metadata"] = self.metadata if not omit_unset or "runnerState" in vars(self): @@ -4589,7 +4589,7 @@ class v1FlatRun(Printable): hyperparameters: "typing.Optional[typing.Dict[str, typing.Any]]" = None labels: "typing.Optional[typing.Sequence[str]]" = None localId: "typing.Optional[str]" = None - logSignal: "typing.Optional[str]" = None + logPolicyMatched: "typing.Optional[str]" = None metadata: "typing.Optional[typing.Dict[str, typing.Any]]" = None searcherMetricValue: "typing.Optional[float]" = None summaryMetrics: "typing.Optional[typing.Dict[str, typing.Any]]" = None @@ -4616,7 +4616,7 @@ def __init__( hyperparameters: "typing.Union[typing.Dict[str, typing.Any], None, Unset]" = _unset, labels: "typing.Union[typing.Sequence[str], None, Unset]" = _unset, localId: "typing.Union[str, None, Unset]" = _unset, - logSignal: "typing.Union[str, None, Unset]" = _unset, + logPolicyMatched: "typing.Union[str, None, Unset]" = _unset, metadata: "typing.Union[typing.Dict[str, typing.Any], None, Unset]" = _unset, searcherMetricValue: "typing.Union[float, None, Unset]" = _unset, summaryMetrics: "typing.Union[typing.Dict[str, typing.Any], None, Unset]" = _unset, @@ -4647,8 +4647,8 @@ def __init__( self.labels = labels if not isinstance(localId, Unset): self.localId = localId - if not isinstance(logSignal, Unset): - self.logSignal = logSignal + if not isinstance(logPolicyMatched, Unset): + self.logPolicyMatched = logPolicyMatched if not isinstance(metadata, Unset): self.metadata = metadata if not isinstance(searcherMetricValue, Unset): @@ -4687,8 +4687,8 @@ def from_json(cls, obj: Json) -> "v1FlatRun": kwargs["labels"] = obj["labels"] if "localId" in obj: kwargs["localId"] = obj["localId"] - if "logSignal" in obj: - kwargs["logSignal"] = obj["logSignal"] + if "logPolicyMatched" in obj: + kwargs["logPolicyMatched"] = obj["logPolicyMatched"] if "metadata" in obj: kwargs["metadata"] = obj["metadata"] if "searcherMetricValue" in obj: @@ -4727,8 +4727,8 @@ def to_json(self, omit_unset: bool = False) -> typing.Dict[str, typing.Any]: out["labels"] = self.labels if not omit_unset or "localId" in vars(self): out["localId"] = self.localId - if not omit_unset or "logSignal" in vars(self): - out["logSignal"] = self.logSignal + if not omit_unset or "logPolicyMatched" in vars(self): + out["logPolicyMatched"] = self.logPolicyMatched if not omit_unset or "metadata" in vars(self): out["metadata"] = self.metadata if not omit_unset or "searcherMetricValue" in vars(self): diff --git a/master/internal/api_experiment.go b/master/internal/api_experiment.go index 9fe3daa6c81..ffd375c48ef 100644 --- a/master/internal/api_experiment.go +++ b/master/internal/api_experiment.go @@ -2741,7 +2741,7 @@ func (a *apiServer) SearchExperiments( Column("searcher_metric_value"). Column("trials.external_trial_id"). ColumnExpr("nullif(trials.metadata, 'null') as metadata"). - ColumnExpr("NULL as log_signal"). + ColumnExpr("NULL as log_policy_matched"). Join("LEFT JOIN validations bv ON trials.best_validation_id = bv.id"). Join("LEFT JOIN validations lv ON trials.latest_validation_id = lv.id"). Join("LEFT JOIN checkpoints_v2 new_ckpt ON new_ckpt.id = trials.warm_start_checkpoint_id"). diff --git a/master/internal/api_runs.go b/master/internal/api_runs.go index 7f6322b2958..807540947fc 100644 --- a/master/internal/api_runs.go +++ b/master/internal/api_runs.go @@ -170,7 +170,7 @@ func getRunsColumns(q *bun.SelectQuery) *bun.SelectQuery { 'pachyderm_integration', NULLIF(e.config#>'{integrations,pachyderm}', 'null'), 'id', e.id) AS experiment`). ColumnExpr("rm.metadata AS metadata"). - ColumnExpr("r.log_signal AS log_signal"). + ColumnExpr("r.log_policy_matched AS log_policy_matched"). Join("LEFT JOIN experiments AS e ON r.experiment_id=e.id"). Join("LEFT JOIN runs_metadata AS rm ON r.id=rm.run_id"). Join("LEFT JOIN users u ON e.owner_id = u.id"). diff --git a/master/internal/api_tasks_intg_test.go b/master/internal/api_tasks_intg_test.go index 843762caf57..157d95edf4d 100644 --- a/master/internal/api_tasks_intg_test.go +++ b/master/internal/api_tasks_intg_test.go @@ -219,12 +219,14 @@ func TestPostTaskLogsLogPattern(t *testing.T) { activeConfig, err := api.m.db.ActiveExperimentConfig(trial.ExperimentID) require.NoError(t, err) activeConfig.RawLogPolicies = expconf.LogPoliciesConfig{ - expconf.LogPolicy{RawPattern: "sub", RawAction: expconf.LogAction{ - RawCancelRetries: &expconf.LogActionCancelRetries{}, - }}, - expconf.LogPolicy{RawPattern: `\d{5}$`, RawAction: expconf.LogAction{ - RawExcludeNode: &expconf.LogActionExcludeNode{}, - }}, + expconf.LogPolicy{ + RawPattern: ptrs.Ptr("sub"), + RawAction: &expconf.LogActionV0{Type: expconf.LogActionTypeCancelRetries}, + }, + expconf.LogPolicy{ + RawPattern: ptrs.Ptr(`\d{5}$`), + RawAction: &expconf.LogActionV0{Type: expconf.LogActionTypeExcludeNode}, + }, } v, err := json.Marshal(activeConfig) @@ -443,3 +445,74 @@ func TestGetAllocationAcceleratorData(t *testing.T) { require.Equal(t, resp.AcceleratorData[0].ResourcePool, a1.ResourcePool, "failed to get the correct allocation's resource pool data") } + +func TestPostTaskLogsLogSignalDataSaving(t *testing.T) { + api, curUser, ctx := setupAPITest(t, nil) + trial, task := createTestTrial(t, api, curUser) + + activeConfig, err := api.m.db.ActiveExperimentConfig(trial.ExperimentID) + require.NoError(t, err) + + activeConfig.RawLogPolicies = expconf.LogPoliciesConfig{ + expconf.LogPolicy{ + RawName: ptrs.Ptr("test"), + RawPattern: ptrs.Ptr("sub"), + }, + } + + v, err := json.Marshal(activeConfig) + require.NoError(t, err) + + var m map[string]any + require.NoError(t, json.Unmarshal(v, &m)) + + _, err = db.Bun().NewUpdate().Table("experiments"). + Where("id = ?", trial.ExperimentID). + Set("config = ?", m). + Exec(ctx) + require.NoError(t, err) + + _, err = api.PostTaskLogs(ctx, &apiv1.PostTaskLogsRequest{ + Logs: []*taskv1.TaskLog{ + { + TaskId: string(task.TaskID), + AgentId: ptrs.Ptr("a1"), + Log: "stringsubstring", + }, + { + TaskId: string(task.TaskID), + AgentId: ptrs.Ptr("a1"), + Log: "12345", + }, + }, + }) + require.NoError(t, err) + + runsOut := struct { + bun.BaseModel `bun:"table:runs"` + LogPolicyMatched *string `db:"log_policy_matched"` + }{} + + err = db.Bun().NewSelect().Model(&runsOut). + Where("id = ?", trial.ID). + Scan(ctx) + require.NoError(t, err) + require.NotNil(t, runsOut) + require.NotNil(t, runsOut.LogPolicyMatched) + + require.Equal(t, "test", *runsOut.LogPolicyMatched) + + tasksOut := struct { + bun.BaseModel `bun:"table:tasks"` + LogPolicyMatched *string `db:"log_policy_matched"` + }{} + err = db.Bun().NewSelect().Model(&tasksOut). + Join("LEFT JOIN run_id_task_id AS rt on tasks.task_id = rt.task_id"). + Where("run_id = ?", trial.ID). + Scan(ctx) + require.NoError(t, err) + require.NotNil(t, tasksOut) + require.NotNil(t, tasksOut.LogPolicyMatched) + + require.Equal(t, "test", *tasksOut.LogPolicyMatched) +} diff --git a/master/internal/api_token.go b/master/internal/api_token.go index bf014048186..5453a55b223 100644 --- a/master/internal/api_token.go +++ b/master/internal/api_token.go @@ -12,16 +12,26 @@ import ( "github.com/determined-ai/determined/master/internal/api" "github.com/determined-ai/determined/master/internal/db" "github.com/determined-ai/determined/master/internal/grpcutil" + "github.com/determined-ai/determined/master/internal/license" "github.com/determined-ai/determined/master/internal/token" "github.com/determined-ai/determined/master/pkg/model" "github.com/determined-ai/determined/proto/pkg/apiv1" ) +var errAccessTokenRequiresEE = status.Error( + codes.FailedPrecondition, + "users cannot log in with an access token without a valid Enterprise Edition license set up.", +) + // PostAccessToken takes user id and optional lifespan, description and creates an // access token for the given user. func (a *apiServer) PostAccessToken( ctx context.Context, req *apiv1.PostAccessTokenRequest, ) (*apiv1.PostAccessTokenResponse, error) { + if !license.IsEE() { + return nil, errAccessTokenRequiresEE + } + curUser, _, err := grpcutil.GetUser(ctx) if err != nil { return nil, err @@ -58,6 +68,10 @@ func (a *apiServer) PostAccessToken( func (a *apiServer) GetAccessTokens( ctx context.Context, req *apiv1.GetAccessTokensRequest, ) (*apiv1.GetAccessTokensResponse, error) { + if !license.IsEE() { + return nil, errAccessTokenRequiresEE + } + curUser, _, err := grpcutil.GetUser(ctx) if err != nil { return nil, err @@ -161,6 +175,10 @@ func (a *apiServer) GetAccessTokens( func (a *apiServer) PatchAccessToken( ctx context.Context, req *apiv1.PatchAccessTokenRequest, ) (*apiv1.PatchAccessTokenResponse, error) { + if !license.IsEE() { + return nil, errAccessTokenRequiresEE + } + curUser, _, err := grpcutil.GetUser(ctx) if err != nil { return nil, err diff --git a/master/internal/api_user_intg_test.go b/master/internal/api_user_intg_test.go index 3e55576a552..1a34b80e97d 100644 --- a/master/internal/api_user_intg_test.go +++ b/master/internal/api_user_intg_test.go @@ -29,6 +29,7 @@ import ( "github.com/determined-ai/determined/master/internal/job/jobservice" "github.com/determined-ai/determined/master/internal/logpattern" "github.com/determined-ai/determined/master/internal/mocks" + "github.com/determined-ai/determined/master/internal/rbac" "github.com/determined-ai/determined/master/internal/rm" "github.com/determined-ai/determined/master/internal/rm/rmevents" "github.com/determined-ai/determined/master/internal/sproto" @@ -38,6 +39,7 @@ import ( "github.com/determined-ai/determined/master/pkg/ptrs" "github.com/determined-ai/determined/master/pkg/tasks" "github.com/determined-ai/determined/proto/pkg/apiv1" + "github.com/determined-ai/determined/proto/pkg/rbacv1" "github.com/determined-ai/determined/proto/pkg/userv1" ) @@ -925,6 +927,62 @@ func TestPostUserActivity(t *testing.T) { require.Equal(t, 1, activityCount, ctx) } +func TestFilterUserByRoleId(t *testing.T) { + api, _, ctx := setupAPITest(t, nil) + config.GetMasterConfig().Security.AuthZ = config.AuthZConfig{Type: "rbac"} + defer func() { + config.GetMasterConfig().Security.AuthZ = config.AuthZConfig{Type: "basic"} + }() + testRole := rbac.Role{ + ID: 10002, + Name: uuid.New().String(), + } + _, err := db.Bun().NewInsert().Model(&testRole).Exec(ctx) + require.NoError(t, err, "failure creating role in setup") + + user0 := model.User{Username: uuid.New().String()} + _, err = db.HackAddUser(context.TODO(), &user0) + require.NoError(t, err) + user1 := model.User{Username: uuid.New().String()} + _, err = db.HackAddUser(context.TODO(), &user1) + require.NoError(t, err) + + err = rbac.AddRoleAssignments(ctx, nil, + []*rbacv1.UserRoleAssignment{ + { + UserId: int32(user0.ID), + RoleAssignment: &rbacv1.RoleAssignment{ + Role: &rbacv1.Role{ + RoleId: int32(testRole.ID), + }, + ScopeWorkspaceId: nil, + }, + }, + { + UserId: int32(user1.ID), + RoleAssignment: &rbacv1.RoleAssignment{ + Role: &rbacv1.Role{ + RoleId: int32(testRole.ID), + }, + ScopeWorkspaceId: ptrs.Ptr(int32(1)), + }, + }, + }, + ) + require.NoError(t, err, "error adding role assignment") + resp, err := api.GetUsers(ctx, &apiv1.GetUsersRequest{ + RoleIdAssignedDirectlyToUser: []int32{int32(testRole.ID)}, + }) + require.NoError(t, err) + require.Len(t, resp.Users, 1) + require.Equal(t, int32(user0.ID), resp.Users[0].Id) + + _, err = db.Bun().NewDelete().Table("roles").Where("id = ?", testRole.ID).Exec(ctx) + if err != nil { + t.Logf("Error cleaning up role") + } +} + func getActivityEntry(ctx context.Context, userID model.UserID, entityID int32) (int, error) { return db.Bun().NewSelect().Model((*model.UserActivity)(nil)).Where("user_id = ?", int32(userID)).Where("entity_id = ?", entityID).Count(ctx) diff --git a/master/internal/configpolicy/postgres_task_config_policy.go b/master/internal/configpolicy/postgres_task_config_policy.go index dea01bf7e42..fcc78c75864 100644 --- a/master/internal/configpolicy/postgres_task_config_policy.go +++ b/master/internal/configpolicy/postgres_task_config_policy.go @@ -19,12 +19,7 @@ const ( // DefaultInvariantConfigStr is the default invariant config val used for tests. DefaultInvariantConfigStr = `{ "description": "random description", - "resources": {"slots": 4, "max_slots": 8}, - "log_policies": [ - { - "pattern": "nonrepeat" - } - ] + "resources": {"slots": 4, "max_slots": 8} }` // DefaultConstraintsStr is the default constraints val used for tests. DefaultConstraintsStr = `{"priority_limit": 10, "resources": {"max_slots": 8}}` diff --git a/master/internal/configpolicy/task_config_policy_intg_test.go b/master/internal/configpolicy/task_config_policy_intg_test.go index 1792f87ff5c..1129ba8b9c1 100644 --- a/master/internal/configpolicy/task_config_policy_intg_test.go +++ b/master/internal/configpolicy/task_config_policy_intg_test.go @@ -471,11 +471,6 @@ func TestMergeWithInvariantExperimentConfigs(t *testing.T) { "read_only": true, "propagation": "cluster-wide" } - ], - "log_policies": [ - { - "pattern": "nonrepeat" - } ] }` @@ -608,6 +603,7 @@ func TestMergeWithInvariantExperimentConfigs(t *testing.T) { require.NoError(t, err) conf.SetName(wkspInvariantConfig.Name()) + conf.RawReproducibility = wkspInvariantConfig.RawReproducibility require.Equal(t, wkspInvariantConfig, *conf) }) @@ -662,6 +658,7 @@ func TestMergeSlicesAndMaps(t *testing.T) { }, "log_policies": [ { + "name": "nonrepeat policy", "pattern": "nonrepeat" } ] @@ -701,6 +698,7 @@ func TestMergeSlicesAndMaps(t *testing.T) { }, "log_policies": [ { + "name": "repeat policy", "pattern": "repeat" } ] @@ -768,9 +766,11 @@ func TestMergeSlicesAndMaps(t *testing.T) { }, "log_policies": [ { + "name": "nonrepeat policy", "pattern": "nonrepeat" }, { + "name": "repeat policy", "pattern": "repeat" } ] @@ -810,6 +810,7 @@ func TestMergeSlicesAndMaps(t *testing.T) { }, "log_policies": [ { + "name": "global repeat policy", "pattern": "gloablrepeat" } ] @@ -893,12 +894,15 @@ func TestMergeSlicesAndMaps(t *testing.T) { }, "log_policies": [ { + "name": "nonrepeat policy", "pattern": "nonrepeat" }, { + "name": "repeat policy", "pattern": "repeat" }, { + "name": "global repeat policy", "pattern": "gloablrepeat" } ] diff --git a/master/internal/configpolicy/utils.go b/master/internal/configpolicy/utils.go index 0afbe9c04d6..0b503e84e31 100644 --- a/master/internal/configpolicy/utils.go +++ b/master/internal/configpolicy/utils.go @@ -151,7 +151,8 @@ func ValidateExperimentConfig( // Verify the workspace invariant config doesn't conflict with global constraints. if err := checkConstraintConflicts(globalConstraints, cp.InvariantConfig.RawResources.RawMaxSlots, cp.InvariantConfig.RawResources.RawSlotsPerTrial, cp.InvariantConfig.RawResources.RawPriority); err != nil { - return status.Errorf(codes.InvalidArgument, fmt.Sprintf(InvalidExperimentConfigPolicyErr+": %s.", err)) + return status.Errorf(codes.InvalidArgument, fmt.Sprintf(InvalidExperimentConfigPolicyErr+ + ": workspace invariant_config conflicts with global constraints: %s.", err)) } } } diff --git a/master/internal/db/postgres_experiments_intg_test.go b/master/internal/db/postgres_experiments_intg_test.go index 48c573bb87b..68cab82424c 100644 --- a/master/internal/db/postgres_experiments_intg_test.go +++ b/master/internal/db/postgres_experiments_intg_test.go @@ -435,17 +435,31 @@ func TestActiveLogPatternPolicies(t *testing.T) { policies, err := ActiveLogPolicies(ctx, exp.ID) require.NoError(t, err) - require.Empty(t, policies) + require.NotEmpty(t, policies) + expected := expconf.LogPoliciesConfig{ + expconf.LogPolicy{ + RawName: ptrs.Ptr(expconf.CUDAOOM), + RawPattern: ptrs.Ptr(expconf.CUDAOOMPattern), + }, + expconf.LogPolicy{ + RawName: ptrs.Ptr(expconf.ECCError), + RawPattern: ptrs.Ptr(expconf.ECCErrorPattern), + }, + } + + require.Equal(t, expected, policies) activeConfig, err := db.ActiveExperimentConfig(exp.ID) require.NoError(t, err) activeConfig.RawLogPolicies = expconf.LogPoliciesConfig{ - expconf.LogPolicy{RawPattern: "sub", RawAction: expconf.LogAction{ - RawCancelRetries: &expconf.LogActionCancelRetries{}, - }}, - expconf.LogPolicy{RawPattern: `\d{5}$`, RawAction: expconf.LogAction{ - RawExcludeNode: &expconf.LogActionExcludeNode{}, - }}, + expconf.LogPolicy{ + RawPattern: ptrs.Ptr(`\d{5}$`), + RawAction: &expconf.LogActionV0{Type: expconf.LogActionTypeExcludeNode}, + }, + expconf.LogPolicy{ + RawPattern: ptrs.Ptr("sub"), + RawAction: &expconf.LogActionV0{Type: expconf.LogActionTypeCancelRetries}, + }, } v, err := json.Marshal(activeConfig) diff --git a/master/internal/logpattern/logpattern.go b/master/internal/logpattern/logpattern.go index f7f62e4d87c..3eaf7a5b006 100644 --- a/master/internal/logpattern/logpattern.go +++ b/master/internal/logpattern/logpattern.go @@ -59,7 +59,11 @@ func (l *LogPatternPolicies) monitor(ctx context.Context, ) error { // TODO when we add rm specific log grabbing we will need to also monitor them. for _, policy := range policies { - compiledRegex, err := l.getCompiledRegex(policy.Pattern()) + pattern := policy.Pattern() + if pattern == nil { + continue + } + compiledRegex, err := l.getCompiledRegex(*pattern) if err != nil { return err } @@ -76,23 +80,48 @@ func (l *LogPatternPolicies) monitor(ctx context.Context, } if compiledRegex.MatchString(log.Log) { - switch policy.Action().GetUnionMember().(type) { - case expconf.LogActionCancelRetries: - if err := addDontRetry( - ctx, model.TaskID(log.TaskID), *log.AgentID, policy.Pattern(), log.Log, - ); err != nil { - return fmt.Errorf("adding don't retry: %w", err) + err = db.Bun().RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + if policy.Action() != nil { + switch policy.Action().Type { + case expconf.LogActionTypeCancelRetries: + if err := addDontRetry( + ctx, model.TaskID(log.TaskID), *log.AgentID, *pattern, log.Log, tx, + ); err != nil { + return fmt.Errorf("adding don't retry: %w", err) + } + + case expconf.LogActionTypeExcludeNode: + if err := addRetryOnDifferentNode( + ctx, model.TaskID(log.TaskID), *log.AgentID, *pattern, log.Log, tx, + ); err != nil { + return fmt.Errorf("adding retry on different node: %w", err) + } + default: + return fmt.Errorf("unrecognized log pattern policy type") + } } - case expconf.LogActionExcludeNode: - if err := addRetryOnDifferentNode( - ctx, model.TaskID(log.TaskID), *log.AgentID, policy.Pattern(), log.Log, - ); err != nil { - return fmt.Errorf("adding retry on different node: %w", err) + if policy.Name() != nil { + if _, err := tx.NewUpdate().Model(&model.Task{}). + Set("log_policy_matched = ?", *policy.Name()). + Where("task_id = ?", log.TaskID). + Exec(ctx); err != nil { + return fmt.Errorf("updating log signal of task %s: %w", log.TaskID, err) + } + if _, err := tx.NewUpdate().Model(&model.Run{}). + Table("run_id_task_id"). + Set("log_policy_matched = ?", *policy.Name()). + Where("run.id = run_id_task_id.run_id"). + Where("run_id_task_id.task_id = ?", log.TaskID). + Exec(ctx); err != nil { + return fmt.Errorf("updating log signal of task %s: %w", log.TaskID, err) + } } - default: - return fmt.Errorf("unrecognized log pattern policy type") + return nil + }) + if err != nil { + return fmt.Errorf("\"%s\" matches pattern \"%s\" but failed to update db: %w", log.Log, *pattern, err) } } } @@ -130,7 +159,7 @@ func GetBlockedNodes(ctx context.Context, taskID model.TaskID) ([]string, error) } func addRetryOnDifferentNode( - ctx context.Context, taskID model.TaskID, nodeName, regex, triggeringLog string, + ctx context.Context, taskID model.TaskID, nodeName, regex, triggeringLog string, tx bun.Tx, ) error { m := &retryOnDifferentNode{ TaskID: taskID, @@ -138,7 +167,7 @@ func addRetryOnDifferentNode( Regex: regex, TriggeringLog: triggeringLog, } - res, err := db.Bun().NewInsert().Model(m). + res, err := tx.NewInsert().Model(m). On("CONFLICT (task_id, node_name, regex) DO NOTHING"). // Only care about the first log. Exec(ctx) if err != nil { @@ -173,7 +202,7 @@ type dontRetry struct { } func addDontRetry( - ctx context.Context, taskID model.TaskID, nodeName, regex, triggeringLog string, + ctx context.Context, taskID model.TaskID, nodeName, regex, triggeringLog string, tx bun.Tx, ) error { m := &dontRetry{ TaskID: taskID, @@ -181,7 +210,7 @@ func addDontRetry( Regex: regex, TriggeringLog: triggeringLog, } - if _, err := db.Bun().NewInsert().Model(m). + if _, err := tx.NewInsert().Model(m). On("CONFLICT (task_id, regex) DO NOTHING"). // Only care about the first log. Exec(ctx); err != nil { return fmt.Errorf("adding don't retry policy %+v: %w", m, err) diff --git a/master/internal/logpattern/logpattern_intg_test.go b/master/internal/logpattern/logpattern_intg_test.go index 48dd5c6d678..f1181f97a49 100644 --- a/master/internal/logpattern/logpattern_intg_test.go +++ b/master/internal/logpattern/logpattern_intg_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/uptrace/bun" "github.com/determined-ai/determined/master/internal/db" "github.com/determined-ai/determined/master/pkg/etc" @@ -53,12 +54,16 @@ func TestRetryOnDifferentNode(t *testing.T) { require.NoError(t, err) require.Empty(t, blocked) - require.NoError(t, addRetryOnDifferentNode(ctx, task.TaskID, "n0", "regexa", "loga")) - require.NoError(t, addRetryOnDifferentNode(ctx, task.TaskID, "n1", "regexa", "logb")) - require.NoError(t, addRetryOnDifferentNode(ctx, task.TaskID, "n0", "regexb", "logc")) + err = db.Bun().RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + require.NoError(t, addRetryOnDifferentNode(ctx, task.TaskID, "n0", "regexa", "loga", tx)) + require.NoError(t, addRetryOnDifferentNode(ctx, task.TaskID, "n1", "regexa", "logb", tx)) + require.NoError(t, addRetryOnDifferentNode(ctx, task.TaskID, "n0", "regexb", "logc", tx)) - require.NoError(t, addRetryOnDifferentNode(ctx, task.TaskID, "n0", "regexa", "dontappear")) - require.NoError(t, addRetryOnDifferentNode(ctx, task.TaskID, "n0", "regexb", "dontappear")) + require.NoError(t, addRetryOnDifferentNode(ctx, task.TaskID, "n0", "regexa", "dontappear", tx)) + require.NoError(t, addRetryOnDifferentNode(ctx, task.TaskID, "n0", "regexb", "dontappear", tx)) + return nil + }) + require.NoError(t, err) // Check DB state is as expected. var actual []*retryOnDifferentNode @@ -105,10 +110,14 @@ func TestShouldRetry(t *testing.T) { require.NoError(t, err) require.Empty(t, resp) - require.NoError(t, addDontRetry(ctx, task.TaskID, "n0", "regexa", "loga")) - require.NoError(t, addDontRetry(ctx, task.TaskID, "n0", "regexb", "logb")) - require.NoError(t, addDontRetry(ctx, task.TaskID, "n0", "regexa", "dontappear")) - require.NoError(t, addDontRetry(ctx, task.TaskID, "n1", "regexb", "dontappear")) + err = db.Bun().RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + require.NoError(t, addDontRetry(ctx, task.TaskID, "n0", "regexa", "loga", tx)) + require.NoError(t, addDontRetry(ctx, task.TaskID, "n0", "regexb", "logb", tx)) + require.NoError(t, addDontRetry(ctx, task.TaskID, "n0", "regexa", "dontappear", tx)) + require.NoError(t, addDontRetry(ctx, task.TaskID, "n1", "regexb", "dontappear", tx)) + return nil + }) + require.NoError(t, err) resp, err = ShouldRetry(ctx, task.TaskID) require.NoError(t, err) diff --git a/master/internal/run/postgres_run_intg_test.go b/master/internal/run/postgres_run_intg_test.go index 36df531103b..634d044f488 100644 --- a/master/internal/run/postgres_run_intg_test.go +++ b/master/internal/run/postgres_run_intg_test.go @@ -29,7 +29,7 @@ func TestMigrateTrials(t *testing.T) { } // get all trial info, excluding additional fields added after the transition to runs. require.NoError(t, db.Bun().NewSelect().Table("trials"). - ColumnExpr("to_jsonb(trials.*)-'metadata'-'log_signal' AS trial_data"). + ColumnExpr("to_jsonb(trials.*)-'metadata'-'log_policy_matched' AS trial_data"). Order("id"). Scan(ctx, ¤tTrialsViewData), ) diff --git a/master/pkg/model/task.go b/master/pkg/model/task.go index 8fb37c232d1..4152ee909b5 100644 --- a/master/pkg/model/task.go +++ b/master/pkg/model/task.go @@ -91,7 +91,8 @@ type Task struct { Config *string `db:"config"` - NoPause *bool `db:"no_pause"` + NoPause *bool `db:"no_pause"` + LogPolicyMatched *string `db:"log_policy_matched"` } // AllocationID is the ID of an allocation of a task. It is usually of the form diff --git a/master/pkg/model/task_container_defaults.go b/master/pkg/model/task_container_defaults.go index 4eeedd4258c..f3579ff3882 100644 --- a/master/pkg/model/task_container_defaults.go +++ b/master/pkg/model/task_container_defaults.go @@ -323,7 +323,7 @@ func (c TaskContainerDefaultsConfig) Merge( if res.LogPolicies == nil { res.LogPolicies = other.LogPolicies } else { - res.LogPolicies = res.LogPolicies.Merge(other.LogPolicies) + res.LogPolicies = other.LogPolicies.Merge(res.LogPolicies) } } diff --git a/master/pkg/model/task_container_defaults_test.go b/master/pkg/model/task_container_defaults_test.go index 4039dec9af8..2edd9d0a4d4 100644 --- a/master/pkg/model/task_container_defaults_test.go +++ b/master/pkg/model/task_container_defaults_test.go @@ -436,7 +436,7 @@ func TestTaskContainerDefaultsConfigMerging(t *testing.T) { } } -func TestLogPatternUnmarshal(t *testing.T) { +func TestLegacyLogPatternUnmarshal(t *testing.T) { var tcd TaskContainerDefaultsConfig require.NoError(t, json.Unmarshal([]byte(string(`{ "log_policies": [ @@ -450,61 +450,28 @@ func TestLogPatternUnmarshal(t *testing.T) { NetworkMode: "bridge", PreemptionTimeout: DefaultPreemptionTimeout, LogPolicies: expconf.LogPoliciesConfig{ - expconf.LogPolicy{RawPattern: "test", RawAction: expconf.LogAction{ - RawExcludeNode: &expconf.LogActionExcludeNode{}, - }}, - expconf.LogPolicy{RawPattern: "test2", RawAction: expconf.LogAction{ - RawCancelRetries: &expconf.LogActionCancelRetries{}, - }}, + expconf.LogPolicy{ + RawPattern: ptrs.Ptr("test"), + RawAction: &expconf.LogActionV0{Type: expconf.LogActionTypeExcludeNode}, + }, + expconf.LogPolicy{ + RawPattern: ptrs.Ptr("test2"), + RawAction: &expconf.LogActionV0{Type: expconf.LogActionTypeCancelRetries}, + }, }, } require.Equal(t, expected, tcd) } -func TestLogPatternPoliciesMerging(t *testing.T) { - defaults := &TaskContainerDefaultsConfig{ - LogPolicies: expconf.LogPoliciesConfig{ - expconf.LogPolicy{RawPattern: "a", RawAction: expconf.LogAction{ - RawCancelRetries: &expconf.LogActionCancelRetries{}, - }}, - expconf.LogPolicy{RawPattern: "b", RawAction: expconf.LogAction{ - RawExcludeNode: &expconf.LogActionExcludeNode{}, - }}, - }, - } - - conf := expconf.ExperimentConfig{ - RawLogPolicies: expconf.LogPoliciesConfig{ - expconf.LogPolicy{RawPattern: "b", RawAction: expconf.LogAction{ - RawCancelRetries: &expconf.LogActionCancelRetries{}, - }}, - expconf.LogPolicy{RawPattern: "b", RawAction: expconf.LogAction{ - RawExcludeNode: &expconf.LogActionExcludeNode{}, - }}, - expconf.LogPolicy{RawPattern: "c", RawAction: expconf.LogAction{ - RawExcludeNode: &expconf.LogActionExcludeNode{}, - }}, - }, - } - - defaults.MergeIntoExpConfig(&conf) - - expected := expconf.LogPoliciesConfig{ - expconf.LogPolicy{RawPattern: "a", RawAction: expconf.LogAction{ - RawCancelRetries: &expconf.LogActionCancelRetries{}, - }}, - expconf.LogPolicy{RawPattern: "b", RawAction: expconf.LogAction{ - RawExcludeNode: &expconf.LogActionExcludeNode{}, - }}, - expconf.LogPolicy{RawPattern: "b", RawAction: expconf.LogAction{ - RawCancelRetries: &expconf.LogActionCancelRetries{}, - }}, - expconf.LogPolicy{RawPattern: "c", RawAction: expconf.LogAction{ - RawExcludeNode: &expconf.LogActionExcludeNode{}, - }}, - } - - require.Equal(t, expected, conf.RawLogPolicies) +func TestLogPatternUnmarshal(t *testing.T) { + var tcd TaskContainerDefaultsConfig + err := json.Unmarshal([]byte(string(`{ + "log_policies": [ + {"name": "policy name", "pattern": "a", "action": "exclude_node"}, + {"name": "policy name", "pattern": "b", "action": "exclude_node"} + ] + }`)), &tcd) + require.ErrorContains(t, err, "log_policies have duplicated names \"policy name\"") } func TestPodSpecsDefaultMerging(t *testing.T) { diff --git a/master/pkg/schemas/expconf/const.go b/master/pkg/schemas/expconf/const.go index 77b73cd0132..48c3d0a20ec 100644 --- a/master/pkg/schemas/expconf/const.go +++ b/master/pkg/schemas/expconf/const.go @@ -12,3 +12,11 @@ const ( CUDAImage = "determinedai/pytorch-ngc-dev:0736b6d" ROCMImage = "determinedai/environments:rocm-5.6-pytorch-1.3-tf-2.10-rocm-mpich-0736b6d" ) + +// Default log policies values. +const ( + CUDAOOM = "CUDA OOM" + CUDAOOMPattern = ".*CUDA out of memory.*" + ECCError = "ECC Error" + ECCErrorPattern = ".*uncorrectable ECC error encountered.*" +) diff --git a/master/pkg/schemas/expconf/latest.go b/master/pkg/schemas/expconf/latest.go index 3d342c88409..9751934700f 100644 --- a/master/pkg/schemas/expconf/latest.go +++ b/master/pkg/schemas/expconf/latest.go @@ -31,8 +31,6 @@ type ( LogPoliciesConfig = LogPoliciesConfigV0 LogPolicy = LogPolicyV0 LogAction = LogActionV0 - LogActionCancelRetries = LogActionCancelRetriesV0 - LogActionExcludeNode = LogActionExcludeNodeV0 LogHyperparameter = LogHyperparameterV0 OptimizationsConfig = OptimizationsConfigV0 PbsConfig = PbsConfigV0 diff --git a/master/pkg/schemas/expconf/log_pattern_config.go b/master/pkg/schemas/expconf/log_pattern_config.go index 7692db460a1..e6cd8f495e2 100644 --- a/master/pkg/schemas/expconf/log_pattern_config.go +++ b/master/pkg/schemas/expconf/log_pattern_config.go @@ -4,86 +4,185 @@ import ( "encoding/json" "fmt" - log "github.com/sirupsen/logrus" + "github.com/pkg/errors" + "github.com/determined-ai/determined/master/pkg/ptrs" "github.com/determined-ai/determined/master/pkg/schemas" - "github.com/determined-ai/determined/master/pkg/union" + "github.com/determined-ai/determined/master/pkg/set" ) // LogPoliciesConfigV0 is a list of log policies. +// +//go:generate ../gen.sh type LogPoliciesConfigV0 []LogPolicyV0 -// Merge implemenets the mergable interface. +// WithDefaults implements the Defaultable pseudo-interface. +func (b LogPoliciesConfigV0) WithDefaults() LogPoliciesConfigV0 { + defaultPolicies := LogPoliciesConfigV0{ + LogPolicyV0{ + RawName: ptrs.Ptr(CUDAOOM), + RawPattern: ptrs.Ptr(CUDAOOMPattern), + }, + LogPolicyV0{ + RawName: ptrs.Ptr(ECCError), + RawPattern: ptrs.Ptr(ECCErrorPattern), + }, + } + + return schemas.Merge(b, defaultPolicies) +} + +// Merge implements the Mergable pseudo-interface. +// Union merge log policies. +// Special cases: +// 1. We may see policies with different names same patterns. We keep both of them, but which name +// will be shown in the UI is undefined. +// 2. There could be policies with the same name different patterns. We save the one with the higher priority. +// Unsetting default values depends on this behavior. +// 2.1 Polices that don't have name but have same patterns is a special case. These are legacy +// policies. func (b LogPoliciesConfigV0) Merge( - other LogPoliciesConfigV0, + src LogPoliciesConfigV0, ) LogPoliciesConfigV0 { - var out LogPoliciesConfigV0 - seen := make(map[string]bool) - for _, p := range append(other, b...) { - json, err := json.Marshal(p) - if err != nil { - log.Errorf("marshaling error %+v %v", p, err) + if src == nil && b == nil { + return nil + } + + // Keep everything in b unconditionally. + out := append(LogPoliciesConfigV0{}, b...) + + names := set.New[string]() + unnamedPolicies := set.New[string]() + for _, lp := range b { + if lp.RawName == nil { + // Not checking nil because we've enforced action and pattern must be set for the legacy policy + // in the json schema. + s := fmt.Sprintf("%v:%v", *lp.RawAction, *lp.RawPattern) + unnamedPolicies.Insert(s) + } else { + names.Insert(*lp.RawName) } - if seen[string(json)] { + } + + // Add policies in src that don't exist in b. + for _, lp := range src { + if lp.RawName == nil { + s := fmt.Sprintf("%v:%v", *lp.RawAction, *lp.RawPattern) + if !unnamedPolicies.Contains(s) { + out = append(out, lp) + unnamedPolicies.Insert(s) + } + } else if !names.Contains(*lp.RawName) { + out = append(out, lp) + names.Insert(*lp.RawName) + } + } + + return out +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// Special cases: +// 1. User may submit policies that have different names but same patterns. We keep both of them, but which name +// will be shown in the UI is undefined. +// 2. User can't submit policies that have the same names. +func (b *LogPoliciesConfigV0) UnmarshalJSON(data []byte) error { + type DefaultParser LogPoliciesConfigV0 + var jsonItems DefaultParser + if err := json.Unmarshal(data, &jsonItems); err != nil { + return errors.Wrapf(err, "failed to parse runtime items") + } + + // Detect policies that have the same names but different patterns + names := set.New[string]() + for _, jsonItem := range jsonItems { + n := jsonItem.RawName + if n == nil { continue } - seen[string(json)] = true - out = append(out, p) + if names.Contains(*n) { + return fmt.Errorf("log_policies have duplicated names %q", *n) + } + names.Insert(*n) } - return out + + *b = LogPoliciesConfig(jsonItems) + return nil } // LogPolicyV0 is an action to take if we match against trial logs. // //go:generate ../gen.sh type LogPolicyV0 struct { - RawPattern string `json:"pattern"` - - RawAction LogActionV0 `json:"action"` + // Legacy log policy doesn't have a name. Legacy log policy will be deprecated. + RawName *string `json:"name,omitempty"` + // Pattern can be nil. So user can override it to disable the default log polices. + RawPattern *string `json:"pattern,omitempty"` + RawAction *LogActionV0 `json:"action,omitempty"` } -// LogActionV0 is a policy to take after matching. -// -//go:generate ../gen.sh -type LogActionV0 struct { - RawCancelRetries *LogActionCancelRetriesV0 `union:"type,cancel_retries" json:"-"` - RawExcludeNode *LogActionExcludeNodeV0 `union:"type,exclude_node" json:"-"` -} +// LogActionType is the type of an action. +type LogActionType string -// Merge implements schemas.Mergeable. -func (s LogActionV0) Merge(other LogActionV0) LogActionV0 { - return schemas.UnionMerge(s, other) -} +// LogActionType refers to the action user can take when a pattern is detected in the log. +const ( + LogActionTypeCancelRetries LogActionType = "cancel_retries" + LogActionTypeExcludeNode LogActionType = "exclude_node" +) -// MarshalJSON implements the json.Marshaler interface. -func (s LogActionV0) MarshalJSON() ([]byte, error) { - return union.MarshalEx(s, true) +// LogActionV0 is a policy to take after matching. +type LogActionV0 struct { + Type LogActionType } // UnmarshalJSON implements the json.Unmarshaler interface. +// It applies a shim to the legacy actions. For example we have a legacy action: +// +// action: +// type: cancel_retries +// +// It will become: +// +// action: cancel_retries func (s *LogActionV0) UnmarshalJSON(data []byte) error { - if err := union.Unmarshal(data, s); err != nil { - return err + // First, check if we can unmarshal the log policy item as a legacy item. + type LegacyAction struct { + Type LogActionType `json:"type"` + } + + var legacy LegacyAction + err := json.Unmarshal(data, &legacy) + if err == nil { + // Apply shim to bring legacy policy into the current format. + switch legacy.Type { + case LogActionTypeCancelRetries, LogActionTypeExcludeNode: + *s = LogActionV0(legacy) + default: + return fmt.Errorf("unrecognized legacy action type: %s, data: %q", legacy.Type, string(data)) + } + return nil } - type DefaultParser *LogActionV0 - if err := json.Unmarshal(data, DefaultParser(s)); err != nil { - return fmt.Errorf("failed to parse searcher config: %w", err) + + // It is not a legacy item. Try to unmarshal it as a modern item. + var lat LogActionType + if err := json.Unmarshal(data, &lat); err == nil { + switch lat { + case LogActionTypeCancelRetries, LogActionTypeExcludeNode: + *s = LogActionV0{Type: lat} + return nil + default: + return fmt.Errorf("unrecognized action type: %s, data: %q", lat, string(data)) + } } - return nil -} -// LogActionCancelRetriesV0 doesn't retry the trial if it fails. -// -//go:generate ../gen.sh -type LogActionCancelRetriesV0 struct { - // This comment is needed to stop ../gen.sh from complaining. + return fmt.Errorf("failed to unmarshal log action: %w, data: %q", err, string(data)) } -// LogActionExcludeNodeV0 will exclude the node the log was seen on -// (only for that trial) and reschedule. -// -//go:generate ../gen.sh -type LogActionExcludeNodeV0 struct { - // This comment is needed to stop ../gen.sh from complaining. +// MarshalJSON implements the json.Marshaler interface. +func (s LogActionV0) MarshalJSON() ([]byte, error) { + if s.Type == LogActionTypeCancelRetries || s.Type == LogActionTypeExcludeNode { + return json.Marshal(s.Type) + } + return nil, fmt.Errorf("failed to marshal LogActionV0: %+v", s) } diff --git a/master/pkg/schemas/expconf/schema_test.go b/master/pkg/schemas/expconf/schema_test.go index 1ee06e5dfd0..c6040754379 100644 --- a/master/pkg/schemas/expconf/schema_test.go +++ b/master/pkg/schemas/expconf/schema_test.go @@ -165,6 +165,8 @@ func objectForURL(url string) interface{} { "http://determined.ai/schemas/expconf/v0/test-union-a.json", "http://determined.ai/schemas/expconf/v0/test-union-b.json": return &TestUnionV0{} + case "http://determined.ai/schemas/expconf/v0/log-policies.json": + return &LogPoliciesConfigV0{} default: panic(fmt.Sprintf("No object to match %v, maybe you need to add one?", url)) } @@ -286,7 +288,6 @@ func (tc SchemaTestCase) CheckMerged(t *testing.T) { assert.NilError(t, err) merged := schemas.Merge(obj, src) - // Compare json-to-json. mergedBytes, err := json.Marshal(merged) assert.NilError(t, err) diff --git a/master/pkg/schemas/expconf/zgen_log_action_cancel_retries_v0.go b/master/pkg/schemas/expconf/zgen_log_action_cancel_retries_v0.go deleted file mode 100644 index c729c49a662..00000000000 --- a/master/pkg/schemas/expconf/zgen_log_action_cancel_retries_v0.go +++ /dev/null @@ -1,21 +0,0 @@ -// Code generated by gen.py. DO NOT EDIT. - -package expconf - -import ( - "github.com/santhosh-tekuri/jsonschema/v2" - - "github.com/determined-ai/determined/master/pkg/schemas" -) - -func (l LogActionCancelRetriesV0) ParsedSchema() interface{} { - return schemas.ParsedLogActionCancelRetriesV0() -} - -func (l LogActionCancelRetriesV0) SanityValidator() *jsonschema.Schema { - return schemas.GetSanityValidator("http://determined.ai/schemas/expconf/v0/log-action-cancel-retries.json") -} - -func (l LogActionCancelRetriesV0) CompletenessValidator() *jsonschema.Schema { - return schemas.GetCompletenessValidator("http://determined.ai/schemas/expconf/v0/log-action-cancel-retries.json") -} diff --git a/master/pkg/schemas/expconf/zgen_log_action_exclude_node_v0.go b/master/pkg/schemas/expconf/zgen_log_action_exclude_node_v0.go deleted file mode 100644 index 7c0063b06fe..00000000000 --- a/master/pkg/schemas/expconf/zgen_log_action_exclude_node_v0.go +++ /dev/null @@ -1,21 +0,0 @@ -// Code generated by gen.py. DO NOT EDIT. - -package expconf - -import ( - "github.com/santhosh-tekuri/jsonschema/v2" - - "github.com/determined-ai/determined/master/pkg/schemas" -) - -func (l LogActionExcludeNodeV0) ParsedSchema() interface{} { - return schemas.ParsedLogActionExcludeNodeV0() -} - -func (l LogActionExcludeNodeV0) SanityValidator() *jsonschema.Schema { - return schemas.GetSanityValidator("http://determined.ai/schemas/expconf/v0/log-action-exclude-node.json") -} - -func (l LogActionExcludeNodeV0) CompletenessValidator() *jsonschema.Schema { - return schemas.GetCompletenessValidator("http://determined.ai/schemas/expconf/v0/log-action-exclude-node.json") -} diff --git a/master/pkg/schemas/expconf/zgen_log_action_v0.go b/master/pkg/schemas/expconf/zgen_log_action_v0.go deleted file mode 100644 index c74aa1384ae..00000000000 --- a/master/pkg/schemas/expconf/zgen_log_action_v0.go +++ /dev/null @@ -1,31 +0,0 @@ -// Code generated by gen.py. DO NOT EDIT. - -package expconf - -import ( - "github.com/santhosh-tekuri/jsonschema/v2" - - "github.com/determined-ai/determined/master/pkg/schemas" -) - -func (l LogActionV0) GetUnionMember() interface{} { - if l.RawCancelRetries != nil { - return *l.RawCancelRetries - } - if l.RawExcludeNode != nil { - return *l.RawExcludeNode - } - panic("no union member defined") -} - -func (l LogActionV0) ParsedSchema() interface{} { - return schemas.ParsedLogActionV0() -} - -func (l LogActionV0) SanityValidator() *jsonschema.Schema { - return schemas.GetSanityValidator("http://determined.ai/schemas/expconf/v0/log-action.json") -} - -func (l LogActionV0) CompletenessValidator() *jsonschema.Schema { - return schemas.GetCompletenessValidator("http://determined.ai/schemas/expconf/v0/log-action.json") -} diff --git a/master/pkg/schemas/expconf/zgen_log_policies_config_v0.go b/master/pkg/schemas/expconf/zgen_log_policies_config_v0.go new file mode 100644 index 00000000000..49b45b654d5 --- /dev/null +++ b/master/pkg/schemas/expconf/zgen_log_policies_config_v0.go @@ -0,0 +1,21 @@ +// Code generated by gen.py. DO NOT EDIT. + +package expconf + +import ( + "github.com/santhosh-tekuri/jsonschema/v2" + + "github.com/determined-ai/determined/master/pkg/schemas" +) + +func (l LogPoliciesConfigV0) ParsedSchema() interface{} { + return schemas.ParsedLogPoliciesConfigV0() +} + +func (l LogPoliciesConfigV0) SanityValidator() *jsonschema.Schema { + return schemas.GetSanityValidator("http://determined.ai/schemas/expconf/v0/log-policies.json") +} + +func (l LogPoliciesConfigV0) CompletenessValidator() *jsonschema.Schema { + return schemas.GetCompletenessValidator("http://determined.ai/schemas/expconf/v0/log-policies.json") +} diff --git a/master/pkg/schemas/expconf/zgen_log_policy_v0.go b/master/pkg/schemas/expconf/zgen_log_policy_v0.go index 91aa8f2eb3f..73034950a8c 100644 --- a/master/pkg/schemas/expconf/zgen_log_policy_v0.go +++ b/master/pkg/schemas/expconf/zgen_log_policy_v0.go @@ -8,19 +8,27 @@ import ( "github.com/determined-ai/determined/master/pkg/schemas" ) -func (l LogPolicyV0) Pattern() string { +func (l LogPolicyV0) Name() *string { + return l.RawName +} + +func (l *LogPolicyV0) SetName(val *string) { + l.RawName = val +} + +func (l LogPolicyV0) Pattern() *string { return l.RawPattern } -func (l *LogPolicyV0) SetPattern(val string) { +func (l *LogPolicyV0) SetPattern(val *string) { l.RawPattern = val } -func (l LogPolicyV0) Action() LogActionV0 { +func (l LogPolicyV0) Action() *LogActionV0 { return l.RawAction } -func (l *LogPolicyV0) SetAction(val LogActionV0) { +func (l *LogPolicyV0) SetAction(val *LogActionV0) { l.RawAction = val } diff --git a/master/pkg/schemas/extensions/disallow_properties.go b/master/pkg/schemas/extensions/disallow_properties.go index b2dc61af94f..76a65482500 100644 --- a/master/pkg/schemas/extensions/disallow_properties.go +++ b/master/pkg/schemas/extensions/disallow_properties.go @@ -1,5 +1,5 @@ // disallowProperties is for restricting which properties are allowed in an object with -// per-property, such as when we allow a k8s pod spec with some fields disallowed. +// per-property error messages, such as when we allow a k8s pod spec with some fields disallowed. // // Example: The "pod_spec" property of the environment config: // diff --git a/master/pkg/schemas/zgen_schemas.go b/master/pkg/schemas/zgen_schemas.go index f90c94923c8..b887031a82a 100644 --- a/master/pkg/schemas/zgen_schemas.go +++ b/master/pkg/schemas/zgen_schemas.go @@ -848,10 +848,9 @@ var ( "array", "null" ], + "$comment": "setting default to [] lets the actual default always comes from WithDefaults()", "default": [], - "items": { - "$ref": "http://determined.ai/schemas/expconf/v0/log-policy.json" - } + "optionalRef": "http://determined.ai/schemas/expconf/v0/log-policies.json" }, "retention_policy": { "type": [ @@ -1496,10 +1495,37 @@ var ( } } `) - textLogActionCancelRetriesV0 = []byte(`{ + textLogActionV0 = []byte(`{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://determined.ai/schemas/expconf/v0/log-action.json", + "title": "LogAction", + "union": { + "defaultMessage": "must be one of \"cancel_retries\" or \"exclude_node\"", + "items": [ + { + "unionKey": "never", + "const": "cancel_retries" + }, + { + "unionKey": "never", + "const": "exclude_node" + }, + { + "unionKey": "const:type=cancel_retries", + "$ref": "http://determined.ai/schemas/expconf/v0/log-legacy-action-cancel-retries.json" + }, + { + "unionKey": "const:type=exclude_node", + "$ref": "http://determined.ai/schemas/expconf/v0/log-legacy-action-exclude-node.json" + } + ] + } +} +`) + textLogLegacyActionCancelRetriesV0 = []byte(`{ "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://determined.ai/schemas/expconf/v0/log-action-cancel-retries.json", - "title": "LogActionCancelRetries", + "$id": "http://determined.ai/schemas/expconf/v0/log-legacy-action-cancel-retries.json", + "title": "LogLegacyActionCancelRetries", "additionalProperties": false, "type": "object", "required": [ @@ -1513,10 +1539,10 @@ var ( } } `) - textLogActionExcludeNodeV0 = []byte(`{ + textLogLegacyActionExcludeNodeV0 = []byte(`{ "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://determined.ai/schemas/expconf/v0/log-action-exclude-node.json", - "title": "LogActionExcludeNode", + "$id": "http://determined.ai/schemas/expconf/v0/log-legacy-action-exclude-node.json", + "title": "LogLegacyActionExcludeNode", "additionalProperties": false, "type": "object", "required": [ @@ -1530,37 +1556,13 @@ var ( } } `) - textLogActionV0 = []byte(`{ + textLogPoliciesConfigV0 = []byte(`{ "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://determined.ai/schemas/expconf/v0/log-action.json", - "title": "LogAction", - "$comment": "this is a union of all possible properties, with validation for the common properties", - "if": { - "required": [ - "type" - ] - }, - "then": { - "union": { - "defaultMessage": "is not an object where object[\"type\"] is one of 'cancel_retries' or 'exclude_node'", - "items": [ - { - "unionKey": "const:type=cancel_retries", - "$ref": "http://determined.ai/schemas/expconf/v0/log-action-cancel-retries.json" - }, - { - "unionKey": "const:type=exclude_node", - "$ref": "http://determined.ai/schemas/expconf/v0/log-action-exclude-node.json" - } - ] - } - }, - "additionalProperties": false, - "eventuallyRequired": [ - "type" - ], - "properties": { - "type": true + "$id": "http://determined.ai/schemas/expconf/v0/log-policies.json", + "title": "LogPoliciesConfig", + "type": "array", + "items": { + "$ref": "http://determined.ai/schemas/expconf/v0/log-policy.json" } } `) @@ -1569,22 +1571,51 @@ var ( "$id": "http://determined.ai/schemas/expconf/v0/log-policy.json", "title": "LogPolicy", "additionalProperties": false, - "required": [ - "pattern", - "action" - ], "type": "object", "properties": { + "name": { + "type": [ + "string", + "null" + ], + "$comment": "Legacy log policy doesn't have a name. Legacy log policy will be deprecated.", + "default": null + }, "pattern": { "type": [ - "string" - ] + "string", + "null" + ], + "$comment": "Pattern can be null. So user can override it to disable the default log polices.", + "default": null }, "action": { "type": [ - "object" + "string", + "object", + "null" ], - "$ref": "http://determined.ai/schemas/expconf/v0/log-action.json" + "optionalRef": "http://determined.ai/schemas/expconf/v0/log-action.json", + "default": null + } + }, + "checks": { + "\"name\" must be set, and \"pattern\" is also required unless you intend to disable an existing policy": { + "anyOf": [ + { + "required": [ + "name" + ], + "$comment": "modern policy requirement" + }, + { + "required": [ + "pattern", + "action" + ], + "$comment": "legacy policy requirements" + } + ] } } } @@ -3454,11 +3485,13 @@ var ( schemaLengthV0 interface{} - schemaLogActionCancelRetriesV0 interface{} + schemaLogActionV0 interface{} + + schemaLogLegacyActionCancelRetriesV0 interface{} - schemaLogActionExcludeNodeV0 interface{} + schemaLogLegacyActionExcludeNodeV0 interface{} - schemaLogActionV0 interface{} + schemaLogPoliciesConfigV0 interface{} schemaLogPolicyV0 interface{} @@ -4095,64 +4128,84 @@ func ParsedLengthV0() interface{} { return schemaLengthV0 } -func ParsedLogActionCancelRetriesV0() interface{} { +func ParsedLogActionV0() interface{} { + cacheLock.RLock() + if schemaLogActionV0 != nil { + cacheLock.RUnlock() + return schemaLogActionV0 + } + cacheLock.RUnlock() + + cacheLock.Lock() + defer cacheLock.Unlock() + if schemaLogActionV0 != nil { + return schemaLogActionV0 + } + err := json.Unmarshal(textLogActionV0, &schemaLogActionV0) + if err != nil { + panic("invalid embedded json for LogActionV0") + } + return schemaLogActionV0 +} + +func ParsedLogLegacyActionCancelRetriesV0() interface{} { cacheLock.RLock() - if schemaLogActionCancelRetriesV0 != nil { + if schemaLogLegacyActionCancelRetriesV0 != nil { cacheLock.RUnlock() - return schemaLogActionCancelRetriesV0 + return schemaLogLegacyActionCancelRetriesV0 } cacheLock.RUnlock() cacheLock.Lock() defer cacheLock.Unlock() - if schemaLogActionCancelRetriesV0 != nil { - return schemaLogActionCancelRetriesV0 + if schemaLogLegacyActionCancelRetriesV0 != nil { + return schemaLogLegacyActionCancelRetriesV0 } - err := json.Unmarshal(textLogActionCancelRetriesV0, &schemaLogActionCancelRetriesV0) + err := json.Unmarshal(textLogLegacyActionCancelRetriesV0, &schemaLogLegacyActionCancelRetriesV0) if err != nil { - panic("invalid embedded json for LogActionCancelRetriesV0") + panic("invalid embedded json for LogLegacyActionCancelRetriesV0") } - return schemaLogActionCancelRetriesV0 + return schemaLogLegacyActionCancelRetriesV0 } -func ParsedLogActionExcludeNodeV0() interface{} { +func ParsedLogLegacyActionExcludeNodeV0() interface{} { cacheLock.RLock() - if schemaLogActionExcludeNodeV0 != nil { + if schemaLogLegacyActionExcludeNodeV0 != nil { cacheLock.RUnlock() - return schemaLogActionExcludeNodeV0 + return schemaLogLegacyActionExcludeNodeV0 } cacheLock.RUnlock() cacheLock.Lock() defer cacheLock.Unlock() - if schemaLogActionExcludeNodeV0 != nil { - return schemaLogActionExcludeNodeV0 + if schemaLogLegacyActionExcludeNodeV0 != nil { + return schemaLogLegacyActionExcludeNodeV0 } - err := json.Unmarshal(textLogActionExcludeNodeV0, &schemaLogActionExcludeNodeV0) + err := json.Unmarshal(textLogLegacyActionExcludeNodeV0, &schemaLogLegacyActionExcludeNodeV0) if err != nil { - panic("invalid embedded json for LogActionExcludeNodeV0") + panic("invalid embedded json for LogLegacyActionExcludeNodeV0") } - return schemaLogActionExcludeNodeV0 + return schemaLogLegacyActionExcludeNodeV0 } -func ParsedLogActionV0() interface{} { +func ParsedLogPoliciesConfigV0() interface{} { cacheLock.RLock() - if schemaLogActionV0 != nil { + if schemaLogPoliciesConfigV0 != nil { cacheLock.RUnlock() - return schemaLogActionV0 + return schemaLogPoliciesConfigV0 } cacheLock.RUnlock() cacheLock.Lock() defer cacheLock.Unlock() - if schemaLogActionV0 != nil { - return schemaLogActionV0 + if schemaLogPoliciesConfigV0 != nil { + return schemaLogPoliciesConfigV0 } - err := json.Unmarshal(textLogActionV0, &schemaLogActionV0) + err := json.Unmarshal(textLogPoliciesConfigV0, &schemaLogPoliciesConfigV0) if err != nil { - panic("invalid embedded json for LogActionV0") + panic("invalid embedded json for LogPoliciesConfigV0") } - return schemaLogActionV0 + return schemaLogPoliciesConfigV0 } func ParsedLogPolicyV0() interface{} { @@ -4906,12 +4959,14 @@ func schemaBytesMap() map[string][]byte { cachedSchemaBytesMap[url] = textKerberosConfigV0 url = "http://determined.ai/schemas/expconf/v0/length.json" cachedSchemaBytesMap[url] = textLengthV0 - url = "http://determined.ai/schemas/expconf/v0/log-action-cancel-retries.json" - cachedSchemaBytesMap[url] = textLogActionCancelRetriesV0 - url = "http://determined.ai/schemas/expconf/v0/log-action-exclude-node.json" - cachedSchemaBytesMap[url] = textLogActionExcludeNodeV0 url = "http://determined.ai/schemas/expconf/v0/log-action.json" cachedSchemaBytesMap[url] = textLogActionV0 + url = "http://determined.ai/schemas/expconf/v0/log-legacy-action-cancel-retries.json" + cachedSchemaBytesMap[url] = textLogLegacyActionCancelRetriesV0 + url = "http://determined.ai/schemas/expconf/v0/log-legacy-action-exclude-node.json" + cachedSchemaBytesMap[url] = textLogLegacyActionExcludeNodeV0 + url = "http://determined.ai/schemas/expconf/v0/log-policies.json" + cachedSchemaBytesMap[url] = textLogPoliciesConfigV0 url = "http://determined.ai/schemas/expconf/v0/log-policy.json" cachedSchemaBytesMap[url] = textLogPolicyV0 url = "http://determined.ai/schemas/expconf/v0/optimizations.json" diff --git a/master/static/migrations/20241024164947_add-log-policy-matched.tx.up.sql b/master/static/migrations/20241024164947_add-log-policy-matched.tx.up.sql new file mode 100644 index 00000000000..c265be3293c --- /dev/null +++ b/master/static/migrations/20241024164947_add-log-policy-matched.tx.up.sql @@ -0,0 +1,5 @@ +ALTER table public.tasks + ADD COLUMN log_policy_matched text; + +ALTER TABLE public.runs + RENAME COLUMN log_signal TO log_policy_matched; diff --git a/master/static/srv/proto_get_trials_plus.sql b/master/static/srv/proto_get_trials_plus.sql index 7e89acc8c8b..b38eb65a39a 100644 --- a/master/static/srv/proto_get_trials_plus.sql +++ b/master/static/srv/proto_get_trials_plus.sql @@ -113,7 +113,7 @@ SELECT t.start_time, t.end_time, t.hparams, - t.log_signal, + t.log_policy_matched, new_ckpt.uuid AS warm_start_checkpoint_uuid, ( SELECT tt.task_id FROM run_id_task_id tt diff --git a/master/static/views_and_triggers/up/metrics.sql b/master/static/views_and_triggers/up/metrics.sql index 023af9cf5b4..ca68ea108a1 100644 --- a/master/static/views_and_triggers/up/metrics.sql +++ b/master/static/views_and_triggers/up/metrics.sql @@ -24,7 +24,7 @@ CREATE VIEW trials AS r.experiment_id, r.warm_start_checkpoint_id, r.runner_state, - r.log_signal, + r.log_policy_matched, rm.metadata AS metadata FROM trials_v2 t JOIN runs r ON t.run_id = r.id diff --git a/proto/buf.image.bin b/proto/buf.image.bin index 1321740333c..b731e53fa1d 100644 Binary files a/proto/buf.image.bin and b/proto/buf.image.bin differ diff --git a/proto/pkg/runv1/run.pb.go b/proto/pkg/runv1/run.pb.go index b2f7a1bd495..75cc2e146d7 100644 --- a/proto/pkg/runv1/run.pb.go +++ b/proto/pkg/runv1/run.pb.go @@ -224,8 +224,8 @@ type FlatRun struct { Archived bool `protobuf:"varint,21,opt,name=archived,proto3" json:"archived,omitempty"` // Project level local id of run. LocalId string `protobuf:"bytes,22,opt,name=local_id,json=localId,proto3" json:"local_id,omitempty"` - // Log signal. - LogSignal string `protobuf:"bytes,23,opt,name=log_signal,json=logSignal,proto3" json:"log_signal,omitempty"` + // Log policy matched. + LogPolicyMatched *string `protobuf:"bytes,23,opt,name=log_policy_matched,json=logPolicyMatched,proto3,oneof" json:"log_policy_matched,omitempty"` } func (x *FlatRun) Reset() { @@ -414,9 +414,9 @@ func (x *FlatRun) GetLocalId() string { return "" } -func (x *FlatRun) GetLogSignal() string { - if x != nil { - return x.LogSignal +func (x *FlatRun) GetLogPolicyMatched() string { + if x != nil && x.LogPolicyMatched != nil { + return *x.LogPolicyMatched } return "" } @@ -904,7 +904,7 @@ var file_determined_run_v1_run_proto_rawDesc = []byte{ 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x70, 0x61, 0x63, 0x68, 0x79, 0x64, 0x65, 0x72, 0x6d, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, - 0xa1, 0x0a, 0x0a, 0x07, 0x46, 0x6c, 0x61, 0x74, 0x52, 0x75, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0xcc, 0x0a, 0x0a, 0x07, 0x46, 0x6c, 0x61, 0x74, 0x52, 0x75, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, @@ -965,173 +965,176 @@ var file_determined_run_v1_run_proto_rawDesc = []byte{ 0x68, 0x69, 0x76, 0x65, 0x64, 0x18, 0x15, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x16, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x64, - 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x17, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x6f, 0x67, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x3a, - 0xa6, 0x01, 0x92, 0x41, 0xa2, 0x01, 0x0a, 0x9f, 0x01, 0xd2, 0x01, 0x02, 0x69, 0x64, 0xd2, 0x01, - 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0xd2, 0x01, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x65, 0xd2, 0x01, 0x04, 0x74, 0x61, 0x67, 0x73, 0xd2, 0x01, 0x0f, 0x63, 0x68, 0x65, - 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0xd2, 0x01, 0x10, 0x63, - 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0xd2, - 0x01, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x0c, 0x70, - 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x0c, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x0e, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x0f, 0x70, 0x61, - 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0xd2, 0x01, 0x08, - 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x73, 0x65, 0x61, - 0x72, 0x63, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, - 0x72, 0x75, 0x6e, 0x5f, 0x69, 0x64, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x68, 0x79, 0x70, 0x65, 0x72, - 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x73, - 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x42, 0x0a, - 0x0a, 0x08, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x64, - 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x65, 0x78, 0x70, 0x65, - 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x22, 0xd9, 0x11, 0x0a, 0x03, 0x52, 0x75, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x30, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x64, + 0x12, 0x31, 0x0a, 0x12, 0x6c, 0x6f, 0x67, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6d, + 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x18, 0x17, 0x20, 0x01, 0x28, 0x09, 0x48, 0x08, 0x52, 0x10, + 0x6c, 0x6f, 0x67, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, + 0x88, 0x01, 0x01, 0x3a, 0xa6, 0x01, 0x92, 0x41, 0xa2, 0x01, 0x0a, 0x9f, 0x01, 0xd2, 0x01, 0x02, + 0x69, 0x64, 0xd2, 0x01, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0xd2, + 0x01, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0xd2, 0x01, 0x04, 0x74, 0x61, 0x67, 0x73, 0xd2, 0x01, + 0x0f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x73, 0x69, 0x7a, 0x65, + 0xd2, 0x01, 0x10, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0xd2, 0x01, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, + 0xd2, 0x01, 0x0c, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0xd2, + 0x01, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0xd2, 0x01, + 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0xd2, + 0x01, 0x0f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, + 0x64, 0xd2, 0x01, 0x08, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x42, 0x18, 0x0a, 0x16, + 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, + 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x69, 0x64, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x68, + 0x79, 0x70, 0x65, 0x72, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x42, 0x12, + 0x0a, 0x10, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, + 0x63, 0x73, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x42, 0x0b, + 0x0a, 0x09, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, + 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x22, 0xd9, + 0x11, 0x0a, 0x03, 0x52, 0x75, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, + 0x65, 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, + 0x69, 0x6e, 0x65, 0x64, 0x2e, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x61, + 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, + 0x6c, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x63, 0x68, 0x65, + 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x37, 0x0a, 0x15, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x13, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x65, + 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, + 0x2b, 0x0a, 0x0f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x75, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0d, 0x65, 0x78, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x46, 0x0a, 0x0f, + 0x68, 0x79, 0x70, 0x65, 0x72, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x02, + 0x52, 0x0f, 0x68, 0x79, 0x70, 0x65, 0x72, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x73, 0x88, 0x01, 0x01, 0x12, 0x45, 0x0a, 0x0f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x5f, + 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x03, 0x52, 0x0e, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, + 0x79, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x07, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x48, 0x04, 0x52, 0x06, + 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x64, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x05, 0x48, 0x05, 0x52, 0x08, 0x64, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x10, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, + 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, + 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0e, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x12, + 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x13, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x06, 0x52, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x72, 0x63, + 0x68, 0x69, 0x76, 0x65, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x72, 0x63, + 0x68, 0x69, 0x76, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, + 0x69, 0x64, 0x18, 0x15, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x65, 0x72, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x16, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x17, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, + 0x12, 0x24, 0x0a, 0x0b, 0x66, 0x6f, 0x72, 0x6b, 0x65, 0x64, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x18, + 0x18, 0x20, 0x01, 0x28, 0x05, 0x48, 0x07, 0x52, 0x0a, 0x66, 0x6f, 0x72, 0x6b, 0x65, 0x64, 0x46, + 0x72, 0x6f, 0x6d, 0x88, 0x01, 0x01, 0x12, 0x31, 0x0a, 0x12, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x69, 0x64, 0x18, 0x19, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x08, 0x52, 0x10, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x6f, 0x6c, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x6f, 0x6c, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x02, + 0x52, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, + 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x1d, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, + 0x09, 0x75, 0x6e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x09, 0x75, 0x6e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x69, + 0x73, 0x5f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x72, 0x75, 0x6e, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0a, 0x69, 0x73, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x72, 0x75, 0x6e, 0x12, 0x51, 0x0a, 0x15, + 0x70, 0x61, 0x63, 0x68, 0x79, 0x64, 0x65, 0x72, 0x6d, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x20, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, + 0x72, 0x75, 0x63, 0x74, 0x48, 0x09, 0x52, 0x14, 0x70, 0x61, 0x63, 0x68, 0x79, 0x64, 0x65, 0x72, + 0x6d, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, + 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x73, 0x18, 0x21, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x08, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x73, 0x12, 0x36, 0x0a, 0x17, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x5f, 0x70, 0x72, 0x6f, + 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x22, 0x20, 0x01, 0x28, 0x05, 0x52, 0x15, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, + 0x73, 0x65, 0x64, 0x12, 0x4d, 0x0a, 0x0f, 0x62, 0x65, 0x73, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x23, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x2e, - 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x16, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x68, 0x65, 0x63, 0x6b, - 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0e, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x53, 0x69, 0x7a, 0x65, - 0x12, 0x29, 0x0a, 0x10, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x63, 0x68, 0x65, 0x63, - 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x37, 0x0a, 0x15, 0x73, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x13, 0x73, 0x65, - 0x61, 0x72, 0x63, 0x68, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x88, 0x01, 0x01, 0x12, 0x2b, 0x0a, 0x0f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, - 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x88, 0x01, - 0x01, 0x12, 0x46, 0x0a, 0x0f, 0x68, 0x79, 0x70, 0x65, 0x72, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, - 0x74, 0x65, 0x72, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, - 0x75, 0x63, 0x74, 0x48, 0x02, 0x52, 0x0f, 0x68, 0x79, 0x70, 0x65, 0x72, 0x70, 0x61, 0x72, 0x61, - 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x88, 0x01, 0x01, 0x12, 0x45, 0x0a, 0x0f, 0x73, 0x75, 0x6d, - 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x18, 0x0b, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x03, 0x52, 0x0e, 0x73, - 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x88, 0x01, 0x01, - 0x12, 0x1c, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, - 0x05, 0x48, 0x04, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x1f, - 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x05, - 0x48, 0x05, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, - 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x21, - 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0f, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, - 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x70, - 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x18, 0x12, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x41, 0x72, 0x63, 0x68, - 0x69, 0x76, 0x65, 0x64, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, - 0x06, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x1a, - 0x0a, 0x08, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x08, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x65, - 0x61, 0x72, 0x63, 0x68, 0x5f, 0x69, 0x64, 0x18, 0x15, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x73, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x61, 0x72, 0x63, - 0x68, 0x65, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x16, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x27, 0x0a, 0x0f, - 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, - 0x17, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x65, 0x72, 0x4d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x24, 0x0a, 0x0b, 0x66, 0x6f, 0x72, 0x6b, 0x65, 0x64, 0x5f, - 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x18, 0x20, 0x01, 0x28, 0x05, 0x48, 0x07, 0x52, 0x0a, 0x66, 0x6f, - 0x72, 0x6b, 0x65, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x88, 0x01, 0x01, 0x12, 0x31, 0x0a, 0x12, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x69, - 0x64, 0x18, 0x19, 0x20, 0x01, 0x28, 0x09, 0x48, 0x08, 0x52, 0x10, 0x65, 0x78, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x23, - 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x6f, 0x6f, 0x6c, 0x18, - 0x1a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, - 0x6f, 0x6f, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, - 0x1b, 0x20, 0x01, 0x28, 0x02, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, - 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x1c, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x1d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x6e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x18, - 0x1e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x75, 0x6e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, - 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x72, 0x75, 0x6e, 0x18, - 0x1f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x72, 0x75, - 0x6e, 0x12, 0x51, 0x0a, 0x15, 0x70, 0x61, 0x63, 0x68, 0x79, 0x64, 0x65, 0x72, 0x6d, 0x5f, 0x69, - 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x20, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x09, 0x52, 0x14, 0x70, 0x61, 0x63, - 0x68, 0x79, 0x64, 0x65, 0x72, 0x6d, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x88, 0x01, 0x01, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x73, - 0x18, 0x21, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x73, - 0x12, 0x36, 0x0a, 0x17, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, - 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x22, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x50, - 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x12, 0x4d, 0x0a, 0x0f, 0x62, 0x65, 0x73, 0x74, - 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x23, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x24, 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, 0x74, - 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x57, - 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x0e, 0x62, 0x65, 0x73, 0x74, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x51, 0x0a, 0x11, 0x6c, 0x61, 0x74, 0x65, 0x73, - 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x24, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, - 0x74, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, - 0x57, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x10, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x50, 0x0a, 0x0f, 0x62, 0x65, - 0x73, 0x74, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x25, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, - 0x2e, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x57, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x0e, 0x62, 0x65, - 0x73, 0x74, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, - 0x72, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x26, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x72, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x26, 0x0a, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x5f, 0x63, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x27, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0d, 0x77, 0x61, 0x6c, 0x6c, 0x43, 0x6c, - 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x3b, 0x0a, 0x1a, 0x77, 0x61, 0x72, 0x6d, 0x5f, - 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x61, 0x72, - 0x6d, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x55, 0x75, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x73, - 0x18, 0x29, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x73, 0x12, - 0x31, 0x0a, 0x12, 0x6c, 0x6f, 0x67, 0x5f, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x64, 0x61, 0x79, 0x73, 0x18, 0x2a, 0x20, 0x01, 0x28, 0x05, 0x48, 0x0a, 0x52, 0x10, 0x6c, - 0x6f, 0x67, 0x52, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x79, 0x73, 0x88, - 0x01, 0x01, 0x3a, 0xa6, 0x01, 0x92, 0x41, 0xa2, 0x01, 0x0a, 0x9f, 0x01, 0xd2, 0x01, 0x02, 0x69, - 0x64, 0xd2, 0x01, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0xd2, 0x01, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0xd2, 0x01, 0x04, 0x74, 0x61, 0x67, 0x73, 0xd2, 0x01, 0x0f, - 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0xd2, - 0x01, 0x10, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0xd2, 0x01, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0xd2, - 0x01, 0x0c, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0xd2, 0x01, - 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x0e, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0xd2, 0x01, - 0x0f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, - 0xd2, 0x01, 0x08, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, - 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x69, 0x64, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x68, 0x79, - 0x70, 0x65, 0x72, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x42, 0x12, 0x0a, - 0x10, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, - 0x73, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x42, 0x0b, 0x0a, - 0x09, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x66, 0x6f, 0x72, 0x6b, - 0x65, 0x64, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x65, 0x78, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x69, 0x64, 0x42, 0x18, - 0x0a, 0x16, 0x5f, 0x70, 0x61, 0x63, 0x68, 0x79, 0x64, 0x65, 0x72, 0x6d, 0x5f, 0x69, 0x6e, 0x74, - 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x6c, 0x6f, 0x67, - 0x5f, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x79, 0x73, 0x42, - 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x65, - 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2d, 0x61, 0x69, 0x2f, 0x64, 0x65, 0x74, 0x65, - 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x6b, 0x67, - 0x2f, 0x72, 0x75, 0x6e, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x57, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, + 0x61, 0x64, 0x52, 0x0e, 0x62, 0x65, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x51, 0x0a, 0x11, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x24, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, + 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, 0x74, 0x72, 0x69, 0x61, 0x6c, + 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x57, 0x6f, 0x72, 0x6b, 0x6c, + 0x6f, 0x61, 0x64, 0x52, 0x10, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x50, 0x0a, 0x0f, 0x62, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x68, + 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x25, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, + 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, 0x74, 0x72, 0x69, 0x61, + 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x57, + 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x0e, 0x62, 0x65, 0x73, 0x74, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x75, 0x6e, 0x6e, 0x65, + 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x26, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, + 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x77, 0x61, + 0x6c, 0x6c, 0x5f, 0x63, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x27, 0x20, + 0x01, 0x28, 0x01, 0x52, 0x0d, 0x77, 0x61, 0x6c, 0x6c, 0x43, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, + 0x6d, 0x65, 0x12, 0x3b, 0x0a, 0x1a, 0x77, 0x61, 0x72, 0x6d, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x75, 0x75, 0x69, 0x64, + 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x61, 0x72, 0x6d, 0x53, 0x74, 0x61, 0x72, + 0x74, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x55, 0x75, 0x69, 0x64, 0x12, + 0x19, 0x0a, 0x08, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x29, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x73, 0x12, 0x31, 0x0a, 0x12, 0x6c, 0x6f, + 0x67, 0x5f, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x79, 0x73, + 0x18, 0x2a, 0x20, 0x01, 0x28, 0x05, 0x48, 0x0a, 0x52, 0x10, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x79, 0x73, 0x88, 0x01, 0x01, 0x3a, 0xa6, 0x01, + 0x92, 0x41, 0xa2, 0x01, 0x0a, 0x9f, 0x01, 0xd2, 0x01, 0x02, 0x69, 0x64, 0xd2, 0x01, 0x0a, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0xd2, 0x01, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0xd2, 0x01, 0x04, 0x74, 0x61, 0x67, 0x73, 0xd2, 0x01, 0x0f, 0x63, 0x68, 0x65, 0x63, 0x6b, + 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0xd2, 0x01, 0x10, 0x63, 0x68, 0x65, + 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0xd2, 0x01, 0x0a, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x0c, 0x70, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x0c, 0x77, 0x6f, 0x72, 0x6b, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0xd2, 0x01, 0x0f, 0x70, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0xd2, 0x01, 0x08, 0x61, 0x72, + 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x75, + 0x6e, 0x5f, 0x69, 0x64, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x68, 0x79, 0x70, 0x65, 0x72, 0x70, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x73, 0x75, 0x6d, + 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x42, 0x0a, 0x0a, 0x08, + 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x64, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x66, 0x6f, 0x72, 0x6b, 0x65, 0x64, 0x5f, 0x66, 0x72, + 0x6f, 0x6d, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, + 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x69, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x70, 0x61, + 0x63, 0x68, 0x79, 0x64, 0x65, 0x72, 0x6d, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x72, 0x65, 0x74, 0x65, + 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x79, 0x73, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, + 0x6e, 0x65, 0x64, 0x2d, 0x61, 0x69, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, + 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x72, 0x75, 0x6e, 0x76, + 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/pkg/trialv1/trial.pb.go b/proto/pkg/trialv1/trial.pb.go index 1646521c0e2..9769cbc4bde 100644 --- a/proto/pkg/trialv1/trial.pb.go +++ b/proto/pkg/trialv1/trial.pb.go @@ -524,8 +524,8 @@ type Trial struct { // metadata associated with the trial (based off the metadata stored in the // run). Metadata *_struct.Struct `protobuf:"bytes,23,opt,name=metadata,proto3,oneof" json:"metadata,omitempty"` - // The log signals. - LogSignal string `protobuf:"bytes,24,opt,name=log_signal,json=logSignal,proto3" json:"log_signal,omitempty"` + // Log Policy Matched. + LogPolicyMatched *string `protobuf:"bytes,24,opt,name=log_policy_matched,json=logPolicyMatched,proto3,oneof" json:"log_policy_matched,omitempty"` } func (x *Trial) Reset() { @@ -715,9 +715,9 @@ func (x *Trial) GetMetadata() *_struct.Struct { return nil } -func (x *Trial) GetLogSignal() string { - if x != nil { - return x.LogSignal +func (x *Trial) GetLogPolicyMatched() string { + if x != nil && x.LogPolicyMatched != nil { + return *x.LogPolicyMatched } return "" } @@ -1399,7 +1399,7 @@ var file_determined_trial_v1_trial_proto_rawDesc = []byte{ 0x68, 0x65, 0x73, 0x3a, 0x34, 0x92, 0x41, 0x31, 0x0a, 0x2f, 0xd2, 0x01, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0xd2, 0x01, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0xd2, 0x01, 0x0a, 0x6e, 0x75, 0x6d, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x73, 0xd2, 0x01, 0x0d, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x5f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x22, 0xfc, 0x09, 0x0a, 0x05, 0x54, 0x72, + 0x6c, 0x5f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x22, 0xa7, 0x0a, 0x0a, 0x05, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x65, 0x78, 0x70, 0x65, @@ -1469,183 +1469,186 @@ var file_determined_trial_v1_trial_proto_rawDesc = []byte{ 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x17, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x01, 0x52, 0x08, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, - 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x18, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, - 0x6f, 0x67, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x3a, 0x5a, 0x92, 0x41, 0x57, 0x0a, 0x55, 0xd2, - 0x01, 0x02, 0x69, 0x64, 0xd2, 0x01, 0x0c, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, - 0x74, 0x49, 0x64, 0xd2, 0x01, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0xd2, - 0x01, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0xd2, 0x01, 0x08, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x73, 0xd2, 0x01, 0x07, 0x68, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0xd2, 0x01, 0x15, 0x74, - 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x63, 0x65, - 0x73, 0x73, 0x65, 0x64, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x72, 0x65, 0x74, - 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x79, 0x73, 0x42, 0x0b, 0x0a, 0x09, 0x5f, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x9b, 0x03, 0x0a, 0x19, 0x54, 0x72, 0x69, - 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, - 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x49, - 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, - 0x12, 0x19, 0x0a, 0x08, 0x67, 0x70, 0x75, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x67, 0x70, 0x75, 0x55, 0x75, 0x69, 0x64, 0x12, 0x62, 0x0a, 0x0b, 0x6d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x41, 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, 0x74, 0x72, - 0x69, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x66, - 0x69, 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, - 0x2e, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, - 0x79, 0x70, 0x65, 0x52, 0x0a, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, 0x79, 0x70, 0x65, 0x22, - 0x9b, 0x01, 0x0a, 0x12, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x54, 0x79, 0x70, 0x65, 0x12, 0x24, 0x0a, 0x20, 0x50, 0x52, 0x4f, 0x46, 0x49, 0x4c, - 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, - 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1f, 0x0a, 0x1b, - 0x50, 0x52, 0x4f, 0x46, 0x49, 0x4c, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, - 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x10, 0x01, 0x12, 0x1f, 0x0a, - 0x1b, 0x50, 0x52, 0x4f, 0x46, 0x49, 0x4c, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, - 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x1d, - 0x0a, 0x19, 0x50, 0x52, 0x4f, 0x46, 0x49, 0x4c, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x52, 0x49, - 0x43, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x49, 0x53, 0x43, 0x10, 0x03, 0x3a, 0x17, 0x92, - 0x41, 0x14, 0x0a, 0x12, 0xd2, 0x01, 0x08, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0xd2, - 0x01, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x81, 0x02, 0x0a, 0x19, 0x54, 0x72, 0x69, 0x61, 0x6c, - 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x02, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, - 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x05, 0x52, 0x07, 0x62, - 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x73, 0x12, 0x46, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, - 0x74, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x50, 0x72, - 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4c, 0x61, 0x62, 0x65, - 0x6c, 0x73, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x3a, 0x2e, 0x92, 0x41, 0x2b, 0x0a, - 0x29, 0xd2, 0x01, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0xd2, 0x01, 0x07, 0x62, 0x61, 0x74, - 0x63, 0x68, 0x65, 0x73, 0xd2, 0x01, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x73, 0xd2, 0x01, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x22, 0xda, 0x01, 0x0a, 0x0e, 0x54, - 0x72, 0x69, 0x61, 0x6c, 0x45, 0x61, 0x72, 0x6c, 0x79, 0x45, 0x78, 0x69, 0x74, 0x12, 0x48, 0x0a, - 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x31, 0x0a, 0x12, 0x6c, 0x6f, 0x67, + 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x18, + 0x18, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x10, 0x6c, 0x6f, 0x67, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x88, 0x01, 0x01, 0x3a, 0x5a, 0x92, 0x41, + 0x57, 0x0a, 0x55, 0xd2, 0x01, 0x02, 0x69, 0x64, 0xd2, 0x01, 0x0c, 0x65, 0x78, 0x70, 0x65, 0x72, + 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0xd2, 0x01, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, + 0x69, 0x6d, 0x65, 0xd2, 0x01, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0xd2, 0x01, 0x08, 0x72, 0x65, + 0x73, 0x74, 0x61, 0x72, 0x74, 0x73, 0xd2, 0x01, 0x07, 0x68, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, + 0xd2, 0x01, 0x15, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x50, + 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x6c, 0x6f, 0x67, + 0x5f, 0x72, 0x65, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x79, 0x73, 0x42, + 0x0b, 0x0a, 0x09, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x15, 0x0a, 0x13, + 0x5f, 0x6c, 0x6f, 0x67, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6d, 0x61, 0x74, 0x63, + 0x68, 0x65, 0x64, 0x22, 0x9b, 0x03, 0x0a, 0x19, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x50, 0x72, 0x6f, + 0x66, 0x69, 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4c, 0x61, 0x62, 0x65, 0x6c, + 0x73, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x07, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x19, 0x0a, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x67, + 0x70, 0x75, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, + 0x70, 0x75, 0x55, 0x75, 0x69, 0x64, 0x12, 0x62, 0x0a, 0x0b, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x41, 0x2e, 0x64, 0x65, + 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x76, + 0x31, 0x2e, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x72, 0x4d, + 0x65, 0x74, 0x72, 0x69, 0x63, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x2e, 0x50, 0x72, 0x6f, 0x66, + 0x69, 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, + 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, 0x79, 0x70, 0x65, 0x22, 0x9b, 0x01, 0x0a, 0x12, 0x50, + 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x24, 0x0a, 0x20, 0x50, 0x52, 0x4f, 0x46, 0x49, 0x4c, 0x45, 0x52, 0x5f, 0x4d, 0x45, + 0x54, 0x52, 0x49, 0x43, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, + 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1f, 0x0a, 0x1b, 0x50, 0x52, 0x4f, 0x46, 0x49, + 0x4c, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x10, 0x01, 0x12, 0x1f, 0x0a, 0x1b, 0x50, 0x52, 0x4f, 0x46, + 0x49, 0x4c, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x54, 0x49, 0x4d, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x1d, 0x0a, 0x19, 0x50, 0x52, 0x4f, + 0x46, 0x49, 0x4c, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x4d, 0x49, 0x53, 0x43, 0x10, 0x03, 0x3a, 0x17, 0x92, 0x41, 0x14, 0x0a, 0x12, 0xd2, + 0x01, 0x08, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x81, 0x02, 0x0a, 0x19, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x66, 0x69, + 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, + 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x02, 0x52, + 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x61, 0x74, 0x63, 0x68, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x05, 0x52, 0x07, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, + 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x12, 0x46, 0x0a, + 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, 0x74, 0x72, 0x69, 0x61, 0x6c, - 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x45, 0x61, 0x72, 0x6c, 0x79, 0x45, 0x78, - 0x69, 0x74, 0x2e, 0x45, 0x78, 0x69, 0x74, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, - 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x0c, 0x45, 0x78, 0x69, 0x74, 0x65, - 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x19, 0x45, 0x58, 0x49, 0x54, 0x45, - 0x44, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x45, 0x58, 0x49, 0x54, 0x45, 0x44, - 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, - 0x48, 0x50, 0x10, 0x01, 0x12, 0x21, 0x0a, 0x1d, 0x45, 0x58, 0x49, 0x54, 0x45, 0x44, 0x5f, 0x52, - 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, - 0x49, 0x44, 0x5f, 0x48, 0x50, 0x10, 0x03, 0x3a, 0x0e, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0xd2, 0x01, - 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x7a, 0x0a, 0x0e, 0x52, 0x65, 0x6e, 0x64, 0x65, - 0x7a, 0x76, 0x6f, 0x75, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x61, 0x6e, 0x6b, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x72, 0x61, 0x6e, 0x6b, 0x12, 0x14, 0x0a, 0x05, 0x73, - 0x6c, 0x6f, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x05, 0x52, 0x05, 0x73, 0x6c, 0x6f, 0x74, - 0x73, 0x3a, 0x20, 0x92, 0x41, 0x1d, 0x0a, 0x1b, 0xd2, 0x01, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x65, 0x73, 0xd2, 0x01, 0x04, 0x72, 0x61, 0x6e, 0x6b, 0xd2, 0x01, 0x05, 0x73, 0x6c, - 0x6f, 0x74, 0x73, 0x22, 0x3a, 0x0a, 0x13, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x52, 0x75, 0x6e, 0x6e, - 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x3a, 0x0d, 0x92, 0x41, 0x0a, 0x0a, 0x08, 0xd2, 0x01, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, - 0xc3, 0x02, 0x0a, 0x0c, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, - 0x12, 0x19, 0x0a, 0x08, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x07, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74, - 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x0a, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x12, 0x2c, 0x0a, - 0x0f, 0x73, 0x74, 0x65, 0x70, 0x73, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x0e, 0x73, 0x74, 0x65, 0x70, 0x73, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x40, 0x0a, 0x0b, 0x72, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x01, 0x52, 0x0a, - 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, - 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, - 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, 0x07, 0x6d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x3a, 0x29, 0x92, 0x41, 0x26, 0x0a, 0x24, 0xd2, 0x01, 0x08, - 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x0c, 0x74, 0x72, 0x69, 0x61, 0x6c, - 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, - 0x73, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x73, 0x74, 0x65, 0x70, 0x73, 0x5f, 0x63, 0x6f, 0x6d, 0x70, - 0x6c, 0x65, 0x74, 0x65, 0x64, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x22, 0xfb, 0x02, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, - 0x73, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x72, 0x69, 0x61, 0x6c, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x74, 0x72, 0x69, 0x61, 0x6c, - 0x49, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x6d, 0x65, 0x74, - 0x72, 0x69, 0x63, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, - 0x75, 0x63, 0x74, 0x52, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x12, 0x23, 0x0a, 0x0d, - 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x65, - 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, - 0x0c, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x0a, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x12, - 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x3a, 0x5c, 0x92, 0x41, 0x59, 0x0a, 0x57, 0xd2, 0x01, 0x08, 0x74, - 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0xd2, 0x01, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0xd2, 0x01, 0x0d, 0x74, - 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0xd2, 0x01, 0x08, 0x61, - 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0xd2, 0x01, 0x02, 0x69, 0x64, 0xd2, 0x01, 0x0c, 0x74, - 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x05, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x22, 0xda, 0x02, 0x0a, 0x0f, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x72, 0x69, 0x61, 0x6c, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x74, 0x72, 0x69, 0x61, 0x6c, - 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x68, 0x65, - 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x55, 0x75, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x08, 0x6d, - 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, - 0x07, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x28, 0x0a, 0x0d, 0x6d, - 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x05, 0x48, 0x01, 0x52, 0x0c, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x5d, 0x0a, 0x16, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, - 0x65, 0x64, 0x2e, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x69, 0x61, - 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x54, 0x79, 0x70, 0x65, 0x52, - 0x13, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, - 0x54, 0x79, 0x70, 0x65, 0x3a, 0x3b, 0x92, 0x41, 0x38, 0x0a, 0x36, 0xd2, 0x01, 0x08, 0x74, 0x72, - 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x0f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x5f, 0x75, 0x75, 0x69, 0x64, 0xd2, 0x01, 0x16, 0x74, 0x72, 0x69, 0x61, 0x6c, - 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x5f, 0x74, 0x79, 0x70, - 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x42, 0x10, - 0x0a, 0x0e, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x2a, 0xb8, 0x02, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, - 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, - 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, - 0x45, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x41, 0x55, - 0x53, 0x45, 0x44, 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, - 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x45, 0x44, - 0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, - 0x50, 0x49, 0x4e, 0x47, 0x5f, 0x4b, 0x49, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, 0x1c, 0x0a, - 0x18, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x5f, - 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x05, 0x12, 0x18, 0x0a, 0x14, 0x53, - 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x5f, 0x45, 0x52, - 0x52, 0x4f, 0x52, 0x10, 0x06, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x43, - 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x45, 0x44, 0x10, 0x07, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x54, 0x41, - 0x54, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x08, 0x12, 0x0f, - 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x09, 0x12, - 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x51, 0x55, 0x45, 0x55, 0x45, 0x44, 0x10, - 0x0a, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x55, 0x4c, 0x4c, 0x49, - 0x4e, 0x47, 0x10, 0x0b, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, - 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x0c, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, - 0x45, 0x5f, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x0d, 0x2a, 0x8b, 0x01, 0x0a, 0x13, - 0x54, 0x72, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x26, 0x0a, 0x22, 0x54, 0x52, 0x49, 0x41, 0x4c, 0x5f, 0x53, 0x4f, 0x55, - 0x52, 0x43, 0x45, 0x5f, 0x49, 0x4e, 0x46, 0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x54, - 0x52, 0x49, 0x41, 0x4c, 0x5f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x4e, 0x46, 0x4f, - 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x46, 0x45, 0x52, 0x45, 0x4e, 0x43, 0x45, 0x10, - 0x01, 0x12, 0x26, 0x0a, 0x22, 0x54, 0x52, 0x49, 0x41, 0x4c, 0x5f, 0x53, 0x4f, 0x55, 0x52, 0x43, - 0x45, 0x5f, 0x49, 0x4e, 0x46, 0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, 0x49, 0x4e, 0x45, - 0x5f, 0x54, 0x55, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, - 0x65, 0x64, 0x2d, 0x61, 0x69, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x72, 0x69, 0x61, 0x6c, - 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, + 0x72, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x06, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x73, 0x3a, 0x2e, 0x92, 0x41, 0x2b, 0x0a, 0x29, 0xd2, 0x01, 0x06, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0xd2, 0x01, 0x07, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0xd2, + 0x01, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0xd2, 0x01, 0x06, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x73, 0x22, 0xda, 0x01, 0x0a, 0x0e, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x45, + 0x61, 0x72, 0x6c, 0x79, 0x45, 0x78, 0x69, 0x74, 0x12, 0x48, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, + 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x54, + 0x72, 0x69, 0x61, 0x6c, 0x45, 0x61, 0x72, 0x6c, 0x79, 0x45, 0x78, 0x69, 0x74, 0x2e, 0x45, 0x78, + 0x69, 0x74, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x0c, 0x45, 0x78, 0x69, 0x74, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x19, 0x45, 0x58, 0x49, 0x54, 0x45, 0x44, 0x5f, 0x52, 0x45, 0x41, + 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x45, 0x58, 0x49, 0x54, 0x45, 0x44, 0x5f, 0x52, 0x45, 0x41, 0x53, + 0x4f, 0x4e, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x48, 0x50, 0x10, 0x01, 0x12, + 0x21, 0x0a, 0x1d, 0x45, 0x58, 0x49, 0x54, 0x45, 0x44, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, + 0x5f, 0x49, 0x4e, 0x49, 0x54, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x48, 0x50, + 0x10, 0x03, 0x3a, 0x0e, 0x92, 0x41, 0x0b, 0x0a, 0x09, 0xd2, 0x01, 0x06, 0x72, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x22, 0x7a, 0x0a, 0x0e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x7a, 0x76, 0x6f, 0x75, 0x73, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x61, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x04, 0x72, 0x61, 0x6e, 0x6b, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x6c, 0x6f, 0x74, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x05, 0x52, 0x05, 0x73, 0x6c, 0x6f, 0x74, 0x73, 0x3a, 0x20, 0x92, 0x41, + 0x1d, 0x0a, 0x1b, 0xd2, 0x01, 0x09, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0xd2, + 0x01, 0x04, 0x72, 0x61, 0x6e, 0x6b, 0xd2, 0x01, 0x05, 0x73, 0x6c, 0x6f, 0x74, 0x73, 0x22, 0x3a, + 0x0a, 0x13, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x52, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x3a, 0x0d, 0x92, 0x41, 0x0a, + 0x0a, 0x08, 0xd2, 0x01, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0xc3, 0x02, 0x0a, 0x0c, 0x54, + 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x74, + 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x74, + 0x72, 0x69, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, + 0x72, 0x75, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x74, 0x72, + 0x69, 0x61, 0x6c, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x0f, 0x73, 0x74, 0x65, 0x70, + 0x73, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x05, 0x48, 0x00, 0x52, 0x0e, 0x73, 0x74, 0x65, 0x70, 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x40, 0x0a, 0x0b, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x01, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x54, 0x69, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x72, + 0x69, 0x63, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x64, 0x65, 0x74, 0x65, + 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, + 0x73, 0x3a, 0x29, 0x92, 0x41, 0x26, 0x0a, 0x24, 0xd2, 0x01, 0x08, 0x74, 0x72, 0x69, 0x61, 0x6c, + 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x0c, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x72, 0x75, 0x6e, 0x5f, + 0x69, 0x64, 0xd2, 0x01, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x42, 0x12, 0x0a, 0x10, + 0x5f, 0x73, 0x74, 0x65, 0x70, 0x73, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, + 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x22, 0xfb, 0x02, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x35, 0x0a, + 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x6e, 0x64, + 0x54, 0x69, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x07, + 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x5f, 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, + 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x72, 0x69, 0x61, + 0x6c, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, + 0x74, 0x72, 0x69, 0x61, 0x6c, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x3a, 0x5c, 0x92, 0x41, 0x59, 0x0a, 0x57, 0xd2, 0x01, 0x08, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, + 0x69, 0x64, 0xd2, 0x01, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0xd2, 0x01, 0x07, + 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0xd2, 0x01, 0x0d, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, + 0x62, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0xd2, 0x01, 0x08, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, + 0x65, 0x64, 0xd2, 0x01, 0x02, 0x69, 0x64, 0xd2, 0x01, 0x0c, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, + 0x72, 0x75, 0x6e, 0x5f, 0x69, 0x64, 0xd2, 0x01, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, 0xda, + 0x02, 0x0a, 0x0f, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, + 0x66, 0x6f, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x27, 0x0a, + 0x0f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x75, 0x75, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x55, 0x75, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x08, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5f, + 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x07, 0x6d, 0x6f, 0x64, 0x65, + 0x6c, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x28, 0x0a, 0x0d, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5f, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x48, 0x01, 0x52, + 0x0c, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, + 0x12, 0x5d, 0x0a, 0x16, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x28, 0x2e, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2e, 0x74, 0x72, + 0x69, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x72, 0x69, 0x61, 0x6c, 0x53, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x54, 0x79, 0x70, 0x65, 0x52, 0x13, 0x74, 0x72, 0x69, 0x61, + 0x6c, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x54, 0x79, 0x70, 0x65, 0x3a, + 0x3b, 0x92, 0x41, 0x38, 0x0a, 0x36, 0xd2, 0x01, 0x08, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x69, + 0x64, 0xd2, 0x01, 0x0f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x75, + 0x75, 0x69, 0x64, 0xd2, 0x01, 0x16, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x0b, 0x0a, 0x09, + 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x6c, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2a, 0xb8, 0x02, 0x0a, 0x05, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, + 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x01, 0x12, 0x10, + 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x41, 0x55, 0x53, 0x45, 0x44, 0x10, 0x02, + 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, + 0x4e, 0x47, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x19, 0x0a, + 0x15, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x5f, + 0x4b, 0x49, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x54, 0x41, 0x54, + 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, + 0x45, 0x54, 0x45, 0x44, 0x10, 0x05, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, + 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x06, + 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, + 0x45, 0x44, 0x10, 0x07, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x43, 0x4f, + 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x08, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x41, + 0x54, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x09, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, + 0x41, 0x54, 0x45, 0x5f, 0x51, 0x55, 0x45, 0x55, 0x45, 0x44, 0x10, 0x0a, 0x12, 0x11, 0x0a, 0x0d, + 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x55, 0x4c, 0x4c, 0x49, 0x4e, 0x47, 0x10, 0x0b, 0x12, + 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, + 0x47, 0x10, 0x0c, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x52, 0x55, 0x4e, + 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x0d, 0x2a, 0x8b, 0x01, 0x0a, 0x13, 0x54, 0x72, 0x69, 0x61, 0x6c, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x54, 0x79, 0x70, 0x65, 0x12, 0x26, + 0x0a, 0x22, 0x54, 0x52, 0x49, 0x41, 0x4c, 0x5f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x49, + 0x4e, 0x46, 0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x54, 0x52, 0x49, 0x41, 0x4c, 0x5f, + 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x4e, 0x46, 0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x49, 0x4e, 0x46, 0x45, 0x52, 0x45, 0x4e, 0x43, 0x45, 0x10, 0x01, 0x12, 0x26, 0x0a, 0x22, + 0x54, 0x52, 0x49, 0x41, 0x4c, 0x5f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x49, 0x4e, 0x46, + 0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, 0x49, 0x4e, 0x45, 0x5f, 0x54, 0x55, 0x4e, 0x49, + 0x4e, 0x47, 0x10, 0x02, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2d, 0x61, 0x69, + 0x2f, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x72, 0x69, 0x61, 0x6c, 0x76, 0x31, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/src/determined/run/v1/run.proto b/proto/src/determined/run/v1/run.proto index f25b1a72776..9ef60d1fa38 100644 --- a/proto/src/determined/run/v1/run.proto +++ b/proto/src/determined/run/v1/run.proto @@ -119,8 +119,8 @@ message FlatRun { bool archived = 21; // Project level local id of run. string local_id = 22; - // Log signal. - string log_signal = 23; + // Log policy matched. + optional string log_policy_matched = 23; } // Flat run respresentation. diff --git a/proto/src/determined/trial/v1/trial.proto b/proto/src/determined/trial/v1/trial.proto index d6e56b7cb24..a75b7c458e4 100644 --- a/proto/src/determined/trial/v1/trial.proto +++ b/proto/src/determined/trial/v1/trial.proto @@ -149,8 +149,8 @@ message Trial { // metadata associated with the trial (based off the metadata stored in the // run). optional google.protobuf.Struct metadata = 23; - // The log signals. - string log_signal = 24; + // Log Policy Matched. + optional string log_policy_matched = 24; } // TrialProfilerMetricLabels are the labels for a single series, where a series diff --git a/schemas/expconf/v0/experiment.json b/schemas/expconf/v0/experiment.json index eb5c17cf690..972713548cd 100644 --- a/schemas/expconf/v0/experiment.json +++ b/schemas/expconf/v0/experiment.json @@ -121,10 +121,9 @@ "array", "null" ], + "$comment": "setting default to [] lets the actual default always comes from WithDefaults()", "default": [], - "items": { - "$ref": "http://determined.ai/schemas/expconf/v0/log-policy.json" - } + "optionalRef": "http://determined.ai/schemas/expconf/v0/log-policies.json" }, "retention_policy": { "type": [ diff --git a/schemas/expconf/v0/log-action.json b/schemas/expconf/v0/log-action.json index 6dcabee6727..193ac032a02 100644 --- a/schemas/expconf/v0/log-action.json +++ b/schemas/expconf/v0/log-action.json @@ -2,32 +2,25 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://determined.ai/schemas/expconf/v0/log-action.json", "title": "LogAction", - "$comment": "this is a union of all possible properties, with validation for the common properties", - "if": { - "required": [ - "type" + "union": { + "defaultMessage": "must be one of \"cancel_retries\" or \"exclude_node\"", + "items": [ + { + "unionKey": "never", + "const": "cancel_retries" + }, + { + "unionKey": "never", + "const": "exclude_node" + }, + { + "unionKey": "const:type=cancel_retries", + "$ref": "http://determined.ai/schemas/expconf/v0/log-legacy-action-cancel-retries.json" + }, + { + "unionKey": "const:type=exclude_node", + "$ref": "http://determined.ai/schemas/expconf/v0/log-legacy-action-exclude-node.json" + } ] - }, - "then": { - "union": { - "defaultMessage": "is not an object where object[\"type\"] is one of 'cancel_retries' or 'exclude_node'", - "items": [ - { - "unionKey": "const:type=cancel_retries", - "$ref": "http://determined.ai/schemas/expconf/v0/log-action-cancel-retries.json" - }, - { - "unionKey": "const:type=exclude_node", - "$ref": "http://determined.ai/schemas/expconf/v0/log-action-exclude-node.json" - } - ] - } - }, - "additionalProperties": false, - "eventuallyRequired": [ - "type" - ], - "properties": { - "type": true } } diff --git a/schemas/expconf/v0/log-action-cancel-retries.json b/schemas/expconf/v0/log-legacy-action-cancel-retries.json similarity index 67% rename from schemas/expconf/v0/log-action-cancel-retries.json rename to schemas/expconf/v0/log-legacy-action-cancel-retries.json index 57105184a0c..9f120b919df 100644 --- a/schemas/expconf/v0/log-action-cancel-retries.json +++ b/schemas/expconf/v0/log-legacy-action-cancel-retries.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://determined.ai/schemas/expconf/v0/log-action-cancel-retries.json", - "title": "LogActionCancelRetries", + "$id": "http://determined.ai/schemas/expconf/v0/log-legacy-action-cancel-retries.json", + "title": "LogLegacyActionCancelRetries", "additionalProperties": false, "type": "object", "required": [ diff --git a/schemas/expconf/v0/log-action-exclude-node.json b/schemas/expconf/v0/log-legacy-action-exclude-node.json similarity index 67% rename from schemas/expconf/v0/log-action-exclude-node.json rename to schemas/expconf/v0/log-legacy-action-exclude-node.json index 4442a9fba5d..523736533f4 100644 --- a/schemas/expconf/v0/log-action-exclude-node.json +++ b/schemas/expconf/v0/log-legacy-action-exclude-node.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "http://determined.ai/schemas/expconf/v0/log-action-exclude-node.json", - "title": "LogActionExcludeNode", + "$id": "http://determined.ai/schemas/expconf/v0/log-legacy-action-exclude-node.json", + "title": "LogLegacyActionExcludeNode", "additionalProperties": false, "type": "object", "required": [ diff --git a/schemas/expconf/v0/log-policies.json b/schemas/expconf/v0/log-policies.json new file mode 100644 index 00000000000..475594526e4 --- /dev/null +++ b/schemas/expconf/v0/log-policies.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://determined.ai/schemas/expconf/v0/log-policies.json", + "title": "LogPoliciesConfig", + "type": "array", + "items": { + "$ref": "http://determined.ai/schemas/expconf/v0/log-policy.json" + } +} diff --git a/schemas/expconf/v0/log-policy.json b/schemas/expconf/v0/log-policy.json index 35c787f85a5..687b8d6e67c 100644 --- a/schemas/expconf/v0/log-policy.json +++ b/schemas/expconf/v0/log-policy.json @@ -3,22 +3,51 @@ "$id": "http://determined.ai/schemas/expconf/v0/log-policy.json", "title": "LogPolicy", "additionalProperties": false, - "required": [ - "pattern", - "action" - ], "type": "object", "properties": { + "name": { + "type": [ + "string", + "null" + ], + "$comment": "Legacy log policy doesn't have a name. Legacy log policy will be deprecated.", + "default": null + }, "pattern": { "type": [ - "string" - ] + "string", + "null" + ], + "$comment": "Pattern can be null. So user can override it to disable the default log polices.", + "default": null }, "action": { "type": [ - "object" + "string", + "object", + "null" ], - "$ref": "http://determined.ai/schemas/expconf/v0/log-action.json" + "optionalRef": "http://determined.ai/schemas/expconf/v0/log-action.json", + "default": null + } + }, + "checks": { + "\"name\" must be set, and \"pattern\" is also required unless you intend to disable an existing policy": { + "anyOf": [ + { + "required": [ + "name" + ], + "$comment": "modern policy requirement" + }, + { + "required": [ + "pattern", + "action" + ], + "$comment": "legacy policy requirements" + } + ] } } } diff --git a/schemas/test_cases/v0/experiment.yaml b/schemas/test_cases/v0/experiment.yaml index 62c20fe1fd1..48542e28d07 100644 --- a/schemas/test_cases/v0/experiment.yaml +++ b/schemas/test_cases/v0/experiment.yaml @@ -196,7 +196,11 @@ add_capabilities: [] drop_capabilities: [] hyperparameters: {} - log_policies: [] + log_policies: + - name: "*" + pattern: "*" + - name: "*" + pattern: "*" labels: [] max_restarts: 5 min_checkpoint_period: diff --git a/schemas/test_cases/v0/log_policies.yaml b/schemas/test_cases/v0/log_policies.yaml new file mode 100644 index 00000000000..d12be1a88c4 --- /dev/null +++ b/schemas/test_cases/v0/log_policies.yaml @@ -0,0 +1,213 @@ +# Log Action Tests +- name: valid log action + sane_as: + - http://determined.ai/schemas/expconf/v0/log-action.json + case: + cancel_retries + +- name: invalid log action + sanity_errors: + http://determined.ai/schemas/expconf/v0/log-action.json: + - "must be one of \"cancel_retries\" or \"exclude_node\"" + case: + invalid_action + +- name: legacy log action cancel_retries + sane_as: + - http://determined.ai/schemas/expconf/v0/log-legacy-action-cancel-retries.json + case: + type: cancel_retries + +- name: legacy log action exclude_node + sane_as: + - http://determined.ai/schemas/expconf/v0/log-legacy-action-exclude-node.json + case: + type: exclude_node + +- name: invalid legacy log action + sanity_errors: + http://determined.ai/schemas/expconf/v0/log-action.json: + - "must be one of \"cancel_retries\" or \"exclude_node\"" + case: + type: invalid_action + +# Log Policy Tests +- name: valid log policy + sane_as: + - http://determined.ai/schemas/expconf/v0/log-policy.json + case: + name: policy name + pattern: a + action: cancel_retries + +- name: valid legacy log policy + sane_as: + - http://determined.ai/schemas/expconf/v0/log-policy.json + case: + pattern: a + action: + type: cancel_retries + +- name: valid log policy (after shim applied to a legacy policy) + sane_as: + - http://determined.ai/schemas/expconf/v0/log-policy.json + case: + pattern: a + action: cancel_retries + +- name: valid log policy (user wants to override default log policy) + sane_as: + - http://determined.ai/schemas/expconf/v0/log-policy.json + case: + name: policy name + pattern: a + +- name: invalid log policy + sanity_errors: + http://determined.ai/schemas/expconf/v0/log-policy.json: + - "\"name\" must be set" + case: {} + +# Log Policies Tests +- name: valid log policies + sane_as: + - http://determined.ai/schemas/expconf/v0/log-policies.json + case: + - name: policy name + pattern: a + action: exclude_node + +- name: policies with different names but same patterns are valid + merge_as: http://determined.ai/schemas/expconf/v0/log-policies.json + case: [] + # Use CheckMerged() to test LogPolicies unmarshal + merge_src: + - name: policy name + pattern: a + action: cancel_retries + - name: different policy name + pattern: a + merged: + - name: policy name + pattern: a + action: cancel_retries + - name: different policy name + pattern: a + +- name: merge policies + merge_as: http://determined.ai/schemas/expconf/v0/log-policies.json + case: + - name: policy 1 + pattern: a + action: exclude_node + - name: policy 2 + pattern: b + action: cancel_retries + - name: policy 3 + pattern: c + action: exclude_node + merge_src: + - name: policy 1 + pattern: aa + action: exclude_node + - name: policy 4 + pattern: b + action: cancel_retries + - name: policy 3 + pattern: c + action: cancel_retries + merged: + - name: policy 1 + pattern: a + action: exclude_node + - name: policy 2 + pattern: b + action: cancel_retries + - name: policy 3 + pattern: c + action: exclude_node + - name: policy 4 + pattern: b + action: cancel_retries + + +- name: merge policies with different names but same patterns, and policies without names + merge_as: http://determined.ai/schemas/expconf/v0/log-policies.json + case: + - name: policy 1 + pattern: a + action: exclude_node + - pattern: b + action: exclude_node + merge_src: + - name: policy 2 + pattern: a + action: cancel_retries + - pattern: b + action: cancel_retries + merged: + - name: policy 1 + pattern: a + action: exclude_node + - pattern: b + action: exclude_node + - name: policy 2 + pattern: a + action: cancel_retries + - pattern: b + action: cancel_retries + +- name: default values + sane_as: + - http://determined.ai/schemas/expconf/v0/log-policies.json + default_as: + http://determined.ai/schemas/expconf/v0/log-policies.json + case: [] + defaulted: + - name: CUDA OOM + pattern: ".*CUDA out of memory.*" + - name: ECC Error + pattern: ".*uncorrectable ECC error encountered.*" + +- name: user can override default values + sane_as: + - http://determined.ai/schemas/expconf/v0/log-policies.json + default_as: + http://determined.ai/schemas/expconf/v0/log-policies.json + case: + - name: CUDA OOM + - name: ECC Error + + defaulted: + - name: CUDA OOM + - name: ECC Error + +- name: legacy log policies merging + merge_as: http://determined.ai/schemas/expconf/v0/log-policies.json + case: + - pattern: b + action: + type: cancel_retries + - pattern: b + action: + type: exclude_node + - pattern: c + action: + type: exclude_node + merge_src: + - pattern: a + action: + type: cancel_retries + - pattern: b + action: + type: exclude_node + merged: + - pattern: b + action: cancel_retries + - pattern: b + action: exclude_node + - pattern: c + action: exclude_node + - pattern: a + action: cancel_retries + diff --git a/schemas/test_cases/v0/unions.yaml b/schemas/test_cases/v0/unions.yaml index 0719defdf63..20218b3a02f 100644 --- a/schemas/test_cases/v0/unions.yaml +++ b/schemas/test_cases/v0/unions.yaml @@ -61,20 +61,6 @@ type: directory container_path: /path/on/disk -- name: log action cancel_retries - sane_as: - - http://determined.ai/schemas/expconf/v0/log-action-cancel-retries.json - - http://determined.ai/schemas/expconf/v0/log-action.json - case: - type: cancel_retries - -- name: log action exclude_node - sane_as: - - http://determined.ai/schemas/expconf/v0/log-action-exclude-node.json - - http://determined.ai/schemas/expconf/v0/log-action.json - case: - type: exclude_node - - name: records length (valid) sane_as: - http://determined.ai/schemas/expconf/v0/length.json diff --git a/webui/react/src/components/ComparisonView.test.mock.tsx b/webui/react/src/components/ComparisonView.test.mock.tsx index 908b9454530..9d5f5a54fd8 100644 --- a/webui/react/src/components/ComparisonView.test.mock.tsx +++ b/webui/react/src/components/ComparisonView.test.mock.tsx @@ -1,11 +1,14 @@ +import { useObservable } from 'micro-observables'; import React from 'react'; import { useGlasbey } from 'hooks/useGlasbey'; import { RunMetricData } from 'hooks/useMetrics'; +import { V1LocationType } from 'services/api-ts-sdk'; import { ExperimentWithTrial, Scale } from 'types'; import { generateTestRunData } from 'utils/tests/generateTestData'; import ComparisonView from './ComparisonView'; +import { FilterFormStore } from './FilterForm/components/FilterFormStore'; export const METRIC_DATA: RunMetricData = { data: { @@ -245,6 +248,7 @@ export const ExperimentComparisonViewWithMocks: React.FC = ({ onWidthChange, open, }: Props): JSX.Element => { + const tableFilters = useObservable(new FilterFormStore(V1LocationType.EXPERIMENT).asJsonString); const colorMap = useGlasbey(SELECTED_EXPERIMENTS.map((exp) => exp.experiment.id)); return ( = ({ initialWidth={200} open={open} projectId={1} + tableFilters={tableFilters} onWidthChange={onWidthChange}> {children} @@ -270,6 +275,7 @@ export const RunComparisonViewWithMocks: React.FC = ({ onWidthChange, open, }: Props): JSX.Element => { + const tableFilters = useObservable(new FilterFormStore(V1LocationType.RUN).asJsonString); const colorMap = useGlasbey(SELECTED_RUNS.map((run) => run.id)); return ( = ({ ? { selections: [], type: 'ONLY_IN' } : { selections: SELECTED_RUNS.map((run) => run.id), type: 'ONLY_IN' } } + tableFilters={tableFilters} onWidthChange={onWidthChange}> {children} diff --git a/webui/react/src/components/ComparisonView.tsx b/webui/react/src/components/ComparisonView.tsx index c5d5d018f06..610227af98c 100644 --- a/webui/react/src/components/ComparisonView.tsx +++ b/webui/react/src/components/ComparisonView.tsx @@ -15,13 +15,16 @@ import useMobile from 'hooks/useMobile'; import useScrollbarWidth from 'hooks/useScrollbarWidth'; import { TrialsComparisonTable } from 'pages/ExperimentDetails/TrialsComparisonModal'; import { searchExperiments, searchRuns } from 'services/api'; +import { V1ColumnType, V1LocationType } from 'services/api-ts-sdk'; import { ExperimentWithTrial, FlatRun, SelectionType, XOR } from 'types'; import handleError from 'utils/error'; import { getIdsFilter as getExperimentIdsFilter } from 'utils/experiment'; +import { combine } from 'utils/filterFormSet'; import { getIdsFilter as getRunIdsFilter } from 'utils/flatRun'; import CompareMetrics from './CompareMetrics'; import { INIT_FORMSET } from './FilterForm/components/FilterFormStore'; +import { FilterFormSet, Operator } from './FilterForm/components/type'; export const EMPTY_MESSAGE = 'No items selected.'; @@ -33,6 +36,8 @@ interface BaseProps { onWidthChange: (width: number) => void; fixedColumnsCount: number; projectId: number; + searchId?: number; + tableFilters: string; } type Props = XOR<{ experimentSelection: SelectionType }, { runSelection: SelectionType }> & @@ -132,6 +137,8 @@ const ComparisonView: React.FC = ({ projectId, experimentSelection, runSelection, + searchId, + tableFilters, }) => { const scrollbarWidth = useScrollbarWidth(); const hasPinnedColumns = fixedColumnsCount > 1; @@ -148,7 +155,10 @@ const ComparisonView: React.FC = ({ return NotLoaded; } try { - const filterFormSet = INIT_FORMSET; + const filterFormSet = + experimentSelection.type === 'ALL_EXCEPT' + ? (JSON.parse(tableFilters) as FilterFormSet) + : INIT_FORMSET; const filter = getExperimentIdsFilter(filterFormSet, experimentSelection); const response = await searchExperiments({ filter: JSON.stringify(filter), @@ -162,7 +172,7 @@ const ComparisonView: React.FC = ({ handleError(e, { publicSubject: 'Unable to fetch experiments for comparison' }); return NotLoaded; } - }, [experimentSelection, open]); + }, [experimentSelection, open, tableFilters]); const loadableSelectedRuns = useAsync(async () => { if ( @@ -172,12 +182,28 @@ const ComparisonView: React.FC = ({ ) { return NotLoaded; } - const filterFormSet = INIT_FORMSET; + const filterFormSet = + runSelection.type === 'ALL_EXCEPT' + ? (JSON.parse(tableFilters) as FilterFormSet) + : INIT_FORMSET; try { const filter = getRunIdsFilter(filterFormSet, runSelection); + if (searchId) { + // only display trials for search + const searchFilter = { + columnName: 'experimentId', + kind: 'field' as const, + location: V1LocationType.RUN, + operator: Operator.Eq, + type: V1ColumnType.NUMBER, + value: searchId, + }; + filter.filterGroup = combine(filter.filterGroup, 'and', searchFilter); + } const response = await searchRuns({ filter: JSON.stringify(filter), limit: SELECTION_LIMIT, + projectId, }); setIsSelectionLimitReached( !!response?.pagination?.total && response?.pagination?.total > SELECTION_LIMIT, @@ -187,7 +213,7 @@ const ComparisonView: React.FC = ({ handleError(e, { publicSubject: 'Unable to fetch runs for comparison' }); return NotLoaded; } - }, [open, runSelection]); + }, [open, projectId, runSelection, searchId, tableFilters]); const minWidths: [number, number] = useMemo(() => { return [fixedColumnsCount * MIN_COLUMN_WIDTH + scrollbarWidth, 100]; diff --git a/webui/react/src/components/ExperimentActionDropdown.tsx b/webui/react/src/components/ExperimentActionDropdown.tsx index 2536e23bcc8..2af2ca397ac 100644 --- a/webui/react/src/components/ExperimentActionDropdown.tsx +++ b/webui/react/src/components/ExperimentActionDropdown.tsx @@ -353,6 +353,7 @@ const ExperimentActionDropdown: React.FC = ({ /> { it('submits a valid create experiment request', async () => { await setup(); - await user.click(screen.getByRole('button', { name: CreateExperimentType.Fork })); + await user.click( + screen.getByRole('button', { name: RunActionCopyMap[CreateExperimentType.Fork] }), + ); expect(mockCreateExperiment).toHaveBeenCalled(); }); }); diff --git a/webui/react/src/components/ExperimentCreateModal.tsx b/webui/react/src/components/ExperimentCreateModal.tsx index 72cc8649d38..59ada006054 100644 --- a/webui/react/src/components/ExperimentCreateModal.tsx +++ b/webui/react/src/components/ExperimentCreateModal.tsx @@ -51,7 +51,7 @@ const ExperimentEntityCopyMap = { trial: 'trial', }; -const RunActionCopyMap = { +export const RunActionCopyMap = { [CreateExperimentType.ContinueTrial]: 'Continue Run', [CreateExperimentType.Fork]: 'Fork', }; @@ -361,7 +361,7 @@ const ExperimentCreateModalComponent = ({ form: idPrefix + FORM_ID, handleError, handler: handleSubmit, - text: type, + text: ExperimentActionCopyMap[type], }} title={titleLabel} onClose={handleModalClose}> diff --git a/webui/react/src/components/ExperimentMoveModal.tsx b/webui/react/src/components/ExperimentMoveModal.tsx index 07e380ea5c8..7fbe5868c92 100644 --- a/webui/react/src/components/ExperimentMoveModal.tsx +++ b/webui/react/src/components/ExperimentMoveModal.tsx @@ -14,14 +14,18 @@ import Link from 'components/Link'; import useFeature from 'hooks/useFeature'; import usePermissions from 'hooks/usePermissions'; import { paths } from 'routes/utils'; -import { moveExperiments } from 'services/api'; -import { V1BulkExperimentFilters } from 'services/api-ts-sdk'; +import { moveSearches } from 'services/api'; +import { V1MoveSearchesRequest } from 'services/api-ts-sdk'; import projectStore from 'stores/projects'; import workspaceStore from 'stores/workspaces'; -import { Project } from 'types'; +import { Project, SelectionType, XOR } from 'types'; import handleError from 'utils/error'; +import { getIdsFilter as getExperimentIdsFilter } from 'utils/experiment'; import { capitalize, pluralizer } from 'utils/string'; +import { INIT_FORMSET } from './FilterForm/components/FilterFormStore'; +import { FilterFormSet } from './FilterForm/components/type'; + const FORM_ID = 'move-experiment-form'; type FormInputs = { @@ -29,19 +33,21 @@ type FormInputs = { workspaceId?: number; }; -interface Props { - excludedExperimentIds?: Map; - experimentIds: number[]; - filters?: V1BulkExperimentFilters; +interface BaseProps { onSubmit?: (successfulIds?: number[]) => void; + selectionSize: number; sourceProjectId: number; sourceWorkspaceId?: number; } +type Props = BaseProps & + XOR<{ experimentIds: number[] }, { selection: SelectionType; tableFilters: string }>; + const ExperimentMoveModalComponent: React.FC = ({ - excludedExperimentIds, experimentIds, - filters, + selection, + selectionSize, + tableFilters, onSubmit, sourceProjectId, sourceWorkspaceId, @@ -54,8 +60,6 @@ const ExperimentMoveModalComponent: React.FC = ({ const projectId = Form.useWatch('projectId', form); const f_flat_runs = useFeature().isOn('flat_runs'); - const entityName = f_flat_runs ? 'searches' : 'experiments'; - useEffect(() => { setDisabled(workspaceId !== 1 && !projectId); }, [workspaceId, projectId, sourceProjectId, sourceWorkspaceId]); @@ -76,6 +80,14 @@ const ExperimentMoveModalComponent: React.FC = ({ } }, [workspaceId]); + // use plurals for indeterminate case + const pluralizerArgs = f_flat_runs + ? (['search', 'searches'] as const) + : (['experiment'] as const); + // we use apply instead of a direct call here because typescript errors when you spread a tuple into arguments + const plural = pluralizer.apply(null, [selectionSize, ...pluralizerArgs]); + const actionCopy = `Move ${capitalize(plural)}`; + const handleSubmit = async () => { if (workspaceId === sourceWorkspaceId && projectId === sourceProjectId) { openToast({ title: 'No changes to save.' }); @@ -84,16 +96,23 @@ const ExperimentMoveModalComponent: React.FC = ({ const values = await form.validateFields(); const projId = values.projectId ?? 1; - if (excludedExperimentIds?.size) { - filters = { ...filters, excludedExperimentIds: Array.from(excludedExperimentIds.keys()) }; + const moveSearchesArgs: V1MoveSearchesRequest = { + destinationProjectId: projId, + sourceProjectId, + }; + + if (tableFilters !== undefined) { + const filterFormSet = + selection.type === 'ALL_EXCEPT' + ? (JSON.parse(tableFilters) as FilterFormSet) + : INIT_FORMSET; + const filter = getExperimentIdsFilter(filterFormSet, selection); + moveSearchesArgs.filter = JSON.stringify(filter); + } else { + moveSearchesArgs.searchIds = experimentIds; } - const results = await moveExperiments({ - destinationProjectId: projId, - experimentIds, - filters, - projectId: sourceProjectId, - }); + const results = await moveSearches(moveSearchesArgs); onSubmit?.(results.successful); @@ -106,19 +125,19 @@ const ExperimentMoveModalComponent: React.FC = ({ if (numSuccesses === 0 && numFailures === 0) { openToast({ - description: `No selected ${entityName} were eligible for moving`, - title: `No eligible ${entityName}`, + description: `No selected ${plural} were eligible for moving`, + title: `No eligible ${plural}`, }); } else if (numFailures === 0) { openToast({ closeable: true, - description: `${results.successful.length} ${entityName} moved to project ${destinationProjectName}`, + description: `${results.successful.length} ${pluralizer.apply(null, [results.successful.length, ...pluralizerArgs])} moved to project ${destinationProjectName}`, link: View Project, title: 'Move Success', }); } else if (numSuccesses === 0) { openToast({ - description: `Unable to move ${numFailures} ${entityName}`, + description: `Unable to move ${numFailures} ${pluralizer.apply(null, [numFailures, ...pluralizerArgs])}`, severity: 'Warning', title: 'Move Failure', }); @@ -127,7 +146,7 @@ const ExperimentMoveModalComponent: React.FC = ({ closeable: true, description: `${numFailures} out of ${ numFailures + numSuccesses - } eligible ${entityName} failed to move + } eligible ${plural} failed to move to project ${destinationProjectName}`, link: View Project, severity: 'Warning', @@ -142,15 +161,6 @@ const ExperimentMoveModalComponent: React.FC = ({ form.setFieldValue('workspaceId', sourceWorkspaceId ?? 1); }, [form, sourceProjectId, sourceWorkspaceId]); - // use plurals for indeterminate case - const entityCount = filters !== undefined ? 2 : experimentIds.length; - const pluralizerArgs = f_flat_runs - ? (['search', 'searches'] as const) - : (['experiment'] as const); - // we use apply instead of a direct call here because typescript errors when you spread a tuple into arguments - const plural = pluralizer.apply(null, [entityCount, ...pluralizerArgs]); - const actionCopy = `Move ${capitalize(plural)}`; - return ( ; labelSingular: string; labelPlural: string; + onActualSelectAll?: () => void; + onClearSelect?: () => void; + pageSize?: number; selectedCount: number; } @@ -17,6 +21,9 @@ const LoadableCount: React.FC = ({ total, labelPlural, labelSingular, + onActualSelectAll, + onClearSelect, + pageSize = 20, selectedCount, }: Props) => { const isMobile = useMobile(); @@ -41,11 +48,37 @@ const LoadableCount: React.FC = ({ }); }, [labelPlural, labelSingular, total, selectedCount]); + const actualSelectAll = useMemo(() => { + return Loadable.match(total, { + _: () => null, + Loaded: (loadedTotal) => { + if (onActualSelectAll && selectedCount >= pageSize && selectedCount < loadedTotal) { + return ( + + ); + } else if (onClearSelect && (selectedCount >= pageSize || selectedCount === loadedTotal)) { + return ( + + ); + } + + return null; + }, + }); + }, [labelPlural, onActualSelectAll, onClearSelect, pageSize, selectedCount, total]); + if (!isMobile) { return ( - - {selectionLabel} - + <> + + {selectionLabel} + + {actualSelectAll} + ); } else { return null; diff --git a/webui/react/src/components/RunActionDropdown.tsx b/webui/react/src/components/RunActionDropdown.tsx index a553e9d0181..e782b73c0a9 100644 --- a/webui/react/src/components/RunActionDropdown.tsx +++ b/webui/react/src/components/RunActionDropdown.tsx @@ -207,7 +207,8 @@ const RunActionDropdown: React.FC = ({ const shared = ( onComplete?.(FlatRunAction.Move, run.id)} diff --git a/webui/react/src/components/RunFilterInterstitialModalComponent.test.tsx b/webui/react/src/components/RunFilterInterstitialModalComponent.test.tsx index 7a05a4fdae1..9cefe4dae02 100644 --- a/webui/react/src/components/RunFilterInterstitialModalComponent.test.tsx +++ b/webui/react/src/components/RunFilterInterstitialModalComponent.test.tsx @@ -111,7 +111,7 @@ describe('RunFilterInterstitialModalComponent', () => { // TODO: is there a better way to test these expectations? expect(filterFormSet.showArchived).toBeTruthy(); - const [, , idFilter] = filterFormSet.filterGroup.children; + const [, idFilter] = filterFormSet.filterGroup.children; for (const child of expectedFilterGroup.children) { expect(filterFormSet.filterGroup.children).toContainEqual(child); } @@ -148,7 +148,7 @@ describe('RunFilterInterstitialModalComponent', () => { const filterFormSet = JSON.parse(filterFormSetString || ''); expect(filterFormSet.showArchived).toBe(false); - const idFilters = filterFormSet.filterGroup.children || []; + const idFilters = filterFormSet.filterGroup.children[0].children || []; expect(idFilters.every((f: FormField) => f.operator === '=')).toBe(true); expect(idFilters.map((f: FormField) => f.value)).toEqual(expectedSelection); }); diff --git a/webui/react/src/components/RunFilterInterstitialModalComponent.tsx b/webui/react/src/components/RunFilterInterstitialModalComponent.tsx index 287b3f4a320..94850975e40 100644 --- a/webui/react/src/components/RunFilterInterstitialModalComponent.tsx +++ b/webui/react/src/components/RunFilterInterstitialModalComponent.tsx @@ -1,5 +1,5 @@ import { useModal } from 'hew/Modal'; -import { Failed, NotLoaded } from 'hew/utils/loadable'; +import { Failed, Loadable, NotLoaded } from 'hew/utils/loadable'; import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'; import { FilterFormSetWithoutId } from 'components/FilterForm/components/type'; @@ -74,11 +74,13 @@ export const RunFilterInterstitialModalComponent = forwardRef ({ close, open })); - const selectionHasSearchRuns = useAsync( + const selectionHasSearchRuns: Loadable = useAsync( async (canceler) => { if (!isOpen) return NotLoaded; const mergedCanceler = mergeAbortControllers(canceler, closeController.current); - const filterWithSingleFilter = combine(filterFormSet.filterGroup, 'and', { + + const filter: FilterFormSetWithoutId = getIdsFilter(filterFormSet, selection); + filter.filterGroup = combine(filter.filterGroup, 'and', { columnName: 'searcherType', kind: 'field', location: 'LOCATION_TYPE_RUN', @@ -86,13 +88,6 @@ export const RunFilterInterstitialModalComponent = forwardRef { +const SearchTensorBoardModal = ({ workspaceId, selectedSearches }: Props): JSX.Element => { const handleSubmit = async () => { - const managedExperimentIds = selectedExperiments - .filter((exp) => !exp.unmanaged) - .map((exp) => exp.id); + const managedSearchIds = selectedSearches.filter((exp) => !exp.unmanaged).map((exp) => exp.id); openCommandResponse( - await openOrCreateTensorBoard({ experimentIds: managedExperimentIds, filters, workspaceId }), + await openOrCreateTensorBoardSearches({ + searchIds: managedSearchIds, + workspaceId, + }), ); }; @@ -42,4 +37,4 @@ const ExperimentTensorBoardModal = ({ ); }; -export default ExperimentTensorBoardModal; +export default SearchTensorBoardModal; diff --git a/webui/react/src/components/Searches/Searches.tsx b/webui/react/src/components/Searches/Searches.tsx index 254927b80b3..cf23e84a574 100644 --- a/webui/react/src/components/Searches/Searches.tsx +++ b/webui/react/src/components/Searches/Searches.tsx @@ -1,4 +1,3 @@ -import { CompactSelection, GridSelection } from '@glideapps/glide-data-grid'; import { isLeft } from 'fp-ts/lib/Either'; import Column from 'hew/Column'; import { @@ -11,15 +10,7 @@ import { MIN_COLUMN_WIDTH, MULTISELECT, } from 'hew/DataGrid/columns'; -import DataGrid, { - DataGridHandle, - HandleSelectionChangeType, - RangelessSelectionType, - SelectionType, - Sort, - validSort, - ValidSort, -} from 'hew/DataGrid/DataGrid'; +import DataGrid, { DataGridHandle, Sort, validSort, ValidSort } from 'hew/DataGrid/DataGrid'; import { MenuItem } from 'hew/Dropdown'; import Icon from 'hew/Icon'; import Link from 'hew/Link'; @@ -56,6 +47,7 @@ import { useDebouncedSettings } from 'hooks/useDebouncedSettings'; import { useGlasbey } from 'hooks/useGlasbey'; import useMobile from 'hooks/useMobile'; import usePolling from 'hooks/usePolling'; +import useSelection from 'hooks/useSelection'; import { useSettings } from 'hooks/useSettings'; import { useTypedParams } from 'hooks/useTypedParams'; import { paths } from 'routes/utils'; @@ -75,7 +67,6 @@ import { Project, ProjectColumn, RunState, - SelectionType as SelectionState, } from 'types'; import handleError from 'utils/error'; import { getProjectExperimentForExperimentItem } from 'utils/experiment'; @@ -97,8 +88,6 @@ interface Props { project: Project; } -type ExperimentWithIndex = { index: number; experiment: BulkExperimentItem }; - const BANNED_FILTER_COLUMNS = new Set(['searcherMetricsVal']); const BANNED_SORT_COLUMNS = new Set(['tags', 'searcherMetricsVal']); @@ -183,6 +172,15 @@ const Searches: React.FC = ({ project }) => { const isMobile = useMobile(); const { openToast } = useToast(); + const { selectionSize, dataGridSelection, handleSelectionChange, isRangeSelected } = useSelection( + { + records: experiments.map((loadable) => loadable.map((exp) => exp.experiment)), + selection: settings.selection, + total, + updateSettings, + }, + ); + const handlePinnedColumnsCountChange = useCallback( (newCount: number) => updateSettings({ pinnedColumnsCount: newCount }), [updateSettings], @@ -248,34 +246,22 @@ const Searches: React.FC = ({ project }) => { return []; }, [settings.selection]); - const loadedSelectedExperimentIds = useMemo(() => { - const selectedMap = new Map(); + const loadedExperimentIdMap = useMemo(() => { + const experimentMap = new Map(); + if (isLoadingSettings) { - return selectedMap; + return experimentMap; } - const selectedIdSet = new Set(allSelectedExperimentIds); + experiments.forEach((e, index) => { - Loadable.forEach(e, ({ experiment }) => { - if (selectedIdSet.has(experiment.id)) { - selectedMap.set(experiment.id, { experiment, index }); - } + Loadable.forEach(e, (experiment) => { + experimentMap.set(experiment.experiment.id, { experiment, index }); }); }); - return selectedMap; - }, [isLoadingSettings, allSelectedExperimentIds, experiments]); + return experimentMap; + }, [experiments, isLoadingSettings]); - const selection = useMemo(() => { - let rows = CompactSelection.empty(); - loadedSelectedExperimentIds.forEach((info) => { - rows = rows.add(info.index); - }); - return { - columns: CompactSelection.empty(), - rows, - }; - }, [loadedSelectedExperimentIds]); - - const colorMap = useGlasbey([...loadedSelectedExperimentIds.keys()]); + const colorMap = useGlasbey([...loadedExperimentIdMap.keys()]); const experimentFilters = useMemo(() => { const filters: V1BulkExperimentFilters = { @@ -399,71 +385,6 @@ const Searches: React.FC = ({ project }) => { }; }, [canceler, stopPolling]); - const rowRangeToIds = useCallback( - (range: [number, number]) => { - const slice = experiments.slice(range[0], range[1]); - return Loadable.filterNotLoaded(slice).map(({ experiment }) => experiment.id); - }, - [experiments], - ); - - const handleSelectionChange: HandleSelectionChangeType = useCallback( - (selectionType: SelectionType | RangelessSelectionType, range?: [number, number]) => { - let newSettings: SelectionState = { ...settings.selection }; - - switch (selectionType) { - case 'add': - if (!range) return; - if (newSettings.type === 'ALL_EXCEPT') { - const excludedSet = new Set(newSettings.exclusions); - rowRangeToIds(range).forEach((id) => excludedSet.delete(id)); - newSettings.exclusions = Array.from(excludedSet); - } else { - const includedSet = new Set(newSettings.selections); - rowRangeToIds(range).forEach((id) => includedSet.add(id)); - newSettings.selections = Array.from(includedSet); - } - - break; - case 'add-all': - newSettings = { - exclusions: [], - type: 'ALL_EXCEPT' as const, - }; - - break; - case 'remove': - if (!range) return; - if (newSettings.type === 'ALL_EXCEPT') { - const excludedSet = new Set(newSettings.exclusions); - rowRangeToIds(range).forEach((id) => excludedSet.add(id)); - newSettings.exclusions = Array.from(excludedSet); - } else { - const includedSet = new Set(newSettings.selections); - rowRangeToIds(range).forEach((id) => includedSet.delete(id)); - newSettings.selections = Array.from(includedSet); - } - - break; - case 'remove-all': - newSettings = DEFAULT_SELECTION; - - break; - case 'set': - if (!range) return; - newSettings = { - ...DEFAULT_SELECTION, - selections: Array.from(rowRangeToIds(range)), - }; - - break; - } - - updateSettings({ selection: newSettings }); - }, - [rowRangeToIds, settings.selection, updateSettings], - ); - const handleActionComplete = useCallback(async () => { /** * Deselect selected rows since their states may have changed where they @@ -639,7 +560,7 @@ const Searches: React.FC = ({ project }) => { const gridColumns = [...STATIC_COLUMNS, ...columnsIfLoaded] .map((columnName) => { if (columnName === MULTISELECT) { - return (columnDefs[columnName] = defaultSelectionColumn(selection.rows, false)); + return (columnDefs[columnName] = defaultSelectionColumn(dataGridSelection.rows, false)); } if (!Loadable.isLoaded(projectColumnsMap)) { @@ -712,39 +633,34 @@ const Searches: React.FC = ({ project }) => { columnsIfLoaded, appTheme, isDarkMode, - selection.rows, + dataGridSelection.rows, users, ]); + const handleActualSelectAll = useCallback(() => { + handleSelectionChange?.('add-all'); + }, [handleSelectionChange]); + + const handleClearSelect = useCallback(() => { + handleSelectionChange?.('remove-all'); + }, [handleSelectionChange]); + + const handleHeaderClick = useCallback( + (columnId: string): void => { + if (columnId === MULTISELECT) { + if (isRangeSelected([0, settings.pageLimit])) { + handleSelectionChange?.('remove', [0, settings.pageLimit]); + } else { + handleSelectionChange?.('add', [0, settings.pageLimit]); + } + } + }, + [handleSelectionChange, isRangeSelected, settings.pageLimit], + ); + const getHeaderMenuItems = (columnId: string, colIdx: number): MenuItem[] => { if (columnId === MULTISELECT) { - const items: MenuItem[] = [ - settings.selection.type === 'ALL_EXCEPT' || settings.selection.selections.length > 0 - ? { - key: 'select-none', - label: 'Clear selected', - onClick: () => { - handleSelectionChange?.('remove-all'); - }, - } - : null, - ...[5, 10, 25].map((n) => ({ - key: `select-${n}`, - label: `Select first ${n}`, - onClick: () => { - handleSelectionChange?.('set', [0, n]); - dataGridRef.current?.scrollToTop(); - }, - })), - { - key: 'select-all', - label: 'Select all', - onClick: () => { - handleSelectionChange?.('add', [0, settings.pageLimit]); - }, - }, - ]; - return items; + return []; } const column = Loadable.getOrElse([], projectColumns).find((c) => c.column === columnId); if (!column) { @@ -875,14 +791,20 @@ const Searches: React.FC = ({ project }) => { isOpenFilter={isOpenFilter} labelPlural="searches" labelSingular="search" + pageSize={settings.pageLimit} project={project} projectColumns={projectColumns} rowHeight={globalSettings.rowHeight} selectedExperimentIds={allSelectedExperimentIds} + selection={settings.selection} + selectionSize={selectionSize} sorts={sorts} + tableFilterString={filtersString} total={total} onActionComplete={handleActionComplete} onActionSuccess={handleActionSuccess} + onActualSelectAll={handleActualSelectAll} + onClearSelect={handleClearSelect} onIsOpenFilterChange={handleIsOpenFilterChange} onRowHeightChange={handleRowHeightChange} onSortChange={handleSortChange} @@ -938,12 +860,13 @@ const Searches: React.FC = ({ project }) => { ); }} rowHeight={rowHeightMap[globalSettings.rowHeight as RowHeight]} - selection={selection} + selection={dataGridSelection} sorts={sorts} staticColumns={STATIC_COLUMNS} onColumnResize={handleColumnWidthChange} onColumnsOrderChange={handleColumnsOrderChange} onContextMenuComplete={handleContextMenuComplete} + onHeaderClicked={handleHeaderClick} onPinnedColumnsCountChange={handlePinnedColumnsCountChange} onSelectionChange={handleSelectionChange} /> diff --git a/webui/react/src/components/TableActionBar.tsx b/webui/react/src/components/TableActionBar.tsx index 1196ff9ca5c..dff835730fd 100644 --- a/webui/react/src/components/TableActionBar.tsx +++ b/webui/react/src/components/TableActionBar.tsx @@ -15,27 +15,28 @@ import BatchActionConfirmModalComponent from 'components/BatchActionConfirmModal import ColumnPickerMenu from 'components/ColumnPickerMenu'; import ExperimentMoveModalComponent from 'components/ExperimentMoveModal'; import ExperimentRetainLogsModalComponent from 'components/ExperimentRetainLogsModal'; -import ExperimentTensorBoardModal from 'components/ExperimentTensorBoardModal'; import { FilterFormStore } from 'components/FilterForm/components/FilterFormStore'; import TableFilter from 'components/FilterForm/TableFilter'; import MultiSortMenu from 'components/MultiSortMenu'; import { OptionsMenu, RowHeight } from 'components/OptionsMenu'; import { defaultProjectSettings } from 'components/Searches/Searches.settings'; +import SearchTensorBoardModal from 'components/SearchTensorBoardModal'; import useMobile from 'hooks/useMobile'; import usePermissions from 'hooks/usePermissions'; import { defaultExperimentColumns } from 'pages/F_ExpList/expListColumns'; import { - activateExperiments, - archiveExperiments, - cancelExperiments, - deleteExperiments, + archiveSearches, + cancelSearches, + deleteSearches, getExperiments, - killExperiments, - openOrCreateTensorBoard, - pauseExperiments, - unarchiveExperiments, + killSearches, + openOrCreateTensorBoardSearches, + pauseSearches, + resumeSearches, + unarchiveSearches, } from 'services/api'; import { V1LocationType } from 'services/api-ts-sdk'; +import { SearchBulkActionParams } from 'services/types'; import { BulkActionResult, BulkExperimentItem, @@ -43,16 +44,19 @@ import { Project, ProjectColumn, ProjectExperiment, + SelectionType, } from 'types'; import handleError, { ErrorLevel } from 'utils/error'; import { canActionExperiment, getActionsForExperimentsUnion, + getIdsFilter, getProjectExperimentForExperimentItem, } from 'utils/experiment'; import { capitalizeWord } from 'utils/string'; import { openCommandResponse } from 'utils/wait'; +import { FilterFormSet } from './FilterForm/components/type'; import LoadableCount from './LoadableCount'; import css from './TableActionBar.module.scss'; @@ -93,6 +97,8 @@ interface Props { isOpenFilter: boolean; onActionComplete?: () => Promise; onActionSuccess?: (action: BatchAction, successfulIds: number[]) => void; + onActualSelectAll?: () => void; + onClearSelect?: () => void; onComparisonViewToggle?: () => void; onHeatmapToggle?: (heatmapOn: boolean) => void; onIsOpenFilterChange?: (value: boolean) => void; @@ -100,10 +106,13 @@ interface Props { onSortChange?: (sorts: Sort[]) => void; onVisibleColumnChange?: (newColumns: string[], pinnedCount?: number) => void; onHeatmapSelectionRemove?: (id: string) => void; + pageSize?: number; project: Project; projectColumns: Loadable; rowHeight: RowHeight; selectedExperimentIds: number[]; + selection: SelectionType; + selectionSize: number; sorts: Sort[]; pinnedColumnsCount?: number; total: Loadable; @@ -113,17 +122,21 @@ interface Props { bannedFilterColumns?: Set; bannedSortColumns?: Set; entityCopy?: string; + tableFilterString: string; } const TableActionBar: React.FC = ({ compareViewOn, formStore, + tableFilterString, heatmapBtnVisible, heatmapOn, initialVisibleColumns, isOpenFilter, onActionComplete, onActionSuccess, + onActualSelectAll, + onClearSelect, onComparisonViewToggle, onHeatmapToggle, onIsOpenFilterChange, @@ -131,6 +144,7 @@ const TableActionBar: React.FC = ({ onSortChange, onHeatmapSelectionRemove, onVisibleColumnChange, + pageSize, project, projectColumns, rowHeight, @@ -144,14 +158,16 @@ const TableActionBar: React.FC = ({ bannedFilterColumns, bannedSortColumns, entityCopy, + selectionSize, + selection, }) => { const permissions = usePermissions(); const [batchAction, setBatchAction] = useState(); const BatchActionConfirmModal = useModal(BatchActionConfirmModalComponent); const ExperimentMoveModal = useModal(ExperimentMoveModalComponent); const ExperimentRetainLogsModal = useModal(ExperimentRetainLogsModalComponent); - const { Component: ExperimentTensorBoardModalComponent, open: openExperimentTensorBoardModal } = - useModal(ExperimentTensorBoardModal); + const { Component: SearchTensorBoardModalComponent, open: openSearchTensorBoardModal } = + useModal(SearchTensorBoardModal); const isMobile = useMobile(); const { openToast } = useToast(); @@ -201,32 +217,50 @@ const TableActionBar: React.FC = ({ ); const availableBatchActions = useMemo(() => { - const experiments = selectedExperimentIds.map((id) => experimentMap[id]) ?? []; - return getActionsForExperimentsUnion(experiments, [...batchActions], permissions); - // Spreading batchActions is so TypeScript doesn't complain that it's readonly. - }, [selectedExperimentIds, experimentMap, permissions]); + switch (selection.type) { + case 'ONLY_IN': { + const experiments = selection.selections.map((id) => experimentMap[id]) ?? []; + return getActionsForExperimentsUnion(experiments, [...batchActions], permissions); // Spreading batchActions is so TypeScript doesn't complain that it's readonly. + } + case 'ALL_EXCEPT': + return batchActions; + } + }, [selection, permissions, experimentMap]); const sendBatchActions = useCallback( async (action: BatchAction): Promise => { - const validExperimentIds = selectedExperiments - .filter((exp) => !exp.unmanaged && canActionExperiment(action, exp)) - .map((exp) => exp.id); - const params = { - experimentIds: validExperimentIds, - projectId: project.id, - }; + const params: SearchBulkActionParams = { projectId: project.id }; + switch (selection.type) { + case 'ONLY_IN': { + const validSearchIds = selectedExperiments + .filter((exp) => !exp.unmanaged && canActionExperiment(action, exp)) + .map((exp) => exp.id); + params.searchIds = validSearchIds; + break; + } + case 'ALL_EXCEPT': { + const filterFormSet = JSON.parse(tableFilterString) as FilterFormSet; + params.filter = JSON.stringify(getIdsFilter(filterFormSet, selection)); + break; + } + } + switch (action) { case ExperimentAction.OpenTensorBoard: { - if (validExperimentIds.length !== selectedExperiments.length) { - // if unmanaged experiments are selected, open experimentTensorBoardModal - openExperimentTensorBoardModal(); - } else { + if ( + params.searchIds === undefined || + params.searchIds.length === selectedExperiments.length + ) { openCommandResponse( - await openOrCreateTensorBoard({ - experimentIds: params.experimentIds, + await openOrCreateTensorBoardSearches({ + filter: params.filter, + searchIds: params.searchIds, workspaceId: project?.workspaceId, }), ); + } else { + // if unmanaged experiments are selected, open searchTensorBoardModal + openSearchTensorBoardModal(); } return; } @@ -235,27 +269,30 @@ const TableActionBar: React.FC = ({ case ExperimentAction.RetainLogs: return ExperimentRetainLogsModal.open(); case ExperimentAction.Activate: - return await activateExperiments(params); + return await resumeSearches(params); case ExperimentAction.Archive: - return await archiveExperiments(params); + return await archiveSearches(params); case ExperimentAction.Cancel: - return await cancelExperiments(params); + return await cancelSearches(params); case ExperimentAction.Kill: - return await killExperiments(params); + return await killSearches(params); case ExperimentAction.Pause: - return await pauseExperiments(params); + return await pauseSearches(params); case ExperimentAction.Unarchive: - return await unarchiveExperiments(params); + return await unarchiveSearches(params); case ExperimentAction.Delete: - return await deleteExperiments(params); + return await deleteSearches(params); } }, [ + project.id, + project?.workspaceId, + selection, selectedExperiments, + tableFilterString, ExperimentMoveModal, ExperimentRetainLogsModal, - openExperimentTensorBoardModal, - project, + openSearchTensorBoardModal, ], ); @@ -312,8 +349,7 @@ const TableActionBar: React.FC = ({ closeable: true, description: `${action} succeeded for ${numSuccesses} out of ${ numFailures + numSuccesses - } eligible - ${labelPlural.toLowerCase()}`, + } ${labelPlural.toLowerCase()}`, severity: 'Warning', title: `Partial ${action} Failure`, }); @@ -376,8 +412,6 @@ const TableActionBar: React.FC = ({ }, [] as MenuItem[]); }, [availableBatchActions]); - const handleAction = useCallback((key: string) => handleBatchAction(key), [handleBatchAction]); - return (
@@ -413,8 +447,8 @@ const TableActionBar: React.FC = ({ onVisibleColumnChange={onVisibleColumnChange} /> - {selectedExperimentIds.length > 0 && ( - + {selectionSize > 0 && ( + @@ -423,8 +457,11 @@ const TableActionBar: React.FC = ({ @@ -460,13 +497,11 @@ const TableActionBar: React.FC = ({ /> )} - canActionExperiment(ExperimentAction.Move, experimentMap[id]) && - permissions.canMoveExperiment({ experiment: experimentMap[id] }), - )} + selection={selection} + selectionSize={selectionSize} sourceProjectId={project.id} sourceWorkspaceId={project.workspaceId} + tableFilters={tableFilterString} onSubmit={handleSubmitMove} /> = ({ projectId={project.id} onSubmit={handleSubmitRetainLogs} /> -
diff --git a/webui/react/src/e2e/models/common/hew/DataGrid.ts b/webui/react/src/e2e/models/common/hew/DataGrid.ts index 98722cc9be7..8b2484f352f 100644 --- a/webui/react/src/e2e/models/common/hew/DataGrid.ts +++ b/webui/react/src/e2e/models/common/hew/DataGrid.ts @@ -5,7 +5,6 @@ import { } from 'playwright-page-model-base/BaseComponent'; import { expect } from 'e2e/fixtures/global-fixtures'; -import { DropdownMenu } from 'e2e/models/common/hew/Dropdown'; import { printMap } from 'e2e/utils/debug'; class IndexNotFoundError extends Error {} @@ -396,14 +395,6 @@ export class HeadRow>> extends NamedCompone parent: this, selector: 'th', }); - readonly selectDropdown = new HeaderDropdown({ - clickThisComponentToOpen: new BaseComponent({ - parent: this, - selector: `[${DataGrid.columnIndexAttribute}="1"]`, - }), - openMethod: this.clickSelectDropdown.bind(this), - root: this.root, - }); #columnDefs = new Map(); @@ -473,18 +464,8 @@ export class HeadRow>> extends NamedCompone /** * Clicks the head row's select button */ - async clickSelectDropdown(): Promise { + async clickSelectHeader(): Promise { // magic numbers for the select button await this.parentTable.pwLocator.click({ position: { x: 5, y: 5 } }); } } - -/** - * Represents the HeaderDropdown from the DataGrid component - */ -class HeaderDropdown extends DropdownMenu { - readonly select5 = this.menuItem('select-5'); - readonly select10 = this.menuItem('select-10'); - readonly select25 = this.menuItem('select-25'); - readonly selectAll = this.menuItem('select-all'); -} diff --git a/webui/react/src/e2e/models/components/TableActionBar.ts b/webui/react/src/e2e/models/components/TableActionBar.ts index 432d6a3df22..17acec62672 100644 --- a/webui/react/src/e2e/models/components/TableActionBar.ts +++ b/webui/react/src/e2e/models/components/TableActionBar.ts @@ -25,6 +25,7 @@ export class TableActionBar extends NamedComponent { count = new BaseComponent({ parent: this, selector: '[data-test="count"]' }); heatmapToggle = new BaseComponent({ parent: this, selector: '[data-test="heatmapToggle"]' }); compare = new BaseComponent({ parent: this, selector: '[data-test="compare"]' }); + clearSelection = new BaseComponent({ parent: this, selector: '[data-test="clear-selection"]' }); // TODO a bunch of modals } diff --git a/webui/react/src/e2e/models/pages/ProjectDetails.ts b/webui/react/src/e2e/models/pages/ProjectDetails.ts index 9c2aa63530d..8aa2d812552 100644 --- a/webui/react/src/e2e/models/pages/ProjectDetails.ts +++ b/webui/react/src/e2e/models/pages/ProjectDetails.ts @@ -5,7 +5,7 @@ import { F_ExperimentList } from 'e2e/models/components/F_ExperimentList'; import { PageComponent } from 'e2e/models/components/Page'; /** - * Represents the SignIn page from src/pages/ProjectDetails.tsx + * Represents the ProjectDetails page from src/pages/ProjectDetails.tsx */ export class ProjectDetails extends DeterminedPage { readonly title = /Uncategorized Experiments|Project Details/; @@ -35,6 +35,8 @@ export class ProjectDetails extends DeterminedPage { return Number(matches[1]); } + // async getRowsSelected(): Promise<{ selected: number; total?: number }> {} + readonly pageComponent = new PageComponent({ parent: this }); readonly dynamicTabs = new DynamicTabs({ parent: this.pageComponent }); readonly runsTab = this.dynamicTabs.pivot.tab('runs'); diff --git a/webui/react/src/e2e/tests/experimentList.spec.ts b/webui/react/src/e2e/tests/experimentList.spec.ts index e2bcc21a50b..cd404d82e70 100644 --- a/webui/react/src/e2e/tests/experimentList.spec.ts +++ b/webui/react/src/e2e/tests/experimentList.spec.ts @@ -13,7 +13,7 @@ test.describe('Experiment List', () => { const getCount = async () => { const count = await projectDetailsPage.f_experimentList.tableActionBar.count.pwLocator.textContent(); - if (count === null) throw new Error('Count is null'); + if (count === null) return 0; return parseInt(count); }; @@ -56,11 +56,14 @@ test.describe('Experiment List', () => { timeout: 10_000, }); await test.step('Deselect', async () => { - try { - await grid.headRow.selectDropdown.menuItem('select-none').select({ timeout: 1_000 }); - } catch (e) { - // close the dropdown by clicking elsewhere - await projectDetailsPage.f_experimentList.tableActionBar.count.pwLocator.click(); + const count = await getCount(); + if (count !== 0) { + await grid.headRow.clickSelectHeader(); + const isClearSelectionVisible = + await projectDetailsPage.f_experimentList.tableActionBar.clearSelection.pwLocator.isVisible(); + if (isClearSelectionVisible) { + await projectDetailsPage.f_experimentList.tableActionBar.clearSelection.pwLocator.click(); + } } }); await test.step('Reset Columns', async () => { @@ -296,11 +299,6 @@ test.describe('Experiment List', () => { await test.step('Read Cell Value', async () => { await expect.soft((await row.getCellByColumnName('ID')).pwLocator).toHaveText(/\d+/); }); - await test.step('Select 5', async () => { - await ( - await projectDetailsPage.f_experimentList.dataGrid.headRow.selectDropdown.open() - ).select5.pwLocator.click(); - }); await test.step('Experiment Overview Navigation', async () => { await projectDetailsPage.f_experimentList.dataGrid.scrollLeft(); const textContent = await (await row.getCellByColumnName('ID')).pwLocator.textContent(); diff --git a/webui/react/src/hooks/useSelection.ts b/webui/react/src/hooks/useSelection.ts new file mode 100644 index 00000000000..5fb6f7e4c5a --- /dev/null +++ b/webui/react/src/hooks/useSelection.ts @@ -0,0 +1,201 @@ +import { CompactSelection, GridSelection } from '@glideapps/glide-data-grid'; +import { + HandleSelectionChangeType, + RangelessSelectionType, + SelectionType, +} from 'hew/DataGrid/DataGrid'; +import { Loadable } from 'hew/utils/loadable'; +import * as t from 'io-ts'; +import { useCallback, useMemo } from 'react'; + +import { RegularSelectionType, SelectionType as SelectionState } from 'types'; + +export const DEFAULT_SELECTION: t.TypeOf = { + selections: [], + type: 'ONLY_IN', +}; + +interface HasId { + id: number; +} + +interface SelectionConfig { + records: Loadable[]; + selection: SelectionState; + total: Loadable; + updateSettings: (p: Record) => void; +} + +interface UseSelectionReturn { + selectionSize: number; + dataGridSelection: GridSelection; + handleSelectionChange: HandleSelectionChangeType; + rowRangeToIds: (range: [number, number]) => number[]; + loadedSelectedRecords: T[]; + loadedSelectedRecordIds: number[]; + isRangeSelected: (range: [number, number]) => boolean; +} + +const useSelection = (config: SelectionConfig): UseSelectionReturn => { + const loadedRecordIdMap = useMemo(() => { + const recordMap = new Map(); + + config.records.forEach((r, index) => { + Loadable.forEach(r, (record) => { + recordMap.set(record.id, { index, record }); + }); + }); + return recordMap; + }, [config.records]); + + const selectedRecordIdSet = useMemo(() => { + switch (config.selection.type) { + case 'ONLY_IN': + return new Set(config.selection.selections); + case 'ALL_EXCEPT': { + const excludedSet = new Set(config.selection.exclusions); + return new Set( + Loadable.filterNotLoaded(config.records, (record) => !excludedSet.has(record.id)).map( + (record) => record.id, + ), + ); + } + } + }, [config.records, config.selection]); + + const dataGridSelection = useMemo(() => { + let rows = CompactSelection.empty(); + switch (config.selection.type) { + case 'ONLY_IN': + config.selection.selections.forEach((id) => { + const incIndex = loadedRecordIdMap.get(id)?.index; + if (incIndex !== undefined) { + rows = rows.add(incIndex); + } + }); + break; + case 'ALL_EXCEPT': + rows = rows.add([0, config.total.getOrElse(1) - 1]); + config.selection.exclusions.forEach((exc) => { + const excIndex = loadedRecordIdMap.get(exc)?.index; + if (excIndex !== undefined) { + rows = rows.remove(excIndex); + } + }); + break; + } + return { + columns: CompactSelection.empty(), + rows, + }; + }, [loadedRecordIdMap, config.selection, config.total]); + + const loadedSelectedRecords: T[] = useMemo(() => { + return Loadable.filterNotLoaded(config.records, (record) => selectedRecordIdSet.has(record.id)); + }, [config.records, selectedRecordIdSet]); + + const loadedSelectedRecordIds: number[] = useMemo(() => { + return loadedSelectedRecords.map((record) => record.id); + }, [loadedSelectedRecords]); + + const selectionSize = useMemo(() => { + switch (config.selection.type) { + case 'ONLY_IN': + return config.selection.selections.length; + case 'ALL_EXCEPT': + return config.total.getOrElse(0) - config.selection.exclusions.length; + } + }, [config.selection, config.total]); + + const rowRangeToIds = useCallback( + (range: [number, number]) => { + const slice = config.records.slice(range[0], range[1]); + return Loadable.filterNotLoaded(slice).map((run) => run.id); + }, + [config.records], + ); + + const handleSelectionChange: HandleSelectionChangeType = useCallback( + (selectionType: SelectionType | RangelessSelectionType, range?: [number, number]) => { + let newSettings: SelectionState = { ...config.selection }; + + switch (selectionType) { + case 'add': + if (!range) return; + if (newSettings.type === 'ALL_EXCEPT') { + const excludedSet = new Set(newSettings.exclusions); + rowRangeToIds(range).forEach((id) => excludedSet.delete(id)); + newSettings.exclusions = Array.from(excludedSet); + } else { + const includedSet = new Set(newSettings.selections); + rowRangeToIds(range).forEach((id) => includedSet.add(id)); + newSettings.selections = Array.from(includedSet); + } + + break; + case 'add-all': + newSettings = { + exclusions: [], + type: 'ALL_EXCEPT', + }; + + break; + case 'remove': + if (!range) return; + if (newSettings.type === 'ALL_EXCEPT') { + const excludedSet = new Set(newSettings.exclusions); + rowRangeToIds(range).forEach((id) => excludedSet.add(id)); + newSettings.exclusions = Array.from(excludedSet); + } else { + const includedSet = new Set(newSettings.selections); + rowRangeToIds(range).forEach((id) => includedSet.delete(id)); + newSettings.selections = Array.from(includedSet); + } + + break; + case 'remove-all': + newSettings = DEFAULT_SELECTION; + + break; + case 'set': + if (!range) return; + newSettings = { + ...DEFAULT_SELECTION, + selections: Array.from(rowRangeToIds(range)), + }; + + break; + } + config.updateSettings({ selection: newSettings }); + }, + [config, rowRangeToIds], + ); + + const isRangeSelected = useCallback( + (range: [number, number]): boolean => { + switch (config.selection.type) { + case 'ONLY_IN': { + const includedSet = new Set(config.selection.selections); + return rowRangeToIds(range).every((id) => includedSet.has(id)); + } + case 'ALL_EXCEPT': { + const excludedSet = new Set(config.selection.exclusions); + return rowRangeToIds(range).every((id) => !excludedSet.has(id)); + } + } + }, + [rowRangeToIds, config.selection], + ); + + return { + dataGridSelection, + handleSelectionChange, + isRangeSelected, + loadedSelectedRecordIds, + loadedSelectedRecords, + rowRangeToIds, + selectionSize, + }; +}; + +export default useSelection; diff --git a/webui/react/src/pages/ExperimentDetails/ExperimentDetailsHeader.tsx b/webui/react/src/pages/ExperimentDetails/ExperimentDetailsHeader.tsx index b6b52f8b1ed..4e6ff2c2f0b 100644 --- a/webui/react/src/pages/ExperimentDetails/ExperimentDetailsHeader.tsx +++ b/webui/react/src/pages/ExperimentDetails/ExperimentDetailsHeader.tsx @@ -785,6 +785,7 @@ const ExperimentDetailsHeader: React.FC = ({ = ({ project }) => { /> = ({ project }) => { const isMobile = useMobile(); const { openToast } = useToast(); + const { + ui: { theme: appTheme }, + isDarkMode, + } = useUI(); + + const { + selectionSize, + dataGridSelection, + handleSelectionChange, + isRangeSelected, + loadedSelectedRecordIds: loadedSelectedExperimentIds, + } = useSelection({ + records: experiments.map((loadable) => loadable.map((exp) => exp.experiment)), + selection: settings.selection, + total, + updateSettings, + }); + const handlePinnedColumnsCountChange = useCallback( (newCount: number) => updateSettings({ pinnedColumnsCount: newCount }), [updateSettings], @@ -248,38 +255,7 @@ const F_ExperimentList: React.FC = ({ project }) => { const [error] = useState(false); const [canceler] = useState(new AbortController()); - const allSelectedExperimentIds = useMemo(() => { - return settings.selection.type === 'ONLY_IN' ? settings.selection.selections : []; - }, [settings.selection]); - - const loadedSelectedExperimentIds = useMemo(() => { - const selectedMap = new Map(); - if (isLoadingSettings) { - return selectedMap; - } - const selectedIdSet = new Set(allSelectedExperimentIds); - experiments.forEach((e, index) => { - Loadable.forEach(e, ({ experiment }) => { - if (selectedIdSet.has(experiment.id)) { - selectedMap.set(experiment.id, { experiment, index }); - } - }); - }); - return selectedMap; - }, [isLoadingSettings, allSelectedExperimentIds, experiments]); - - const selection = useMemo(() => { - let rows = CompactSelection.empty(); - loadedSelectedExperimentIds.forEach((info) => { - rows = rows.add(info.index); - }); - return { - columns: CompactSelection.empty(), - rows, - }; - }, [loadedSelectedExperimentIds]); - - const colorMap = useGlasbey([...loadedSelectedExperimentIds.keys()]); + const colorMap = useGlasbey(loadedSelectedExperimentIds); const { width: containerWidth } = useResize(contentRef); const experimentFilters = useMemo(() => { @@ -437,71 +413,6 @@ const F_ExperimentList: React.FC = ({ project }) => { }; }, [canceler, stopPolling]); - const rowRangeToIds = useCallback( - (range: [number, number]) => { - const slice = experiments.slice(range[0], range[1]); - return Loadable.filterNotLoaded(slice).map(({ experiment }) => experiment.id); - }, - [experiments], - ); - - const handleSelectionChange: HandleSelectionChangeType = useCallback( - (selectionType: SelectionType | RangelessSelectionType, range?: [number, number]) => { - let newSettings: SelectionState = { ...settings.selection }; - - switch (selectionType) { - case 'add': - if (!range) return; - if (newSettings.type === 'ALL_EXCEPT') { - const excludedSet = new Set(newSettings.exclusions); - rowRangeToIds(range).forEach((id) => excludedSet.delete(id)); - newSettings.exclusions = Array.from(excludedSet); - } else { - const includedSet = new Set(newSettings.selections); - rowRangeToIds(range).forEach((id) => includedSet.add(id)); - newSettings.selections = Array.from(includedSet); - } - - break; - case 'add-all': - newSettings = { - exclusions: [], - type: 'ALL_EXCEPT' as const, - }; - - break; - case 'remove': - if (!range) return; - if (newSettings.type === 'ALL_EXCEPT') { - const excludedSet = new Set(newSettings.exclusions); - rowRangeToIds(range).forEach((id) => excludedSet.add(id)); - newSettings.exclusions = Array.from(excludedSet); - } else { - const includedSet = new Set(newSettings.selections); - rowRangeToIds(range).forEach((id) => includedSet.delete(id)); - newSettings.selections = Array.from(includedSet); - } - - break; - case 'remove-all': - newSettings = DEFAULT_SELECTION; - - break; - case 'set': - if (!range) return; - newSettings = { - ...DEFAULT_SELECTION, - selections: Array.from(rowRangeToIds(range)), - }; - - break; - } - - updateSettings({ selection: newSettings }); - }, - [rowRangeToIds, settings.selection, updateSettings], - ); - const handleActionComplete = useCallback(async () => { /** * Deselect selected rows since their states may have changed where they @@ -576,6 +487,14 @@ const F_ExperimentList: React.FC = ({ project }) => { [handleSelectionChange, openToast], ); + const handleActualSelectAll = useCallback(() => { + handleSelectionChange?.('add-all'); + }, [handleSelectionChange]); + + const handleClearSelect = useCallback(() => { + handleSelectionChange?.('remove-all'); + }, [handleSelectionChange]); + const handleContextMenuComplete = useCallback( (action: ExperimentAction, id: number, data?: Partial) => handleActionSuccess(action, [id], data), @@ -734,11 +653,6 @@ const F_ExperimentList: React.FC = ({ project }) => { ); }, [isMobile, settings.compare, settings.pinnedColumnsCount]); - const { - ui: { theme: appTheme }, - isDarkMode, - } = useUI(); - const users = useObservable(usersStore.getUsers()); const columns: ColumnDef[] = useMemo(() => { @@ -761,7 +675,7 @@ const F_ExperimentList: React.FC = ({ project }) => { ) .map((columnName) => { if (columnName === MULTISELECT) { - return (columnDefs[columnName] = defaultSelectionColumn(selection.rows, false)); + return (columnDefs[columnName] = defaultSelectionColumn(dataGridSelection.rows, false)); } if (!Loadable.isLoaded(projectColumnsMap)) { @@ -892,49 +806,36 @@ const F_ExperimentList: React.FC = ({ project }) => { .flatMap((col) => (col ? [col] : [])); return gridColumns; }, [ - settings.compare, - settings.pinnedColumnsCount, projectColumns, + appTheme, settings.columnWidths, - settings.heatmapSkipped, - projectHeatmap, + settings.compare, + settings.pinnedColumnsCount, settings.heatmapOn, - columnsIfLoaded, - appTheme, + settings.heatmapSkipped, isDarkMode, - selection.rows, users, + columnsIfLoaded, + dataGridSelection.rows, + projectHeatmap, ]); + const handleHeaderClick = useCallback( + (columnId: string): void => { + if (columnId === MULTISELECT) { + if (isRangeSelected([0, settings.pageLimit])) { + handleSelectionChange?.('remove', [0, settings.pageLimit]); + } else { + handleSelectionChange?.('add', [0, settings.pageLimit]); + } + } + }, + [handleSelectionChange, isRangeSelected, settings.pageLimit], + ); + const getHeaderMenuItems = (columnId: string, colIdx: number): MenuItem[] => { if (columnId === MULTISELECT) { - const items: MenuItem[] = [ - settings.selection.type === 'ALL_EXCEPT' || settings.selection.selections.length > 0 - ? { - key: 'select-none', - label: 'Clear selected', - onClick: () => { - handleSelectionChange?.('remove-all'); - }, - } - : null, - ...[5, 10, 25].map((n) => ({ - key: `select-${n}`, - label: `Select first ${n}`, - onClick: () => { - handleSelectionChange?.('set', [0, n]); - dataGridRef.current?.scrollToTop(); - }, - })), - { - key: 'select-all', - label: 'Select all', - onClick: () => { - handleSelectionChange?.('add', [0, settings.pageLimit]); - }, - }, - ]; - return items; + return []; } const column = Loadable.getOrElse([], projectColumns).find((c) => c.column === columnId); if (!column) { @@ -1096,11 +997,16 @@ const F_ExperimentList: React.FC = ({ project }) => { project={project} projectColumns={projectColumns} rowHeight={globalSettings.rowHeight} - selectedExperimentIds={allSelectedExperimentIds} + selectedExperimentIds={loadedSelectedExperimentIds} + selection={settings.selection} + selectionSize={selectionSize} sorts={sorts} + tableFilterString={filtersString} total={total} onActionComplete={handleActionComplete} onActionSuccess={handleActionSuccess} + onActualSelectAll={handleActualSelectAll} + onClearSelect={handleClearSelect} onComparisonViewToggle={handleToggleComparisonView} onHeatmapSelectionRemove={(id) => { const newSelection = settings.heatmapSkipped.filter((s) => s !== id); @@ -1130,6 +1036,7 @@ const F_ExperimentList: React.FC = ({ project }) => { initialWidth={comparisonViewTableWidth} open={settings.compare} projectId={project.id} + tableFilters={filtersString} onWidthChange={handleCompareWidthChange}> columns={columns} @@ -1165,12 +1072,13 @@ const F_ExperimentList: React.FC = ({ project }) => { ); }} rowHeight={rowHeightMap[globalSettings.rowHeight]} - selection={selection} + selection={dataGridSelection} sorts={sorts} staticColumns={STATIC_COLUMNS} onColumnResize={handleColumnWidthChange} onColumnsOrderChange={handleColumnsOrderChange} onContextMenuComplete={handleContextMenuComplete} + onHeaderClicked={handleHeaderClick} onPinnedColumnsCountChange={handlePinnedColumnsCountChange} onSelectionChange={handleSelectionChange} /> diff --git a/webui/react/src/pages/FlatRuns/FlatRunActionButton.test.tsx b/webui/react/src/pages/FlatRuns/FlatRunActionButton.test.tsx index f6f9bd4dced..de659e91200 100644 --- a/webui/react/src/pages/FlatRuns/FlatRunActionButton.test.tsx +++ b/webui/react/src/pages/FlatRuns/FlatRunActionButton.test.tsx @@ -25,9 +25,12 @@ const setup = (selectedFlatRuns: ReadonlyArray>) => { render( run.id), type: 'ONLY_IN' }} + selectionSize={selectedFlatRuns.length} workspaceId={1} onActionComplete={onActionComplete} onActionSuccess={onActionSuccess} diff --git a/webui/react/src/pages/FlatRuns/FlatRunActionButton.tsx b/webui/react/src/pages/FlatRuns/FlatRunActionButton.tsx index 9dc726499a8..6b8b8820f6c 100644 --- a/webui/react/src/pages/FlatRuns/FlatRunActionButton.tsx +++ b/webui/react/src/pages/FlatRuns/FlatRunActionButton.tsx @@ -9,6 +9,7 @@ import { useObservable } from 'micro-observables'; import { useCallback, useMemo, useState } from 'react'; import BatchActionConfirmModalComponent from 'components/BatchActionConfirmModal'; +import { FilterFormSetWithoutId } from 'components/FilterForm/components/type'; import Link from 'components/Link'; import usePermissions from 'hooks/usePermissions'; import FlatRunMoveModalComponent from 'pages/FlatRuns/FlatRunMoveModal'; @@ -21,10 +22,11 @@ import { resumeRuns, unarchiveRuns, } from 'services/api'; +import { RunBulkActionParams } from 'services/types'; import projectStore from 'stores/projects'; -import { BulkActionResult, ExperimentAction, FlatRun, Project } from 'types'; +import { BulkActionResult, ExperimentAction, FlatRun, Project, SelectionType } from 'types'; import handleError from 'utils/error'; -import { canActionFlatRun, getActionsForFlatRunsUnion } from 'utils/flatRun'; +import { canActionFlatRun, getActionsForFlatRunsUnion, getIdsFilter } from 'utils/flatRun'; import { capitalizeWord, pluralizer } from 'utils/string'; const BATCH_ACTIONS = [ @@ -52,18 +54,24 @@ const ACTION_ICONS: Record = { const LABEL_PLURAL = 'runs'; interface Props { + filter: string; isMobile: boolean; selectedRuns: ReadonlyArray>; projectId: number; workspaceId: number; onActionSuccess?: (action: BatchAction, successfulIds: number[]) => void; onActionComplete?: () => void | Promise; + selection: SelectionType; + selectionSize: number; } const FlatRunActionButton = ({ + filter, isMobile, selectedRuns, projectId, + selection, + selectionSize, workspaceId, onActionSuccess, onActionComplete, @@ -80,13 +88,21 @@ const FlatRunActionButton = ({ const sendBatchActions = useCallback( async (action: BatchAction): Promise => { - const validRunIds = selectedRuns - .filter((exp) => canActionFlatRun(action, exp)) - .map((run) => run.id); - const params = { - projectId, - runIds: validRunIds, - }; + const params: RunBulkActionParams = { projectId }; + switch (selection.type) { + case 'ONLY_IN': { + const validRunIds = selectedRuns + .filter((run) => canActionFlatRun(action, run)) + .map((run) => run.id); + params.runIds = validRunIds; + break; + } + case 'ALL_EXCEPT': { + const filterFormSet = JSON.parse(filter) as FilterFormSetWithoutId; + params.filter = JSON.stringify(getIdsFilter(filterFormSet, selection)); + break; + } + } switch (action) { case ExperimentAction.Move: flatRunMoveModalOpen(); @@ -105,7 +121,7 @@ const FlatRunActionButton = ({ return await resumeRuns(params); } }, - [flatRunMoveModalOpen, projectId, selectedRuns], + [flatRunMoveModalOpen, projectId, selectedRuns, selection, filter], ); const submitBatchAction = useCallback( @@ -139,8 +155,7 @@ const FlatRunActionButton = ({ } else { openToast({ closeable: true, - description: `${action} succeeded for ${numSuccesses} out of ${numFailures + numSuccesses} eligible - ${pluralizer(numFailures + numSuccesses, 'run')}`, + description: `${action} succeeded for ${numSuccesses} out of ${numFailures + numSuccesses} ${pluralizer(numFailures + numSuccesses, 'run')}`, severity: 'Warning', title: `Partial ${action} Failure`, }); @@ -175,8 +190,13 @@ const FlatRunActionButton = ({ ); const availableBatchActions = useMemo(() => { - return getActionsForFlatRunsUnion(selectedRuns, [...BATCH_ACTIONS], permissions); - }, [selectedRuns, permissions]); + switch (selection.type) { + case 'ONLY_IN': + return getActionsForFlatRunsUnion(selectedRuns, [...BATCH_ACTIONS], permissions); + case 'ALL_EXCEPT': + return BATCH_ACTIONS; + } + }, [selection.type, selectedRuns, permissions]); const editMenuItems = useMemo(() => { const groupedBatchActions = [BATCH_ACTIONS]; @@ -197,7 +217,7 @@ const FlatRunActionButton = ({ }, []); }, [availableBatchActions]); - const onSubmit = useCallback( + const onSubmitMove = useCallback( async (results: BulkActionResult, destinationProjectId: number) => { const numSuccesses = results?.successful.length ?? 0; const numFailures = results?.failed.length ?? 0; @@ -241,7 +261,7 @@ const FlatRunActionButton = ({ return ( <> - {selectedRuns.length > 0 && ( + {selectionSize > 0 && ( diff --git a/webui/react/src/pages/FlatRuns/FlatRunMoveModal.tsx b/webui/react/src/pages/FlatRuns/FlatRunMoveModal.tsx index 68f3544f87d..c60b305bed3 100644 --- a/webui/react/src/pages/FlatRuns/FlatRunMoveModal.tsx +++ b/webui/react/src/pages/FlatRuns/FlatRunMoveModal.tsx @@ -10,6 +10,8 @@ import { List } from 'immutable'; import { useObservable } from 'micro-observables'; import React, { Ref, useCallback, useEffect, useId, useRef } from 'react'; +import { INIT_FORMSET } from 'components/FilterForm/components/FilterFormStore'; +import { FilterFormSet } from 'components/FilterForm/components/type'; import RunFilterInterstitialModalComponent, { ControlledModalRef, } from 'components/RunFilterInterstitialModalComponent'; @@ -19,10 +21,12 @@ import RunMoveWarningModalComponent, { import usePermissions from 'hooks/usePermissions'; import { formStore } from 'pages/FlatRuns/FlatRuns'; import { moveRuns } from 'services/api'; +import { V1MoveRunsRequest } from 'services/api-ts-sdk'; import projectStore from 'stores/projects'; import workspaceStore from 'stores/workspaces'; -import { BulkActionResult, FlatRun, Project } from 'types'; +import { BulkActionResult, Project, SelectionType, XOR } from 'types'; import handleError from 'utils/error'; +import { getIdsFilter as getRunIdsFilter } from 'utils/flatRun'; import { pluralizer } from 'utils/string'; const FORM_ID = 'move-flat-run-form'; @@ -32,15 +36,21 @@ type FormInputs = { destinationWorkspaceId?: number; }; -interface Props { - flatRuns: Readonly[]; +interface BaseProps { + selectionSize: number; sourceProjectId: number; sourceWorkspaceId?: number; onSubmit?: (results: BulkActionResult, destinationProjectId: number) => void | Promise; } +type Props = BaseProps & + XOR<{ runIds: number[] }, { selection: SelectionType; tableFilters: string }>; + const FlatRunMoveModalComponent: React.FC = ({ - flatRuns, + runIds, + tableFilters, + selection, + selectionSize, sourceProjectId, sourceWorkspaceId, onSubmit, @@ -97,24 +107,38 @@ const FlatRunMoveModalComponent: React.FC = ({ return; } - const results = await moveRuns({ + const moveRunsArgs: V1MoveRunsRequest = { destinationProjectId: projId, - runIds: flatRuns.map((flatRun) => flatRun.id), sourceProjectId, - }); + }; + + if (tableFilters !== undefined) { + const filterFormSet = + selection.type === 'ALL_EXCEPT' + ? (JSON.parse(tableFilters) as FilterFormSet) + : INIT_FORMSET; + const filter = getRunIdsFilter(filterFormSet, selection); + moveRunsArgs.filter = JSON.stringify(filter); + } else { + moveRunsArgs.runIds = runIds; + } + + const results = await moveRuns(moveRunsArgs); await onSubmit?.(results, projId); form.resetFields(); } catch (e) { handleError(e, { publicSubject: 'Unable to move runs' }); } }, [ - flatRuns, form, - onSubmit, - openToast, - sourceProjectId, - sourceWorkspaceId, destinationWorkspaceId, + sourceWorkspaceId, + sourceProjectId, + openToast, + tableFilters, + onSubmit, + selection, + runIds, ]); return ( @@ -127,9 +151,9 @@ const FlatRunMoveModalComponent: React.FC = ({ form: idPrefix + FORM_ID, handleError, handler: handleSubmit, - text: `Move ${pluralizer(flatRuns.length, 'Run')}`, + text: `Move ${pluralizer(selectionSize, 'Run')}`, }} - title={`Move ${pluralizer(flatRuns.length, 'Run')}`}> + title={`Move ${pluralizer(selectionSize, 'Run')}`}>
= ({ flatRun.id), type: 'ONLY_IN' }} + selection={selection ?? { selections: runIds, type: 'ONLY_IN' }} /> ); diff --git a/webui/react/src/pages/FlatRuns/FlatRuns.tsx b/webui/react/src/pages/FlatRuns/FlatRuns.tsx index 5769c760778..33b5830dafc 100644 --- a/webui/react/src/pages/FlatRuns/FlatRuns.tsx +++ b/webui/react/src/pages/FlatRuns/FlatRuns.tsx @@ -1,4 +1,3 @@ -import { CompactSelection, GridSelection } from '@glideapps/glide-data-grid'; import { isLeft } from 'fp-ts/lib/Either'; import Button from 'hew/Button'; import Column from 'hew/Column'; @@ -14,15 +13,7 @@ import { MULTISELECT, } from 'hew/DataGrid/columns'; import { ContextMenuCompleteHandlerProps } from 'hew/DataGrid/contextMenu'; -import DataGrid, { - DataGridHandle, - HandleSelectionChangeType, - RangelessSelectionType, - SelectionType, - Sort, - validSort, - ValidSort, -} from 'hew/DataGrid/DataGrid'; +import DataGrid, { DataGridHandle, Sort, validSort, ValidSort } from 'hew/DataGrid/DataGrid'; import { MenuItem } from 'hew/Dropdown'; import Icon from 'hew/Icon'; import Link from 'hew/Link'; @@ -38,10 +29,15 @@ import { v4 as uuidv4 } from 'uuid'; import ColumnPickerMenu from 'components/ColumnPickerMenu'; import ComparisonView from 'components/ComparisonView'; import { Error } from 'components/exceptions'; -import { FilterFormStore, ROOT_ID } from 'components/FilterForm/components/FilterFormStore'; +import { + FilterFormStore, + INIT_FORMSET, + ROOT_ID, +} from 'components/FilterForm/components/FilterFormStore'; import { AvailableOperators, FilterFormSet, + FilterFormSetWithoutId, FormField, FormGroup, FormKind, @@ -67,6 +63,7 @@ import useMobile from 'hooks/useMobile'; import usePolling from 'hooks/usePolling'; import useResize from 'hooks/useResize'; import useScrollbarWidth from 'hooks/useScrollbarWidth'; +import useSelection from 'hooks/useSelection'; import { useSettings } from 'hooks/useSettings'; import useTypedParams from 'hooks/useTypedParams'; import FlatRunActionButton from 'pages/FlatRuns/FlatRunActionButton'; @@ -75,16 +72,10 @@ import { getProjectColumns, getProjectNumericMetricsRange, searchRuns } from 'se import { V1ColumnType, V1LocationType, V1TableType } from 'services/api-ts-sdk'; import userStore from 'stores/users'; import userSettings from 'stores/userSettings'; -import { - DetailedUser, - FlatRun, - FlatRunAction, - ProjectColumn, - RunState, - SelectionType as SelectionState, -} from 'types'; +import { DetailedUser, FlatRun, FlatRunAction, ProjectColumn, RunState } from 'types'; import handleError from 'utils/error'; import { formatColumnKey } from 'utils/flatRun'; +import { combine } from 'utils/filterFormSet'; import { eagerSubscribe } from 'utils/observable'; import { pluralizer } from 'utils/string'; @@ -193,7 +184,7 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { _: () => [], Loaded: (formset: FilterFormSet) => formset.filterGroup.children, }); - const filtersString = useObservable(formStore.asJsonString); + const filtersString = useObservable(formStore.asJsonString) || JSON.stringify(INIT_FORMSET); const [total, setTotal] = useState>(NotLoaded); const isMobile = useMobile(); const [isLoading, setIsLoading] = useState(true); @@ -204,6 +195,19 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { const { openToast } = useToast(); const { width: containerWidth } = useResize(contentRef); + const { + selectionSize, + dataGridSelection, + handleSelectionChange, + loadedSelectedRecords: loadedSelectedRuns, + isRangeSelected, + } = useSelection({ + records: runs, + selection: settings.selection, + total, + updateSettings, + }); + const { ui: { theme: appTheme }, isDarkMode, @@ -249,10 +253,6 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { return new Set([...BANNED_SORT_COLUMNS, ...arrayTypeColumns]); }, [arrayTypeColumns]); - const selectedRunIdSet = useMemo(() => { - return new Set(settings.selection.type === 'ONLY_IN' ? settings.selection.selections : []); - }, [settings.selection]); - const columnsIfLoaded = useMemo( () => (isLoadingSettings ? [] : settings.columns), [isLoadingSettings, settings.columns], @@ -264,41 +264,18 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { ); }, [isMobile, settings.compare, settings.pinnedColumnsCount]); - const loadedSelectedRunIds = useMemo(() => { - const selectedMap = new Map(); - const selectedArray: FlatRun[] = []; - if (isLoadingSettings) { - return selectedMap; - } + const loadedRunIdMap = useMemo(() => { + const runMap = new Map(); runs.forEach((r, index) => { Loadable.forEach(r, (run) => { - if (selectedRunIdSet.has(run.id)) { - selectedMap.set(run.id, { index, run }); - selectedArray.push(run); - } + runMap.set(run.id, { index, run }); }); }); - return selectedMap; - }, [isLoadingSettings, runs, selectedRunIdSet]); + return runMap; + }, [runs]); - const selection = useMemo(() => { - let rows = CompactSelection.empty(); - loadedSelectedRunIds.forEach((info) => { - rows = rows.add(info.index); - }); - return { - columns: CompactSelection.empty(), - rows, - }; - }, [loadedSelectedRunIds]); - - const selectedRuns: FlatRun[] = useMemo(() => { - const selected = runs.flatMap((run) => { - return run.isLoaded && selectedRunIdSet.has(run.data.id) ? [run.data] : []; - }); - return selected; - }, [runs, selectedRunIdSet]); + const colorMap = useGlasbey([...loadedRunIdMap.keys()]); const handleIsOpenFilterChange = useCallback((newOpen: boolean) => { setIsOpenFilter(newOpen); @@ -307,8 +284,6 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { } }, []); - const colorMap = useGlasbey([...loadedSelectedRunIds.keys()]); - const handleToggleComparisonView = useCallback(() => { updateSettings({ compare: !settings.compare }); }, [settings.compare, updateSettings]); @@ -337,7 +312,7 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { ) .map((columnName) => { if (columnName === MULTISELECT) { - return defaultSelectionColumn(selection.rows, false); + return defaultSelectionColumn(dataGridSelection.rows, false); } if (!Loadable.isLoaded(projectColumnsMap)) { @@ -482,7 +457,7 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { isDarkMode, projectColumns, projectHeatmap, - selection.rows, + dataGridSelection.rows, settings.columnWidths, settings.compare, settings.heatmapOn, @@ -541,31 +516,30 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { setRuns(INITIAL_LOADING_RUNS); }, [setPage]); + const filterFormSetString = useMemo(() => { + const filter = JSON.parse(filtersString) as FilterFormSetWithoutId; + if (searchId) { + // only display trials for search + const searchFilter = { + columnName: 'experimentId', + kind: 'field' as const, + location: V1LocationType.RUN, + operator: Operator.Eq, + type: V1ColumnType.NUMBER, + value: searchId, + }; + filter.filterGroup = combine(filter.filterGroup, 'and', searchFilter); + } + return JSON.stringify(filter); + }, [filtersString, searchId]); + const fetchRuns = useCallback(async (): Promise => { if (isLoadingSettings || Loadable.isNotLoaded(loadableFormset)) return; try { - const filters = JSON.parse(filtersString); - if (searchId) { - // only display trials for search - const existingFilterGroup = { ...filters.filterGroup }; - const searchFilter = { - columnName: 'experimentId', - kind: 'field', - location: 'LOCATION_TYPE_RUN', - operator: '=', - type: 'COLUMN_TYPE_NUMBER', - value: searchId, - }; - filters.filterGroup = { - children: [existingFilterGroup, searchFilter], - conjunction: 'and', - kind: 'group', - }; - } const offset = page * settings.pageLimit; const response = await searchRuns( { - filter: JSON.stringify(filters), + filter: filterFormSetString, limit: settings.pageLimit, offset, projectId: projectId, @@ -589,16 +563,15 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { setIsLoading(false); } }, [ - canceler.signal, - filtersString, isLoadingSettings, loadableFormset, page, - projectId, - resetPagination, settings.pageLimit, + filterFormSetString, + projectId, sortString, - searchId, + canceler.signal, + resetPagination, ]); const { stopPolling } = usePolling(fetchRuns, { rerunOnNewFn: true }); @@ -706,70 +679,6 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { [settings.columnWidths, updateColumnWidths], ); - const rowRangeToIds = useCallback( - (range: [number, number]) => { - const slice = runs.slice(range[0], range[1]); - return Loadable.filterNotLoaded(slice).map((run) => run.id); - }, - [runs], - ); - - const handleSelectionChange: HandleSelectionChangeType = useCallback( - (selectionType: SelectionType | RangelessSelectionType, range?: [number, number]) => { - let newSettings: SelectionState = { ...settings.selection }; - - switch (selectionType) { - case 'add': - if (!range) return; - if (newSettings.type === 'ALL_EXCEPT') { - const excludedSet = new Set(newSettings.exclusions); - rowRangeToIds(range).forEach((id) => excludedSet.delete(id)); - newSettings.exclusions = Array.from(excludedSet); - } else { - const includedSet = new Set(newSettings.selections); - rowRangeToIds(range).forEach((id) => includedSet.add(id)); - newSettings.selections = Array.from(includedSet); - } - - break; - case 'add-all': - newSettings = { - exclusions: [], - type: 'ALL_EXCEPT' as const, - }; - - break; - case 'remove': - if (!range) return; - if (newSettings.type === 'ALL_EXCEPT') { - const excludedSet = new Set(newSettings.exclusions); - rowRangeToIds(range).forEach((id) => excludedSet.add(id)); - newSettings.exclusions = Array.from(excludedSet); - } else { - const includedSet = new Set(newSettings.selections); - rowRangeToIds(range).forEach((id) => includedSet.delete(id)); - newSettings.selections = Array.from(includedSet); - } - - break; - case 'remove-all': - newSettings = DEFAULT_SELECTION; - - break; - case 'set': - if (!range) return; - newSettings = { - ...DEFAULT_SELECTION, - selections: Array.from(rowRangeToIds(range)), - }; - - break; - } - updateSettings({ selection: newSettings }); - }, - [rowRangeToIds, settings.selection, updateSettings], - ); - const onActionComplete = useCallback(async () => { handleSelectionChange('remove-all'); await fetchRuns(); @@ -873,36 +782,31 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { [updateSettings], ); + const handleActualSelectAll = useCallback(() => { + handleSelectionChange?.('add-all'); + }, [handleSelectionChange]); + + const handleClearSelect = useCallback(() => { + handleSelectionChange?.('remove-all'); + }, [handleSelectionChange]); + + const handleHeaderClick = useCallback( + (columnId: string): void => { + if (columnId === MULTISELECT) { + if (isRangeSelected([0, settings.pageLimit])) { + handleSelectionChange?.('remove', [0, settings.pageLimit]); + } else { + handleSelectionChange?.('add', [0, settings.pageLimit]); + } + } + }, + [handleSelectionChange, isRangeSelected, settings.pageLimit], + ); + const getHeaderMenuItems = useCallback( (columnId: string, colIdx: number): MenuItem[] => { if (columnId === MULTISELECT) { - const items: MenuItem[] = [ - settings.selection.type === 'ALL_EXCEPT' || settings.selection.selections.length > 0 - ? { - key: 'select-none', - label: 'Clear selected', - onClick: () => { - handleSelectionChange?.('remove-all'); - }, - } - : null, - ...[5, 10, 25].map((n) => ({ - key: `select-${n}`, - label: `Select first ${n}`, - onClick: () => { - handleSelectionChange?.('set', [0, n]); - dataGridRef.current?.scrollToTop(); - }, - })), - { - key: 'select-all', - label: 'Select all', - onClick: () => { - handleSelectionChange?.('add', [0, settings.pageLimit]); - }, - }, - ]; - return items; + return []; } const column = Loadable.getOrElse([], projectColumns).find((c) => c.column === columnId); @@ -1054,12 +958,9 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { bannedSortColumns, projectColumns, settings.pinnedColumnsCount, - settings.selection, - settings.pageLimit, settings.heatmapOn, settings.heatmapSkipped, isMobile, - handleSelectionChange, columnsIfLoaded, handleColumnsOrderChange, rootFilterChildren, @@ -1121,17 +1022,23 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { onRowHeightChange={onRowHeightChange} /> @@ -1180,6 +1087,8 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { open={settings.compare} projectId={projectId} runSelection={settings.selection} + searchId={searchId} + tableFilters={filtersString} onWidthChange={handleCompareWidthChange}> columns={columns} @@ -1211,12 +1120,13 @@ const FlatRuns: React.FC = ({ projectId, workspaceId, searchId }) => { ); }} rowHeight={rowHeightMap[globalSettings.rowHeight as RowHeight]} - selection={selection} + selection={dataGridSelection} sorts={sorts} staticColumns={STATIC_COLUMNS} onColumnResize={handleColumnWidthChange} onColumnsOrderChange={handleColumnsOrderChange} onContextMenuComplete={handleContextMenuComplete} + onHeaderClicked={handleHeaderClick} onPinnedColumnsCountChange={handlePinnedColumnsCountChange} onSelectionChange={handleSelectionChange} /> diff --git a/webui/react/src/pages/FlatRuns/columns.ts b/webui/react/src/pages/FlatRuns/columns.ts index 7de32358ffd..bdb71a34162 100644 --- a/webui/react/src/pages/FlatRuns/columns.ts +++ b/webui/react/src/pages/FlatRuns/columns.ts @@ -436,7 +436,7 @@ export const getColumnDefs = ({ data: { appTheme, kind: STATE_CELL, - label: record.logSignal, + label: record.logPolicyMatched, state: getCellStateFromExperimentState(record.state), }, kind: GridCellKind.Custom, diff --git a/webui/react/src/pages/TrialDetails/Header/TrialHeaderLeft.tsx b/webui/react/src/pages/TrialDetails/Header/TrialHeaderLeft.tsx index 5871c92a460..4c4b9116f1b 100644 --- a/webui/react/src/pages/TrialDetails/Header/TrialHeaderLeft.tsx +++ b/webui/react/src/pages/TrialDetails/Header/TrialHeaderLeft.tsx @@ -34,14 +34,14 @@ const TrialHeaderLeft: React.FC = ({ experiment, trial }: Props) => {
{f_flat_runs ? 'Run' : 'Trial'} {trial.id}
- {trial.logSignal && - (trial.logSignal.length < labelMaxLength ? ( - + {trial.logPolicyMatched && + (trial.logPolicyMatched.length < labelMaxLength ? ( + ) : ( - + ))} diff --git a/webui/react/src/services/api-ts-sdk/api.ts b/webui/react/src/services/api-ts-sdk/api.ts index 16d99d09b3f..ff48702c2d8 100644 --- a/webui/react/src/services/api-ts-sdk/api.ts +++ b/webui/react/src/services/api-ts-sdk/api.ts @@ -934,11 +934,11 @@ export interface Trialv1Trial { */ metadata?: any; /** - * The log signals. + * Log Policy Matched. * @type {string} * @memberof Trialv1Trial */ - logSignal?: string; + logPolicyMatched?: string; } /** * @@ -3744,11 +3744,11 @@ export interface V1FlatRun { */ localId?: string; /** - * Log signal. + * Log policy matched. * @type {string} * @memberof V1FlatRun */ - logSignal?: string; + logPolicyMatched?: string; } /** * diff --git a/webui/react/src/services/api.ts b/webui/react/src/services/api.ts index 02a4134d631..a4e57fc0aed 100644 --- a/webui/react/src/services/api.ts +++ b/webui/react/src/services/api.ts @@ -7,7 +7,7 @@ import { DeterminedInfo, Telemetry } from 'stores/determinedInfo'; import { EmptyParams, RawJson, SingleEntityParams } from 'types'; import * as Type from 'types'; import { generateDetApi } from 'utils/service'; -import { tensorBoardMatchesSource } from 'utils/task'; +import { tensorBoardMatchesSource, tensorBoardSearchesMatchesSource } from 'utils/task'; /* Authentication */ @@ -493,6 +493,75 @@ export const changeExperimentLogRetention = generateDetApi< Type.BulkActionResult >(Config.changeExperimentLogRetention); +/* Searches */ + +export const archiveSearches = generateDetApi< + Api.V1ArchiveSearchesRequest, + Api.V1ArchiveSearchesResponse, + Type.BulkActionResult +>(Config.archiveSearches); + +export const deleteSearches = generateDetApi< + Api.V1DeleteSearchesRequest, + Api.V1DeleteSearchesResponse, + Type.BulkActionResult +>(Config.deleteSearches); + +export const killSearches = generateDetApi< + Api.V1KillSearchesRequest, + Api.V1KillSearchesResponse, + Type.BulkActionResult +>(Config.killSearches); + +export const moveSearches = generateDetApi< + Api.V1MoveSearchesRequest, + Api.V1MoveSearchesResponse, + Type.BulkActionResult +>(Config.moveSearches); + +export const unarchiveSearches = generateDetApi< + Api.V1UnarchiveSearchesRequest, + Api.V1UnarchiveSearchesResponse, + void +>(Config.unarchiveSearches); + +export const pauseSearches = generateDetApi< + Api.V1ResumeSearchesRequest, + Api.V1ResumeSearchesResponse, + Type.BulkActionResult +>(Config.pauseSearches); + +export const resumeSearches = generateDetApi< + Api.V1ResumeSearchesRequest, + Api.V1ResumeSearchesResponse, + Type.BulkActionResult +>(Config.resumeSearches); + +export const cancelSearches = generateDetApi< + Api.V1ResumeSearchesRequest, + Api.V1ResumeSearchesResponse, + Type.BulkActionResult +>(Config.resumeSearches); + +export const launchTensorBoardSearches = generateDetApi< + Service.LaunchTensorBoardSearchesParams, + Api.V1LaunchTensorboardSearchesResponse, + Type.CommandResponse +>(Config.launchTensorBoardSearches); + +export const openOrCreateTensorBoardSearches = async ( + params: Service.LaunchTensorBoardSearchesParams, +): Promise => { + const tensorboards = await getTensorBoards({}); + const match = tensorboards.find( + (tensorboard) => + !terminalCommandStates.has(tensorboard.state) && + tensorBoardSearchesMatchesSource(tensorboard, params), + ); + if (match) return { command: match, warnings: [V1LaunchWarning.CURRENTSLOTSEXCEEDED] }; + return launchTensorBoardSearches(params); +}; + /* Tasks */ export const getTask = generateDetApi< diff --git a/webui/react/src/services/apiConfig.ts b/webui/react/src/services/apiConfig.ts index b6c2b0ed1b8..2472eb6e06b 100644 --- a/webui/react/src/services/apiConfig.ts +++ b/webui/react/src/services/apiConfig.ts @@ -1153,6 +1153,104 @@ export const getTrialWorkloads: DetApi< ), }; +/* Searches */ + +export const archiveSearches: DetApi< + Api.V1ArchiveSearchesRequest, + Api.V1ArchiveSearchesResponse, + Type.BulkActionResult +> = { + name: 'archiveSearches', + postProcess: (response) => decoder.mapV1ActionResults(response.results), + request: (params, options) => detApi.Internal.archiveSearches(params, options), +}; + +export const deleteSearches: DetApi< + Api.V1DeleteSearchesRequest, + Api.V1DeleteSearchesResponse, + Type.BulkActionResult +> = { + name: 'deleteSearches', + postProcess: (response) => decoder.mapV1ActionResults(response.results), + request: (params, options) => detApi.Internal.deleteSearches(params, options), +}; + +export const killSearches: DetApi< + Api.V1KillSearchesRequest, + Api.V1KillSearchesResponse, + Type.BulkActionResult +> = { + name: 'killSearches', + postProcess: (response) => decoder.mapV1ActionResults(response.results), + request: (params, options) => detApi.Internal.killSearches(params, options), +}; + +export const moveSearches: DetApi< + Api.V1MoveSearchesRequest, + Api.V1MoveSearchesResponse, + Type.BulkActionResult +> = { + name: 'moveSearches', + postProcess: (response) => decoder.mapV1ActionResults(response.results), + request: (params, options) => detApi.Internal.moveSearches(params, options), +}; + +export const unarchiveSearches: DetApi< + Api.V1UnarchiveSearchesRequest, + Api.V1UnarchiveSearchesResponse, + Type.BulkActionResult +> = { + name: 'unarchiveSearches', + postProcess: (response) => decoder.mapV1ActionResults(response.results), + request: (params, options) => detApi.Internal.unarchiveSearches(params, options), +}; + +export const pauseSearches: DetApi< + Api.V1PauseSearchesRequest, + Api.V1PauseSearchesResponse, + Type.BulkActionResult +> = { + name: 'pauseSearches', + postProcess: (response) => decoder.mapV1ActionResults(response.results), + request: (params, options) => detApi.Internal.pauseSearches(params, options), +}; + +export const resumeSearches: DetApi< + Api.V1ResumeSearchesRequest, + Api.V1ResumeSearchesResponse, + Type.BulkActionResult +> = { + name: 'resumeSearches', + postProcess: (response) => decoder.mapV1ActionResults(response.results), + request: (params, options) => detApi.Internal.resumeSearches(params, options), +}; + +export const cancelSearches: DetApi< + Api.V1CancelSearchesRequest, + Api.V1CancelSearchesResponse, + Type.BulkActionResult +> = { + name: 'cancelSearches', + postProcess: (response) => decoder.mapV1ActionResults(response.results), + request: (params, options) => detApi.Internal.cancelSearches(params, options), +}; + +export const launchTensorBoardSearches: DetApi< + Service.LaunchTensorBoardSearchesParams, + Api.V1LaunchTensorboardSearchesResponse, + Type.CommandResponse +> = { + name: 'launchTensorBoard', + postProcess: (response) => { + return { + command: decoder.mapV1TensorBoard(response.tensorboard), + warnings: response.warnings || [], + }; + }, + request: (params: Service.LaunchTensorBoardSearchesParams) => + detApi.Internal.launchTensorboardSearches(params), +}; + /* Runs */ export const searchRuns: DetApi< diff --git a/webui/react/src/services/decoder.ts b/webui/react/src/services/decoder.ts index 7c8ec5554a4..70d7417c5de 100644 --- a/webui/react/src/services/decoder.ts +++ b/webui/react/src/services/decoder.ts @@ -680,8 +680,8 @@ export const decodeV1TrialToTrialItem = (data: Sdk.Trialv1Trial): types.TrialIte hyperparameters: flattenObject(data.hparams || {}), id: data.id, latestValidationMetric: data.latestValidation && decodeMetricsWorkload(data.latestValidation), + logPolicyMatched: data.logPolicyMatched, logRetentionDays: data.logRetentionDays, - logSignal: data.logSignal, metadata: data.metadata, searcherMetricsVal: data.searcherMetricValue, startTime: data.startTime as unknown as string, diff --git a/webui/react/src/services/types.ts b/webui/react/src/services/types.ts index 3b41ec11778..01960ebd252 100644 --- a/webui/react/src/services/types.ts +++ b/webui/react/src/services/types.ts @@ -164,6 +164,18 @@ export interface SearchRunsParams extends PaginationParams { sort?: string; } +export interface RunBulkActionParams { + projectId: number; + filter?: string; + runIds?: number[]; +} + +export interface SearchBulkActionParams { + projectId: number; + filter?: string; + searchIds?: number[]; +} + export interface GetTaskParams { taskId: string; } @@ -288,6 +300,12 @@ export interface LaunchTensorBoardParams { filters?: Api.V1BulkExperimentFilters; } +export interface LaunchTensorBoardSearchesParams { + searchIds?: Array; + workspaceId?: number; + filter?: string; +} + export interface LaunchJupyterLabParams { config?: { description?: string; diff --git a/webui/react/src/types.ts b/webui/react/src/types.ts index bd8a64e84a9..5ef5b089d0b 100644 --- a/webui/react/src/types.ts +++ b/webui/react/src/types.ts @@ -710,7 +710,7 @@ export interface TrialItem extends StartEndTimes { logRetentionDays?: number; taskId?: string; metadata?: JsonObject; - logSignal?: string; + logPolicyMatched?: string; } export interface TrialDetails extends TrialItem { @@ -1298,7 +1298,7 @@ export interface FlatRun { archived: boolean; parentArchived: boolean; experiment?: FlatRunExperiment; - logSignal?: string; + logPolicyMatched?: string; } export interface FlatRunExperiment { diff --git a/webui/react/src/utils/experiment.ts b/webui/react/src/utils/experiment.ts index 862bf0e0df7..032124abfa6 100644 --- a/webui/react/src/utils/experiment.ts +++ b/webui/react/src/utils/experiment.ts @@ -374,7 +374,7 @@ const idToFilter = (operator: Operator, id: number) => export const getIdsFilter = ( filterFormSet: FilterFormSetWithoutId, selection: SelectionType, -): FilterFormSetWithoutId | undefined => { +): FilterFormSetWithoutId => { const filterGroup: FilterFormSetWithoutId['filterGroup'] = selection.type === 'ALL_EXCEPT' ? { diff --git a/webui/react/src/utils/task.ts b/webui/react/src/utils/task.ts index 0c220bf9e56..f29c4ba6d61 100644 --- a/webui/react/src/utils/task.ts +++ b/webui/react/src/utils/task.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import { killableCommandStates, killableRunStates, terminalCommandStates } from 'constants/states'; -import { LaunchTensorBoardParams } from 'services/types'; +import { LaunchTensorBoardParams, LaunchTensorBoardSearchesParams } from 'services/types'; import * as Type from 'types'; import { CommandState, RunState, State } from 'types'; @@ -221,6 +221,23 @@ export const tensorBoardMatchesSource = ( return false; }; +// Checks whether tensorboard source matches a given source list. +export const tensorBoardSearchesMatchesSource = ( + tensorBoard: Type.CommandTask, + source: LaunchTensorBoardSearchesParams, +): boolean => { + if (source.searchIds) { + source.searchIds?.sort(); + tensorBoard.misc?.experimentIds?.sort(); + + if (_.isEqual(tensorBoard.misc?.experimentIds, source.searchIds)) { + return true; + } + } + + return false; +}; + const commandStateSortOrder: CommandState[] = [ CommandState.Pulling, CommandState.Starting,