Repository: mlflow/mlflow

Issue:
[BUG] run_name doesn't work with start_run
### Issues Policy acknowledgement

- [X] I have read and agree to submit bug reports in accordance with the [issues policy](https://www.github.com/mlflow/mlflow/blob/master/ISSUE_POLICY.md)

### Willingness to contribute

Yes. I can contribute a fix for this bug independently.

### MLflow version

- Client: 1.30.0

### System information

- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: macOS Ventura 13.0
- **Python version**: 3.9.10
- **yarn version, if running the dev UI**: N/A


### Describe the problem

When using MLFlow with the context manager, the specified `run_name` isn't saved to the MLFlow UI.

This issue only happens with version 1.30.0, I tried the following versions as well: 1.27.0, 1.28.0 and 1.29.0. For all of those versions, the `run_name` was saved properly.

### Tracking information

MLflow version: 1.30.0
Tracking URI: https://XXX.com/
Artifact URI: s3://XXX/XXX/artifacts

### Code to reproduce issue

```python
import mlflow

mlflow.set_experiment("mlflow_issue_experiment")
with mlflow.start_run(run_name="test_version_1.30.0"):
    mlflow.log_metric("score", 1)
```

### Stack trace

N/A

### Other info / logs

_No response_

### What component(s) does this bug affect?

- [ ] `area/artifacts`: Artifact stores and artifact logging
- [ ] `area/build`: Build and test infrastructure for MLflow
- [ ] `area/docs`: MLflow documentation pages
- [ ] `area/examples`: Example code
- [ ] `area/model-registry`: Model Registry service, APIs, and the fluent client calls for Model Registry
- [ ] `area/models`: MLmodel format, model serialization/deserialization, flavors
- [ ] `area/pipelines`: Pipelines, Pipeline APIs, Pipeline configs, Pipeline Templates
- [ ] `area/projects`: MLproject format, project running backends
- [ ] `area/scoring`: MLflow Model server, model deployment tools, Spark UDFs
- [ ] `area/server-infra`: MLflow Tracking server backend
- [X] `area/tracking`: Tracking Service, tracking client APIs, autologging

### What interface(s) does this bug affect?

- [ ] `area/uiux`: Front-end, user experience, plotting, JavaScript, JavaScript dev server
- [X] `area/docker`: Docker use across MLflow's components, such as MLflow Projects and MLflow Models
- [ ] `area/sqlalchemy`: Use of SQLAlchemy in the Tracking Service or Model Registry
- [ ] `area/windows`: Windows support

### What language(s) does this bug affect?

- [ ] `language/r`: R APIs and clients
- [ ] `language/java`: Java APIs and clients
- [ ] `language/new`: Proposals for new client languages

### What integration(s) does this bug affect?

- [ ] `integrations/azure`: Azure and Azure ML integrations
- [ ] `integrations/sagemaker`: SageMaker integrations
- [ ] `integrations/databricks`: Databricks integrations


Discussion:
Hi @Cokral , is your MLflow server also running MLflow 1.30.0? cc @BenWilson2 
Looks similar to https://github.com/mlflow/mlflow/issues/7195.
Ran the following code on master:

```python
import mlflow
from unittest.mock import patch


mlflow.set_tracking_uri("http://localhost:5000")

with patch(
    "requests.Session.request",
    lambda *args, **kwargs: print(kwargs.get("json")),
) as mock_request:
    with mlflow.start_run(run_name="test"):
        pass
```

Result (formatted):

```python
{
    "experiment_id": "0",
    "user_id": "haru",
    "run_name": "test",
    "start_time": 1667362376520,
    "tags": [
        {"key": "mlflow.user", "value": "haru"},
        {"key": "mlflow.source.name", "value": "a.py"},
        {"key": "mlflow.source.type", "value": "LOCAL"},
        {"key": "mlflow.source.git.commit", "value": "e20e81af02424c38860df2ac05d2883539aab07d"},
    ],
}
```

`tags` should contain `mlflow.runName`, but it doesn't.
With MLflow 1.29.0:

```python
{
    "experiment_id": "0",
    "user_id": "root",
    "start_time": 1667362728979,
    "tags": [
        {"key": "mlflow.user", "value": "root"},
        {"key": "mlflow.source.name", "value": "a.py"},
        {"key": "mlflow.source.type", "value": "LOCAL"},
        {"key": "mlflow.runName", "value": "test"},
    ],
}
```

Command:

```
docker run --rm -w /mlflow -v $(pwd)/a.py:/mlflow/a.py python:3.8 bash -c "pip install mlflow==1.29.0 && python a.py"
```
We need to revert https://github.com/mlflow/mlflow/commit/5ddfe9a1b48e065540094d83125040d3273c48fa#diff-b33420360cc29a5224c8d3080bbfcaa0ac1c8891416137e13be044fcdfdd26f9L348-L349
We can use `mlflow.start_run(tags={"mlflow.runName": "test"})` as a workaround.
Thanks @harupy ! I completely agree - those lines need to be reintroduced :)
@Cokral We can make the following change to fix this issue. Looking forward to your PR :)

```diff
diff --git a/mlflow/tracking/fluent.py b/mlflow/tracking/fluent.py
index a67a0f770..3cfb49473 100644
--- a/mlflow/tracking/fluent.py
+++ b/mlflow/tracking/fluent.py
@@ -33,6 +33,7 @@ from mlflow.utils.autologging_utils import (
 from mlflow.utils.import_hooks import register_post_import_hook
 from mlflow.utils.mlflow_tags import (
     MLFLOW_PARENT_RUN_ID,
+    MLFLOW_RUN_NAME,
     MLFLOW_RUN_NOTE,
     MLFLOW_EXPERIMENT_PRIMARY_METRIC_NAME,
     MLFLOW_EXPERIMENT_PRIMARY_METRIC_GREATER_IS_BETTER,
@@ -342,6 +343,8 @@ def start_run(
             user_specified_tags[MLFLOW_RUN_NOTE] = description
         if parent_run_id is not None:
             user_specified_tags[MLFLOW_PARENT_RUN_ID] = parent_run_id
+        if run_name:
+            user_specified_tags[MLFLOW_RUN_NAME] = run_name
 
         resolved_tags = context_registry.resolve_tags(user_specified_tags)
```

Resolving patch:
diff --git a/mlflow/store/tracking/file_store.py b/mlflow/store/tracking/file_store.py
--- a/mlflow/store/tracking/file_store.py
+++ b/mlflow/store/tracking/file_store.py
@@ -579,10 +579,10 @@ def create_run(self, experiment_id, user_id, start_time, tags, run_name):
             )
         tags = tags or []
         run_name_tag = _get_run_name_from_tags(tags)
-        if run_name and run_name_tag:
+        if run_name and run_name_tag and run_name != run_name_tag:
             raise MlflowException(
-                "Both 'run_name' argument and 'mlflow.runName' tag are specified. "
-                "Remove 'mlflow.runName' tag.",
+                "Both 'run_name' argument and 'mlflow.runName' tag are specified, but with "
+                f"different values (run_name='{run_name}', mlflow.runName='{run_name_tag}').",
                 INVALID_PARAMETER_VALUE,
             )
         run_name = run_name or run_name_tag or _generate_random_name()
diff --git a/mlflow/store/tracking/sqlalchemy_store.py b/mlflow/store/tracking/sqlalchemy_store.py
--- a/mlflow/store/tracking/sqlalchemy_store.py
+++ b/mlflow/store/tracking/sqlalchemy_store.py
@@ -470,10 +470,10 @@ def create_run(self, experiment_id, user_id, start_time, tags, run_name):
             )
             tags = tags or []
             run_name_tag = _get_run_name_from_tags(tags)
-            if run_name and run_name_tag:
+            if run_name and run_name_tag and (run_name != run_name_tag):
                 raise MlflowException(
-                    "Both 'run_name' argument and 'mlflow.runName' tag are specified. "
-                    "Remove 'mlflow.runName' tag.",
+                    "Both 'run_name' argument and 'mlflow.runName' tag are specified, but with "
+                    f"different values (run_name='{run_name}', mlflow.runName='{run_name_tag}').",
                     INVALID_PARAMETER_VALUE,
                 )
             run_name = run_name or run_name_tag or _generate_random_name()
diff --git a/mlflow/tracking/fluent.py b/mlflow/tracking/fluent.py
--- a/mlflow/tracking/fluent.py
+++ b/mlflow/tracking/fluent.py
@@ -33,6 +33,7 @@
 from mlflow.utils.import_hooks import register_post_import_hook
 from mlflow.utils.mlflow_tags import (
     MLFLOW_PARENT_RUN_ID,
+    MLFLOW_RUN_NAME,
     MLFLOW_RUN_NOTE,
     MLFLOW_EXPERIMENT_PRIMARY_METRIC_NAME,
     MLFLOW_EXPERIMENT_PRIMARY_METRIC_GREATER_IS_BETTER,
@@ -342,6 +343,8 @@ def start_run(
             user_specified_tags[MLFLOW_RUN_NOTE] = description
         if parent_run_id is not None:
             user_specified_tags[MLFLOW_PARENT_RUN_ID] = parent_run_id
+        if run_name:
+            user_specified_tags[MLFLOW_RUN_NAME] = run_name
 
         resolved_tags = context_registry.resolve_tags(user_specified_tags)
 


Test patch:
diff --git a/tests/store/tracking/test_file_store.py b/tests/store/tracking/test_file_store.py
--- a/tests/store/tracking/test_file_store.py
+++ b/tests/store/tracking/test_file_store.py
@@ -851,13 +851,16 @@ def test_create_run_sets_name(self):
 
         with pytest.raises(
             MlflowException,
-            match=re.escape("Both 'run_name' argument and 'mlflow.runName' tag are specified."),
+            match=re.escape(
+                "Both 'run_name' argument and 'mlflow.runName' tag are specified, but with "
+                "different values (run_name='my name', mlflow.runName='test')."
+            ),
         ):
             fs.create_run(
                 experiment_id=FileStore.DEFAULT_EXPERIMENT_ID,
                 user_id="user",
                 start_time=0,
-                run_name="test",
+                run_name="my name",
                 tags=[RunTag(MLFLOW_RUN_NAME, "test")],
             )
 
diff --git a/tests/store/tracking/test_rest_store.py b/tests/store/tracking/test_rest_store.py
--- a/tests/store/tracking/test_rest_store.py
+++ b/tests/store/tracking/test_rest_store.py
@@ -148,6 +148,7 @@ def test_requestor(self):
 
         user_name = "mock user"
         source_name = "rest test"
+        run_name = "my name"
 
         source_name_patch = mock.patch(
             "mlflow.tracking.context.default_context._get_source_name", return_value=source_name
@@ -163,17 +164,18 @@ def test_requestor(self):
         ), mock.patch(
             "time.time", return_value=13579
         ), source_name_patch, source_type_patch:
-            with mlflow.start_run(experiment_id="43", run_name="my name"):
+            with mlflow.start_run(experiment_id="43", run_name=run_name):
                 cr_body = message_to_json(
                     CreateRun(
                         experiment_id="43",
                         user_id=user_name,
-                        run_name="my name",
+                        run_name=run_name,
                         start_time=13579000,
                         tags=[
                             ProtoRunTag(key="mlflow.source.name", value=source_name),
                             ProtoRunTag(key="mlflow.source.type", value="LOCAL"),
                             ProtoRunTag(key="mlflow.user", value=user_name),
+                            ProtoRunTag(key="mlflow.runName", value=run_name),
                         ],
                     )
                 )
diff --git a/tests/store/tracking/test_sqlalchemy_store.py b/tests/store/tracking/test_sqlalchemy_store.py
--- a/tests/store/tracking/test_sqlalchemy_store.py
+++ b/tests/store/tracking/test_sqlalchemy_store.py
@@ -853,14 +853,17 @@ def test_create_run_sets_name(self):
 
         with pytest.raises(
             MlflowException,
-            match=re.escape("Both 'run_name' argument and 'mlflow.runName' tag are specified."),
+            match=re.escape(
+                "Both 'run_name' argument and 'mlflow.runName' tag are specified, but with "
+                "different values (run_name='test', mlflow.runName='test_2').",
+            ),
         ):
             self.store.create_run(
                 experiment_id=experiment_id,
                 user_id="user",
                 start_time=0,
                 run_name="test",
-                tags=[RunTag(mlflow_tags.MLFLOW_RUN_NAME, "test")],
+                tags=[RunTag(mlflow_tags.MLFLOW_RUN_NAME, "test_2")],
             )
 
     def test_get_run_with_name(self):
diff --git a/tests/tracking/fluent/test_fluent.py b/tests/tracking/fluent/test_fluent.py
--- a/tests/tracking/fluent/test_fluent.py
+++ b/tests/tracking/fluent/test_fluent.py
@@ -454,12 +454,14 @@ def test_start_run_defaults(empty_active_run_stack):  # pylint: disable=unused-a
     source_version_patch = mock.patch(
         "mlflow.tracking.context.git_context._get_source_version", return_value=mock_source_version
     )
+    run_name = "my name"
 
     expected_tags = {
         mlflow_tags.MLFLOW_USER: mock_user,
         mlflow_tags.MLFLOW_SOURCE_NAME: mock_source_name,
         mlflow_tags.MLFLOW_SOURCE_TYPE: SourceType.to_string(SourceType.NOTEBOOK),
         mlflow_tags.MLFLOW_GIT_COMMIT: mock_source_version,
+        mlflow_tags.MLFLOW_RUN_NAME: run_name,
     }
 
     create_run_patch = mock.patch.object(MlflowClient, "create_run")
@@ -472,7 +474,7 @@ def test_start_run_defaults(empty_active_run_stack):  # pylint: disable=unused-a
         source_version_patch,
         create_run_patch,
     ):
-        active_run = start_run(run_name="my name")
+        active_run = start_run(run_name=run_name)
         MlflowClient.create_run.assert_called_once_with(
             experiment_id=mock_experiment_id, tags=expected_tags, run_name="my name"
         )