8000 fix(docker_executor): explain the missing Docker daemon at startup by dmux · Pull Request #87 · dmux/minilake · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Everything is set through environment variables. Nothing needs a config file.
| `MINILAKE_DOCKER_VOLUME` | unset | Named volume backing `MINILAKE_DATA_DIR`, shared with spawned job containers. Falls back to introspecting minilake's own mounts |
| `MINILAKE_DOCKER_NETWORK` | unset | Network to attach job containers to, so a task can call back into the API. Falls back to introspection when minilake is on exactly one network |
| `MINILAKE_IVY_SEED` | unset (`/opt/ivy-cache` in the image) | Pre-resolved Delta / Unity Catalog jars, copied onto the shared data volume on first job run so `spark-submit --packages` resolves without Maven Central |
| `MINILAKE_SPARK_PREWARM` | unset | `0` skips the best-effort `docker pull` of the Spark image at startup — worth setting on a machine with no network, where the attempt only costs a timeout |
| `MINILAKE_SPARK_PREWARM` | unset | `0` skips the best-effort `docker pull` of the Spark image at startup — worth setting on a machine with no network, where the attempt only costs a timeout. Also silences the "Docker is not reachable" warning on a machine without Docker |

### TLS

Expand Down
5 changes: 5 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ pip install minilake
minilake --port 8000
```

Docker is optional for a `pip` install. Without it everything works except Jobs that run on
real Spark, and startup logs a single warning saying Docker is not reachable. Set
`MINILAKE_SPARK_PREWARM=0` to silence it, or `MINILAKE_JOB_EXECUTOR=subprocess` to run job
tasks as local processes (no Spark).

Add the MCP extra if you want the agent interface: `pip install 'minilake[mcp]'`. See
[MCP overview](mcp/index.md).

Expand Down
21 changes: 17 additions & 4 deletions src/minilake/docker_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from typing import Dict, List, Optional

import docker
from docker.errors import DockerException
from docker.errors import APIError, DockerException

from minilake.config import ensure_writable_dir, settings

Expand Down Expand Up @@ -335,8 +335,21 @@ def prewarm_spark_image(image: Optional[str] = None) -> None:
logger.info(f"Pre-pulling job execution image: {spark_image}")
client.images.pull(spark_image)
logger.info(f"Pre-pulled job execution image: {spark_image}")
except APIError as e:
# The daemon answered but the pull failed (no network, registry error): real
# execution falls back to pulling lazily on first job run.
logger.warning(f"Failed to pre-pull job execution image {spark_image} (non-critical): {e}")
except DockerException as e:
# No daemon to talk to (missing socket, permissions). Common for a plain
# `pip install`, where Docker is optional, so say what it means and how to opt out
# rather than surfacing the raw connection error. Stays a WARNING: the default log
# level hides INFO, and Spark job tasks really will fail without Docker.
logger.warning(
f"Docker is not reachable ({e}); skipping the Spark image pre-pull. "
"Spark job tasks need Docker (or set MINILAKE_JOB_EXECUTOR=subprocess). "
"Set MINILAKE_SPARK_PREWARM=0 to silence this."
)
except Exception as e:
# Broad catch: no Docker socket, no network, wrong permissions, etc. are
# all non-fatal here — real execution just falls back to pulling lazily
# on first job run instead of being pre-warmed.
# Anything else is non-fatal too: real execution just falls back to pulling
# lazily on first job run instead of being pre-warmed.
logger.warning(f"Failed to pre-pull job execution image {spark_image} (non-critical): {e}")
52 changes: 52 additions & 0 deletions tests/test_docker_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
"""

import asyncio
import logging
from pathlib import Path

import pytest
from docker.errors import APIError, DockerException

from minilake import docker_executor

Expand Down Expand Up @@ -156,3 +158,53 @@ def test_subprocess_executor_times_out_for_real(monkeypatch, tmp_path: Path):

assert result.timed_out is True
print("✓ subprocess executor real timeout enforcement works")


def _raise(exc: Exception):
def _factory(*_args, **_kwargs):
raise exc

return _factory


@pytest.mark.error
def test_prewarm_without_docker_logs_an_actionable_warning(monkeypatch, caplog):
"""No Docker socket is expected on a plain `pip install`: say what it means and how to opt out."""
monkeypatch.delenv("MINILAKE_JOB_EXECUTOR", raising=False)
monkeypatch.delenv("MINILAKE_SPARK_PREWARM", raising=False)
monkeypatch.setattr(
docker_executor,
"_get_client",
_raise(DockerException("Error while fetching server API version: FileNotFoundError(2)")),
)

with caplog.at_level(logging.INFO, logger=docker_executor.logger.name):
docker_executor.prewarm_spark_image()

warnings = [r.getMessage() for r in caplog.records if r.levelno == logging.WARNING]
assert not any("Failed to pre-pull" in msg for msg in warnings)
notice = next(msg for msg in warnings if "Docker is not reachable" in msg)
assert "MINILAKE_SPARK_PREWARM=0" in notice
assert "MINILAKE_JOB_EXECUTOR=subprocess" in notice


@pytest.mark.error
def test_prewarm_pull_failure_still_warns(monkeypatch, caplog):
"""A daemon that answers but cannot pull is a real (non-critical) warning."""
monkeypatch.delenv("MINILAKE_JOB_EXECUTOR", raising=False)
monkeypatch.delenv("MINILAKE_SPARK_PREWARM", raising=False)

class _Images:
pull = staticmethod(_raise(APIError("registry unreachable")))

class _Client:
images = _Images()

monkeypatch.setattr(docker_executor, "_get_client", lambda: _Client())

with caplog.at_level(logging.INFO, logger=docker_executor.logger.name):
docker_executor.prewarm_spark_image()

warnings = [r.getMessage() for r in caplog.records if r.levelno == logging.WARNING]
assert any("Failed to pre-pull" in msg for msg in warnings)
assert not any("Docker is not reachable" in r.getMessage() for r in caplog.records)
Loading
0