Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Delete runs API #11519

Open
wants to merge 38 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5e93575
added delete_runs()
natannvw Mar 25, 2024
061f020
update import
natannvw Mar 25, 2024
ec228da
update import
natannvw Mar 25, 2024
1a5977b
Merge branch 'delete-runs' of https://github.com/natannvw/mlflow into…
natannvw Mar 25, 2024
ba6e2ab
Merge branch 'delete-runs' of https://github.com/natannvw/mlflow into…
natannvw Mar 25, 2024
2b59ecb
Merge branch 'delete-runs' of https://github.com/natannvw/mlflow into…
natannvw Mar 25, 2024
aa81ca9
added try except in delete_runs()
natannvw Mar 26, 2024
fe54a40
change to in_() in delete_runs()
natannvw Mar 26, 2024
3dd9b32
add new proto DeleteRuns
natannvw Mar 26, 2024
6bc1f37
add new proto DeleteRuns
natannvw Mar 26, 2024
c2b8cf8
add _delete_runs()
natannvw Mar 26, 2024
ce6d213
add _assert_list_of_strings()
natannvw Mar 26, 2024
fe1482e
import new proto DeleteRuns
natannvw Mar 26, 2024
e416720
modify delete_runs() to use DeleteRuns()
natannvw Mar 26, 2024
20a9701
added delete_runs()
natannvw Mar 27, 2024
73fe50e
added tests for delete_runs()
natannvw Mar 27, 2024
bfec7a9
changed run_id to run_ids in message DeleteRuns
natannvw Mar 31, 2024
d27c486
Update mlflow/protos/service.proto
natannvw Mar 31, 2024
dc5b205
Merge branch 'delete-runs' of https://github.com/natannvw/mlflow into…
natannvw Mar 31, 2024
4e16b93
ruff
natannvw Apr 1, 2024
062ba45
[ML-35177] Move the new "deprecated" decorator to mlflow (#11506)
ernestwong-db Mar 25, 2024
6a06b17
Set `PYTHONUTF8` to enable UTF-8 encoding by default for Windows (#11…
harupy Mar 25, 2024
8387e25
add docstring to lifecycle_stage property (#11463)
taranarmo Mar 25, 2024
d352ddd
added delete_runs()
natannvw Mar 25, 2024
540fbfd
update import
natannvw Mar 25, 2024
b765e95
added try except in delete_runs()
natannvw Mar 26, 2024
72149bb
change to in_() in delete_runs()
natannvw Mar 26, 2024
bcb57e4
add new proto DeleteRuns
natannvw Mar 26, 2024
d1a6416
add new proto DeleteRuns
natannvw Mar 26, 2024
542fcbc
add _delete_runs()
natannvw Mar 26, 2024
90fd644
add _assert_list_of_strings()
natannvw Mar 26, 2024
33bd4db
import new proto DeleteRuns
natannvw Mar 26, 2024
c49dfd8
modify delete_runs() to use DeleteRuns()
natannvw Mar 26, 2024
1272442
added delete_runs()
natannvw Mar 27, 2024
3913e07
added tests for delete_runs()
natannvw Mar 27, 2024
7bd2276
changed run_id to run_ids in message DeleteRuns
natannvw Mar 31, 2024
dd97132
ruff
natannvw Apr 1, 2024
32146c5
Merge branch 'delete-runs' of https://github.com/natannvw/mlflow into…
natannvw Apr 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions mlflow/protos/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,15 @@ message DeleteRun {
message Response {}
}

message DeleteRuns {
option (scalapb.message).extends = "com.databricks.rpc.RPC[$this.Response]";

// IDs of the runs to delete.
repeated string run_id = 1 [(validate_required) = true];
natannvw marked this conversation as resolved.
Show resolved Hide resolved

message Response {}
}

message RestoreRun {
option (scalapb.message).extends = "com.databricks.rpc.RPC[$this.Response]";

Expand Down
20 changes: 20 additions & 0 deletions mlflow/server/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
CreateRun,
DeleteExperiment,
DeleteRun,
DeleteRuns,
DeleteTag,
GetExperiment,
GetExperimentByName,
Expand Down Expand Up @@ -312,6 +313,12 @@ def _assert_string(x):
assert isinstance(x, str)


def _assert_list_of_strings(x):
assert isinstance(x, list)
for item in x:
_assert_string(item)


def _assert_intlike(x):
try:
x = int(x)
Expand Down Expand Up @@ -765,6 +772,18 @@ def _delete_run():
return response


@catch_mlflow_exception
@_disable_if_artifacts_only
def _delete_runs():
request_message = _get_request_message(
DeleteRuns(), schema={"run_id": [_assert_required, _assert_list_of_strings]}
)
_get_tracking_store().delete_runs(request_message.run_id)
response_message = DeleteRuns.Response()
response = Response(mimetype="application/json")
response.set_data(message_to_json(response_message))


@catch_mlflow_exception
@_disable_if_artifacts_only
def _restore_run():
Expand Down Expand Up @@ -2292,6 +2311,7 @@ def get_endpoints(get_handler=get_handler):
CreateRun: _create_run,
UpdateRun: _update_run,
DeleteRun: _delete_run,
DeleteRuns: _delete_runs,
RestoreRun: _restore_run,
LogParam: _log_param,
LogMetric: _log_metric,
Expand Down
11 changes: 11 additions & 0 deletions mlflow/store/tracking/abstract_store.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from abc import ABCMeta, abstractmethod

Check failure on line 1 in mlflow/store/tracking/abstract_store.py

View workflow job for this annotation

GitHub Actions / lint

Unformatted file. Run `ruff format .` to format.
from typing import List, Optional

from mlflow.entities import DatasetInput, ViewType
Expand Down Expand Up @@ -223,6 +223,17 @@
"""
pass

@abstractmethod
def delete_runs(self, run_ids):
"""
Delete multiple runs.

Args:
run_ids: List of run ids to delete.

"""
pass

Check failure on line 236 in mlflow/store/tracking/abstract_store.py

View workflow job for this annotation

GitHub Actions / lint

[*] Blank line contains whitespace. Run `ruff --fix .` to fix this error.
@abstractmethod
def restore_run(self, run_id):
"""
Expand Down
13 changes: 13 additions & 0 deletions mlflow/store/tracking/file_store.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import json

Check failure on line 1 in mlflow/store/tracking/file_store.py

View workflow job for this annotation

GitHub Actions / lint

Unformatted file. Run `ruff format .` to format.
import logging
import os
import shutil
Expand Down Expand Up @@ -531,6 +531,19 @@
new_info = run_info._copy_with_overrides(lifecycle_stage=LifecycleStage.DELETED)
self._overwrite_run_info(new_info, deleted_time=get_current_time_millis())

def delete_runs(self, run_ids):
for run_id in run_ids:
try:
run_info = self._get_run_info(run_id)
if run_info is None:
raise MlflowException(
f"Run '{run_id}' metadata is in invalid state.", databricks_pb2.INVALID_STATE

Check failure on line 540 in mlflow/store/tracking/file_store.py

View workflow job for this annotation

GitHub Actions / lint

Line too long (101 > 100). See https://docs.astral.sh/ruff/rules/E501 for how to fix this error.
)
new_info = run_info._copy_with_overrides(lifecycle_stage=LifecycleStage.DELETED)
self._overwrite_run_info(new_info, deleted_time=get_current_time_millis())
except Exception as e:
logging.error(f"Failed to delete run '{run_id}': {str(e)!s}")

def _hard_delete_run(self, run_id):
"""
Permanently delete a run (metadata and metrics, tags, parameters).
Expand Down
5 changes: 5 additions & 0 deletions mlflow/store/tracking/rest_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
CreateRun,
DeleteExperiment,
DeleteRun,
DeleteRuns,
DeleteTag,
GetExperiment,
GetExperimentByName,
Expand Down Expand Up @@ -313,6 +314,10 @@ def delete_run(self, run_id):
req_body = message_to_json(DeleteRun(run_id=run_id))
self._call_endpoint(DeleteRun, req_body)

def delete_runs(self, run_ids):
req_body = message_to_json(DeleteRuns(run_ids=run_ids))
self._call_endpoint(DeleteRuns, req_body)

def restore_run(self, run_id):
req_body = message_to_json(RestoreRun(run_id=run_id))
self._call_endpoint(RestoreRun, req_body)
Expand Down
11 changes: 10 additions & 1 deletion mlflow/store/tracking/sqlalchemy_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import sqlalchemy
import sqlalchemy.sql.expression as sql
from sqlalchemy import and_, func, sql, text
from sqlalchemy import and_, func, or_, text

Check failure on line 13 in mlflow/store/tracking/sqlalchemy_store.py

View workflow job for this annotation

GitHub Actions / lint

[*] `sqlalchemy.or_` imported but unused. Run `ruff --fix .` to fix this error.
from sqlalchemy.future import select

import mlflow.store.db.utils
Expand Down Expand Up @@ -655,6 +655,15 @@
run.deleted_time = get_current_time_millis()
session.add(run)

def delete_runs(self, run_ids):
with self.ManagedSessionMaker() as session:
conditions = SqlRun.run_uuid.in_(run_ids)
runs = session.query(SqlRun).filter(conditions).all()
for run in runs:
run.lifecycle_stage = LifecycleStage.DELETED
run.deleted_time = get_current_time_millis()
session.add(run)

def _hard_delete_run(self, run_id):
"""
Permanently delete a run (metadata and metrics, tags, parameters).
Expand Down
6 changes: 6 additions & 0 deletions mlflow/tracking/_tracking_service/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,12 @@ def delete_run(self, run_id):
"""
self.store.delete_run(run_id)

def delete_runs(self, run_ids):
"""
Deletes runs with the given IDs.
"""
self.store.delete_runs(run_ids)

def restore_run(self, run_id):
"""
Restores a deleted run with the given ID.
Expand Down
33 changes: 33 additions & 0 deletions mlflow/tracking/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2099,9 +2099,42 @@
run_id: a61c7a1851324f7094e8d5014c58c8c8; lifecycle_stage: active
run_id: a61c7a1851324f7094e8d5014c58c8c8; lifecycle_stage: deleted

"""

Check failure on line 2102 in mlflow/tracking/client.py

View workflow job for this annotation

GitHub Actions / lint

Line too long (115 > 100). See https://docs.astral.sh/ruff/rules/E501 for how to fix this error.
self._tracking_client.delete_run(run_id)

def delete_runs(self, run_ids: List[str]) -> None:
"""Deletes runs with the given IDs.

Check failure on line 2107 in mlflow/tracking/client.py

View workflow job for this annotation

GitHub Actions / lint

Line too long (123 > 100). See https://docs.astral.sh/ruff/rules/E501 for how to fix this error.
Args:
run_ids: List of unique run ids to delete.

.. code-block:: python
:caption: Example

Check failure on line 2112 in mlflow/tracking/client.py

View workflow job for this annotation

GitHub Actions / lint

Line too long (132 > 100). See https://docs.astral.sh/ruff/rules/E501 for how to fix this error.

Check failure on line 2113 in mlflow/tracking/client.py

View workflow job for this annotation

GitHub Actions / lint

Line too long (134 > 100). See https://docs.astral.sh/ruff/rules/E501 for how to fix this error.
from mlflow import MlflowClient

# Create runs under the default experiment (whose id is '0').
client = MlflowClient()
experiment_id = "0"
run1 = client.create_run(experiment_id)
run2 = client.create_run(experiment_id)
run_ids = [run1.info.run_id, run2.info.run_id]
print(f"run_ids: {run_ids}; lifecycle_stage: {[run1.info.lifecycle_stage, run2.info.lifecycle_stage]}")
print("--")
client.delete_runs(run_ids)
del_run1 = client.get_run(run_ids[0])
del_run2 = client.get_run(run_ids[1])
print(f"run_ids: {run_ids}; lifecycle_stage: {[del_run1.info.lifecycle_stage, del_run2.info.lifecycle_stage]}")

.. code-block:: text
:caption: Output

run_ids: ['a61c7a1851324f7094e8d5014c58c8c8', 'b71d7a1851324f7094e8d5014c58c8c9']; lifecycle_stage: ['active', 'active']
run_ids: ['a61c7a1851324f7094e8d5014c58c8c8', 'b71d7a1851324f7094e8d5014c58c8c9']; lifecycle_stage: ['deleted', 'deleted']

"""
self._tracking_client.delete_runs(run_ids)

def restore_run(self, run_id: str) -> None:
"""Restores a deleted run with the given ID.

Expand Down
31 changes: 31 additions & 0 deletions mlflow/tracking/fluent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1755,6 +1755,37 @@ def delete_run(run_id: str) -> None:
"""
MlflowClient().delete_run(run_id)

def delete_runs(run_ids: List[str]) -> None:
"""
Deletes runs with the given IDs.

Args:
run_ids: List of unique run ids to delete.

.. code-block:: python
:caption: Example

import mlflow

run_ids = []
for _ in range(2):
with mlflow.start_run() as run:
mlflow.log_param("p", 0)
run_ids.append(run.info.run_id)

mlflow.delete_runs(run_ids)

lifecycle_stages = [mlflow.get_run(run_id).info.lifecycle_stage for run_id in run_ids]
print(f"run_ids: {run_ids}; lifecycle_stages: {lifecycle_stages}")

.. code-block:: text
:caption: Output

run_ids = ['45f4af3e6fd349e58579b27fcb0b8277', 'b71d7a1851324f7094e8d5014c58c8c9']
lifecycle_stages = ['deleted', 'deleted']

"""
MlflowClient().delete_runs(run_ids)

def get_artifact_uri(artifact_path: Optional[str] = None) -> str:
"""
Expand Down
29 changes: 29 additions & 0 deletions tests/store/tracking/test_sqlalchemy_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,24 @@ def test_delete_run(store: SqlAlchemyStore):
assert actual.run_uuid == deleted_run.info.run_id


def test_delete_runs(store: SqlAlchemyStore):
runs = [_run_factory(store) for _ in range(2)]
run_ids = [run.info.run_id for run in runs]

store.delete_runs(run_ids)

with store.ManagedSessionMaker() as session:
for run_id in run_ids:
actual = session.query(models.SqlRun).filter_by(run_uuid=run_id).first()
assert actual.lifecycle_stage == entities.LifecycleStage.DELETED
assert (
actual.deleted_time is not None
) # deleted time should be updated and thus not None anymore

deleted_run = store.get_run(run_id)
assert actual.run_uuid == deleted_run.info.run_id


def test_hard_delete_run(store: SqlAlchemyStore):
run = _run_factory(store)
metric = entities.Metric("blahmetric", 100.0, get_current_time_millis(), 0)
Expand Down Expand Up @@ -924,6 +942,17 @@ def test_get_deleted_runs(store: SqlAlchemyStore):
assert deleted_run_ids == [run.info.run_uuid]


def test_get_deleted_runs_with_delete_runs(store: SqlAlchemyStore):
runs = [_run_factory(store) for _ in range(2)]
run_ids = [run.info.run_id for run in runs]
deleted_run_ids = store._get_deleted_runs()
assert deleted_run_ids == []

store.delete_runs(run_ids)
deleted_run_ids = store._get_deleted_runs()
assert set(deleted_run_ids) == set(run_ids)


def test_log_metric(store: SqlAlchemyStore):
run = _run_factory(store)

Expand Down
13 changes: 12 additions & 1 deletion tests/tracking/test_tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,18 @@
pass
assert mlflow.active_run() is None


def test_start_deleted_runs():
run_ids = []
for _ in range(2):
with mlflow.start_run() as active_run:
run_ids.append(active_run.info.run_id)
tracking.MlflowClient().delete_runs(run_ids)
for run_id in run_ids:
with pytest.raises(MlflowException, match="because it is in the deleted state."):
with mlflow.start_run(run_id=run_id):
pass
assert mlflow.active_run() is None

Check failure on line 698 in tests/tracking/test_tracking.py

View workflow job for this annotation

GitHub Actions / lint

[*] Blank line contains whitespace. Run `ruff --fix .` to fix this error.
@pytest.mark.usefixtures("reset_active_experiment")
def test_start_run_exp_id_0():
mlflow.set_experiment("some-experiment")
Expand Down