Repository: marimo-team/marimo
Pull request: fix: lower timeout in version check
Fixes #3580

- lower timeout in version check
- if check fails, don't retry 

Problem statement:
Marimo very slow to start
### Describe the bug

I mostly code in WSL2 running Ubuntu.
Lately, opening marimo notebooks is painfully slow.
I have tried to enable debug logging etc but it doesn't show me much.

I have shown the result of running uv tool run but I get similar results from running marimo directly from a virtual environment. Similar delays without the logging or dev mode.

What else can I do to gather more information about this?
Thanks.

`uv tool run marimo -l DEBUG -d edit test.py`

`[D 250127 12:58:35 file_router:43] Routing to file test.py`
`WARNING:  Current configuration will not reload as not all conditions are met, please refer to documentation.`
`INFO:     Started server process [9633]`
`INFO:     Waiting for application startup.`
`[D 250127 13:02:57 lifespans:56] Setup: lsp`

### Environment

<details>

```
{
  "marimo": "0.10.17",
  "OS": "Linux",
  "OS Version": "5.15.167.4-microsoft-standard-WSL2",
  "Processor": "x86_64",
  "Python Version": "3.13.1",
  "Binaries": {
    "Browser": "--",
    "Node": "v23.6.1"
  },
  "Dependencies": {
    "click": "8.1.8",
    "docutils": "0.21.2",
    "itsdangerous": "2.2.0",
    "jedi": "0.19.2",
    "markdown": "3.7",
    "narwhals": "1.24.0",
    "packaging": "24.2",
    "psutil": "6.1.1",
    "pygments": "2.19.1",
    "pymdown-extensions": "10.14.1",
    "pyyaml": "6.0.2",
    "ruff": "0.9.3",
    "starlette": "0.45.3",
    "tomlkit": "0.13.2",
    "typing-extensions": "missing",
    "uvicorn": "0.34.0",
    "websockets": "14.2"
  },
  "Optional Dependencies": {},
  "Experimental Flags": {}
}
```

</details>


### Code to reproduce

_No response_

Hints from issue discussion:
Yea, 4 seconds between those two lines seems off. For reference, this is mine:

```
[D 250127 10:31:32 file_router:43] Routing to file test.py
WARNING:  Current configuration will not reload as not all conditions are met, please refer to documentation.
INFO:     Started server process [77133]
INFO:     Waiting for application startup.
[D 250127 10:31:32 lifespans:56] Setup: lsp
```

Did this just happen to you or has it been happening?  Can you try this in another python file? 


```python
import time
from marimo._config.manager import get_default_config_manager

start = time.time()
get_default_config_manager(current_path=None).get_config()
end = time.time()
print(end - start)
```

Commit message: fix logging

Patch to review:
diff --git a/marimo/_cli/upgrade.py b/marimo/_cli/upgrade.py
index 2a31eb2bfb5..1dd8034bf51 100644
--- a/marimo/_cli/upgrade.py
+++ b/marimo/_cli/upgrade.py
@@ -3,18 +3,21 @@
 
 import json
 import os
+import urllib.error
 import urllib.request
 from dataclasses import dataclass
 from datetime import datetime
 from typing import Any, Callable, Dict, Optional
 
-from marimo import __version__ as current_version
+from marimo import __version__ as current_version, _loggers
 from marimo._cli.print import echo, green, orange
 from marimo._server.api.status import HTTPException
 from marimo._tracer import server_tracer
 from marimo._utils.config.config import ConfigReader
 
-FETCH_TIMEOUT = 5
+FETCH_TIMEOUT = 3
+
+LOGGER = _loggers.marimo_logger()
 
 
 @dataclass
@@ -34,9 +37,9 @@ def print_latest_version(current_version: str, latest_version: str) -> None:
 def check_for_updates(on_update: Callable[[str, str], None]) -> None:
     try:
         _check_for_updates_internal(on_update)
-    except Exception:
-        # Errors are caught internally
-        # but as a last resort, we don't want to crash the CLI
+    except Exception as e:
+        LOGGER.warning("Failed to check for updates", exc_info=e)
+        # Don't want to crash the CLI on any errors.
         pass
 
 
@@ -70,6 +73,9 @@ def _check_for_updates_internal(on_update: Callable[[str, str], None]) -> None:
     config_reader.write_toml(state)
 
 
+DATA_FORMAT = "%Y-%m-%d"
+
+
 def _update_with_latest_version(state: MarimoCLIState) -> MarimoCLIState:
     """
     If we have not saved the latest version,
@@ -85,40 +91,56 @@ def _update_with_latest_version(state: MarimoCLIState) -> MarimoCLIState:
         api_url = "https://marimo.io/api/oss/latest-version"
 
     # We only update the state once a day
+    now = datetime.now()
     if state.last_checked_at:
         last_checked_date = datetime.strptime(
-            state.last_checked_at, "%Y-%m-%d"
-        ).date()
-        now = datetime.now()
-        year = last_checked_date.timetuple().tm_year
-        today_year = now.timetuple().tm_year
-        day_of_the_year = last_checked_date.timetuple().tm_yday
-        today_day_of_the_year = now.timetuple().tm_yday
-        if year == today_year and today_day_of_the_year == day_of_the_year:
+            state.last_checked_at, DATA_FORMAT
+        )
+        if _is_same_day(last_checked_date, now):
             # Same day, so do nothing
             return state
 
-    # Fetch the latest version from PyPI
     try:
+        # Fetch the latest version from PyPI
         response = _fetch_data_from_url(api_url)
         version = response["info"]["version"]
         state.latest_version = version
-        state.last_checked_at = datetime.now().strftime("%Y-%m-%d")
+        state.last_checked_at = now.strftime(DATA_FORMAT)
         return state
     except Exception:
-        # Avoid errors blocking the CLI or adding noise
+        # Set that we have checked for updates
+        # so we don't fail multiple times a day
+        state.last_checked_at = now.strftime(DATA_FORMAT)
         return state
 
 
 def _fetch_data_from_url(url: str) -> Dict[str, Any]:
-    with urllib.request.urlopen(url, timeout=FETCH_TIMEOUT) as response:
-        status = response.status
-        if status == 200:
-            data = response.read()
-            encoding = response.info().get_content_charset("utf-8")
-            return json.loads(data.decode(encoding))  # type: ignore
-        else:
-            raise HTTPException(
-                status_code=status,
-                detail=f"HTTP request failed with status code {status}",
-            )
+    try:
+        with urllib.request.urlopen(url, timeout=FETCH_TIMEOUT) as response:
+            status = response.status
+            if status == 200:
+                data = response.read()
+                encoding = response.info().get_content_charset("utf-8")
+                return json.loads(data.decode(encoding))  # type: ignore
+            else:
+                raise HTTPException(
+                    status_code=status,
+                    detail=f"HTTP request failed with status code {status}",
+                )
+    except urllib.error.URLError as e:
+        LOGGER.warning(
+            f"Network error while checking for version updates: {e}"
+        )
+        raise
+    except json.JSONDecodeError as e:
+        LOGGER.warning(f"Invalid JSON response from version check: {e}")
+        raise
+    except TimeoutError:
+        LOGGER.warning(
+            f"Timeout ({FETCH_TIMEOUT}s) while checking for version updates"
+        )
+        raise
+
+
+def _is_same_day(date1: datetime, date2: datetime) -> bool:
+    return date1.date() == date2.date()
diff --git a/tests/_cli/test_upgrade.py b/tests/_cli/test_upgrade.py
index b42421d2ddc..989fae5bd6f 100644
--- a/tests/_cli/test_upgrade.py
+++ b/tests/_cli/test_upgrade.py
@@ -1,6 +1,8 @@
 # Copyright 2024 Marimo. All rights reserved.
 from __future__ import annotations
 
+import json
+import urllib.error
 from datetime import datetime
 from typing import Any
 from unittest.mock import mock_open, patch
@@ -106,3 +108,53 @@ def test_update_with_within_the_same_day(
     assert state == updated_state
     # Assert that the _fetch_data_from_url was not called
     mock_fetch_data_from_url.assert_not_called()
+
+
+@patch("marimo._cli.upgrade.current_version", "0.1.0")
+@patch("marimo._cli.upgrade._fetch_data_from_url")
+def test_version_comparison_edge_cases(mock_fetch_data_from_url: Any) -> None:
+    # Test same version
+    mock_fetch_data_from_url.return_value = {"info": {"version": "0.1.0"}}
+    with patch("marimo._cli.upgrade.echo") as mock_echo:
+        check_for_updates(print_latest_version)
+        mock_echo.assert_not_called()
+
+    # Test pre-release version
+    mock_fetch_data_from_url.return_value = {
+        "info": {"version": "0.2.0-alpha.1"}
+    }
+    with patch("marimo._cli.upgrade.echo") as mock_echo:
+        check_for_updates(print_latest_version)
+        mock_echo.assert_called()
+
+    # Test lower version (shouldn't happen in practice)
+    mock_fetch_data_from_url.return_value = {"info": {"version": "0.0.9"}}
+    with patch("marimo._cli.upgrade.echo") as mock_echo:
+        check_for_updates(print_latest_version)
+        mock_echo.assert_not_called()
+
+
+@patch("marimo._cli.upgrade._fetch_data_from_url")
+def test_network_errors(mock_fetch_data_from_url: Any) -> None:
+    state = MarimoCLIState(
+        latest_version="0.1.0", last_checked_at="2020-01-01"
+    )
+
+    # Test timeout
+    mock_fetch_data_from_url.side_effect = TimeoutError()
+    updated_state = _update_with_latest_version(state)
+    assert updated_state.latest_version == "0.1.0"  # Should keep old version
+
+    # Test network error
+    mock_fetch_data_from_url.side_effect = urllib.error.URLError(
+        "Network down"
+    )
+    updated_state = _update_with_latest_version(state)
+    assert updated_state.latest_version == "0.1.0"  # Should keep old version
+
+    # Test invalid JSON
+    mock_fetch_data_from_url.side_effect = json.JSONDecodeError(
+        "Invalid JSON", "", 0
+    )
+    updated_state = _update_with_latest_version(state)
+    assert updated_state.latest_version == "0.1.0"  # Should keep old version


Code review comments:
diff_hunk: @@ -70,6 +73,9 @@ def _check_for_updates_internal(on_update: Callable[[str, str], None]) -> None:
     config_reader.write_toml(state)
 
 
+DATA_FORMAT = "%Y-%m-%d"
path: marimo/_cli/upgrade.py
text: @user1:
DATE_FORMAT ?