Skip to content

Commit 2c4af12

Browse files
committed
fix(connector): clean up the local code bundle copy
Without the async LRU cache the connector rebuilds a tarball on every remote execution, and each one lands in a random local path that nobody deletes. Solved this by using a temporary directory that is removed once the upload finishes. Signed-off-by: Jeff Chung <sh1001309@gmail.com>
1 parent 183c329 commit 2c4af12

2 files changed

Lines changed: 101 additions & 14 deletions

File tree

src/flyte/connectors/_connector.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import asyncio
22
import json
33
import os
4+
import pathlib
5+
import tempfile
46
import typing
57
from abc import ABC, abstractmethod
68
from dataclasses import asdict, dataclass
@@ -248,24 +250,12 @@ async def execute(self, **kwargs) -> Any:
248250
if not storage.is_remote(tctx.raw_data_path.path):
249251
return await TaskTemplate.execute(self, **kwargs)
250252
else:
251-
local_code_bundle = await build_code_bundle(
252-
from_dir=cfg.root_dir,
253-
dryrun=True,
254-
)
255-
if local_code_bundle.tgz is None:
256-
raise RuntimeError("no tgz found in code bundle")
257-
remote_code_path = await storage.put(
258-
local_code_bundle.tgz, prefix + "/code_bundle/" + os.path.basename(local_code_bundle.tgz)
259-
)
253+
code_bundle = await _build_and_upload_code_bundle(cfg.root_dir, prefix)
260254
sc = SerializationContext(
261255
project=tctx.action.project,
262256
domain=tctx.action.domain,
263257
org=tctx.action.org,
264-
code_bundle=CodeBundle(
265-
tgz=remote_code_path,
266-
computed_version=local_code_bundle.computed_version,
267-
destination="/opt/flyte/",
268-
),
258+
code_bundle=code_bundle,
269259
version=tctx.version,
270260
image_cache=await build_images.aio(task.parent_env()) if task.parent_env else None,
271261
root_dir=cfg.root_dir,
@@ -331,6 +321,25 @@ async def execute(self, **kwargs) -> Any:
331321
return tuple(resource.outputs.values())
332322

333323

324+
async def _build_and_upload_code_bundle(from_dir: pathlib.Path, prefix: str) -> CodeBundle:
325+
with tempfile.TemporaryDirectory(prefix="flyte-code-bundle-") as tmp_dir:
326+
local_code_bundle = await build_code_bundle(
327+
from_dir=from_dir,
328+
dryrun=True,
329+
copy_bundle_to=pathlib.Path(tmp_dir),
330+
)
331+
if local_code_bundle.tgz is None:
332+
raise RuntimeError("no tgz found in code bundle")
333+
remote_code_path = await storage.put(
334+
local_code_bundle.tgz, prefix + "/code_bundle/" + os.path.basename(local_code_bundle.tgz)
335+
)
336+
return CodeBundle(
337+
tgz=remote_code_path,
338+
computed_version=local_code_bundle.computed_version,
339+
destination="/opt/flyte/",
340+
)
341+
342+
334343
async def get_resource_proto(resource: Resource) -> connector_pb2.Resource:
335344
if resource.outputs:
336345
interface = NativeInterface.from_types(inputs={}, outputs={k: type(v) for k, v in resource.outputs.items()})
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import pathlib
2+
3+
import pytest
4+
5+
from flyte.connectors import _connector
6+
from flyte.models import CodeBundle
7+
8+
9+
@pytest.mark.asyncio
10+
async def test_upload_code_bundle_cleans_up_local_copy(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
11+
bundle_dirs: list[pathlib.Path] = []
12+
13+
async def build_code_bundle(
14+
*, from_dir: pathlib.Path, dryrun: bool, copy_bundle_to: pathlib.Path | None = None
15+
) -> CodeBundle:
16+
assert from_dir == tmp_path
17+
assert dryrun is True
18+
assert copy_bundle_to is not None
19+
bundle_dirs.append(copy_bundle_to)
20+
local_bundle = copy_bundle_to / "bundle.tar.gz"
21+
local_bundle.write_bytes(b"bundle")
22+
return CodeBundle(tgz=str(local_bundle), computed_version="v1")
23+
24+
async def put(from_path: str, to_path: str) -> str:
25+
local_path = pathlib.Path(from_path)
26+
assert bundle_dirs
27+
assert local_path.parent == bundle_dirs[0]
28+
assert local_path.exists()
29+
assert to_path == "s3://bucket/prefix/code_bundle/bundle.tar.gz"
30+
return "s3://bucket/uploaded/bundle.tar.gz"
31+
32+
monkeypatch.setattr(_connector, "build_code_bundle", build_code_bundle)
33+
monkeypatch.setattr(_connector.storage, "put", put)
34+
35+
result = await _connector._build_and_upload_code_bundle(tmp_path, "s3://bucket/prefix")
36+
37+
assert result == CodeBundle(
38+
tgz="s3://bucket/uploaded/bundle.tar.gz",
39+
computed_version="v1",
40+
destination="/opt/flyte/",
41+
)
42+
assert len(bundle_dirs) == 1
43+
assert not bundle_dirs[0].exists()
44+
45+
46+
@pytest.mark.asyncio
47+
async def test_upload_code_bundle_cleans_up_local_copy_when_upload_fails(
48+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
49+
) -> None:
50+
bundle_dirs: list[pathlib.Path] = []
51+
52+
async def build_code_bundle(
53+
*, from_dir: pathlib.Path, dryrun: bool, copy_bundle_to: pathlib.Path | None = None
54+
) -> CodeBundle:
55+
assert from_dir == tmp_path
56+
assert dryrun is True
57+
assert copy_bundle_to is not None
58+
bundle_dirs.append(copy_bundle_to)
59+
local_bundle = copy_bundle_to / "bundle.tar.gz"
60+
local_bundle.write_bytes(b"bundle")
61+
return CodeBundle(tgz=str(local_bundle), computed_version="v1")
62+
63+
async def put(from_path: str, to_path: str) -> str:
64+
local_path = pathlib.Path(from_path)
65+
assert bundle_dirs
66+
assert local_path.parent == bundle_dirs[0]
67+
assert local_path.exists()
68+
assert to_path == "s3://bucket/prefix/code_bundle/bundle.tar.gz"
69+
raise OSError("upload failed")
70+
71+
monkeypatch.setattr(_connector, "build_code_bundle", build_code_bundle)
72+
monkeypatch.setattr(_connector.storage, "put", put)
73+
74+
with pytest.raises(OSError, match="upload failed"):
75+
await _connector._build_and_upload_code_bundle(tmp_path, "s3://bucket/prefix")
76+
77+
assert len(bundle_dirs) == 1
78+
assert not bundle_dirs[0].exists()

0 commit comments

Comments
 (0)