Skip to content

Commit c46ba7c

Browse files
authored
Merge pull request #1138 from golemfactory/scx1332/golem_registry_integration
* Added image tag selection for blender * Working on registry integration * Working on resolving images using golem registry * Working on resolving packages * Modified blender example only * Working on blender example * Revert init.py * Removed srv test (no longer needed) * Trying to fix styling * format * Fix type checkings * Revert accidental commit * revert accidental commit * Update * fix * formatting * revert the blender example, add registry usage as a separate example * pull request review * add tests for vm.repo * remove overeager checks * throw PackageException instead of the generic ValueError when resolving an image * use `dev_mode` argument to `vm.repo` to mark resolve queries as "dev mode" instead of a hidden env var * remove `srvresolver` from requirements, add re-run for the connection error... * remove irrelevant `todo` ;) * add one more error to retry list --------- Co-authored-by: shadeofblue <[email protected]>
2 parents 53fb007 + 9a062fa commit c46ba7c

File tree

6 files changed

+430
-92
lines changed

6 files changed

+430
-92
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
#!/usr/bin/env python3
2+
import pathlib
3+
import sys
4+
from datetime import datetime, timedelta
5+
6+
from yapapi import Golem, Task, WorkContext
7+
from yapapi.payload import vm
8+
from yapapi.rest.activity import BatchTimeoutError
9+
10+
examples_dir = pathlib.Path(__file__).resolve().parent.parent
11+
sys.path.append(str(examples_dir))
12+
13+
from utils import (
14+
TEXT_COLOR_CYAN,
15+
TEXT_COLOR_DEFAULT,
16+
TEXT_COLOR_MAGENTA,
17+
TEXT_COLOR_RED,
18+
build_parser,
19+
format_usage,
20+
print_env_info,
21+
run_golem_example,
22+
)
23+
24+
25+
async def start(subnet_tag, package, payment_driver=None, payment_network=None, show_usage=False):
26+
async def worker(ctx: WorkContext, tasks):
27+
script_dir = pathlib.Path(__file__).resolve().parent
28+
scene_path = str(script_dir / "cubes.blend")
29+
30+
# Set timeout for the first script executed on the provider. Usually, 30 seconds
31+
# should be more than enough for computing a single frame of the provided scene,
32+
# however a provider may require more time for the first task if it needs to download
33+
# the VM image first. Once downloaded, the VM image will be cached and other tasks that use
34+
# that image will be computed faster.
35+
script = ctx.new_script(timeout=timedelta(minutes=10))
36+
script.upload_file(scene_path, "/golem/resource/scene.blend")
37+
38+
async for task in tasks:
39+
frame = task.data
40+
crops = [{"outfilebasename": "out", "borders_x": [0.0, 1.0], "borders_y": [0.0, 1.0]}]
41+
script.upload_json(
42+
{
43+
"scene_file": "/golem/resource/scene.blend",
44+
"resolution": (400, 300),
45+
"use_compositing": False,
46+
"crops": crops,
47+
"samples": 100,
48+
"frames": [frame],
49+
"output_format": "PNG",
50+
"RESOURCES_DIR": "/golem/resources",
51+
"WORK_DIR": "/golem/work",
52+
"OUTPUT_DIR": "/golem/output",
53+
},
54+
"/golem/work/params.json",
55+
)
56+
57+
script.run("/golem/entrypoints/run-blender.sh")
58+
output_file = f"output_{frame}.png"
59+
script.download_file(f"/golem/output/out{frame:04d}.png", output_file)
60+
try:
61+
yield script
62+
# TODO: Check if job results are valid
63+
# and reject by: task.reject_task(reason = 'invalid file')
64+
task.accept_result(result=output_file)
65+
except BatchTimeoutError:
66+
print(
67+
f"{TEXT_COLOR_RED}"
68+
f"Task {task} timed out on {ctx.provider_name}, time: {task.running_time}"
69+
f"{TEXT_COLOR_DEFAULT}"
70+
)
71+
raise
72+
73+
# reinitialize the script which we send to the engine to compute subsequent frames
74+
script = ctx.new_script(timeout=timedelta(minutes=1))
75+
76+
if show_usage:
77+
raw_state = await ctx.get_raw_state()
78+
usage = format_usage(await ctx.get_usage())
79+
cost = await ctx.get_cost()
80+
print(
81+
f"{TEXT_COLOR_MAGENTA}"
82+
f" --- {ctx.provider_name} STATE: {raw_state}\n"
83+
f" --- {ctx.provider_name} USAGE: {usage}\n"
84+
f" --- {ctx.provider_name} COST: {cost}"
85+
f"{TEXT_COLOR_DEFAULT}"
86+
)
87+
88+
# Iterator over the frame indices that we want to render
89+
frames: range = range(0, 60, 10)
90+
# Worst-case overhead, in minutes, for initialization (negotiation, file transfer etc.)
91+
# TODO: make this dynamic, e.g. depending on the size of files to transfer
92+
init_overhead = 3
93+
# Providers will not accept work if the timeout is outside of the [5 min, 30min] range.
94+
# We increase the lower bound to 6 min to account for the time needed for our demand to
95+
# reach the providers.
96+
min_timeout, max_timeout = 6, 30
97+
98+
timeout = timedelta(minutes=max(min(init_overhead + len(frames) * 2, max_timeout), min_timeout))
99+
100+
async with Golem(
101+
budget=10.0,
102+
subnet_tag=subnet_tag,
103+
payment_driver=payment_driver,
104+
payment_network=payment_network,
105+
) as golem:
106+
print_env_info(golem)
107+
108+
num_tasks = 0
109+
start_time = datetime.now()
110+
111+
completed_tasks = golem.execute_tasks(
112+
worker,
113+
[Task(data=frame) for frame in frames],
114+
payload=package,
115+
max_workers=3,
116+
timeout=timeout,
117+
)
118+
async for task in completed_tasks:
119+
num_tasks += 1
120+
print(
121+
f"{TEXT_COLOR_CYAN}"
122+
f"Task computed: {task}, result: {task.result}, time: {task.running_time}"
123+
f"{TEXT_COLOR_DEFAULT}"
124+
)
125+
126+
print(
127+
f"{TEXT_COLOR_CYAN}"
128+
f"{num_tasks} tasks computed, total time: {datetime.now() - start_time}"
129+
f"{TEXT_COLOR_DEFAULT}"
130+
)
131+
132+
133+
async def create_package(args, default_image_tag):
134+
# Use golem/blender:latest image tag,
135+
# you can overwrite this option with --image-tag or --image-hash
136+
if args.image_url and args.image_tag:
137+
raise ValueError("Only one of --image-url and --image-tag can be specified")
138+
if args.image_url and not args.image_hash:
139+
raise ValueError("--image-url requires --image-hash to be specified")
140+
if args.image_hash and args.image_tag:
141+
raise ValueError("Only one of --image-hash and --image-tag can be specified")
142+
elif args.image_hash:
143+
image_tag = None
144+
else:
145+
image_tag = args.image_tag or default_image_tag
146+
147+
# resolve image by tag, hash or direct link
148+
package = await vm.repo(
149+
image_tag=image_tag,
150+
image_hash=args.image_hash,
151+
image_url=args.image_url,
152+
image_use_https=args.image_use_https,
153+
# only run on provider nodes that have more than 0.5gb of RAM available
154+
min_mem_gib=0.5,
155+
# only run on provider nodes that have more than 2gb of storage space available
156+
min_storage_gib=2.0,
157+
# only run on provider nodes which a certain number of CPU threads (logical CPU cores)
158+
# available
159+
min_cpu_threads=args.min_cpu_threads,
160+
)
161+
return package
162+
163+
164+
async def main(args):
165+
# Create a package using options specified in the command line
166+
package = await create_package(args, default_image_tag="golem/blender:latest")
167+
168+
await start(
169+
subnet_tag=args.subnet_tag,
170+
package=package,
171+
payment_driver=args.payment_driver,
172+
payment_network=args.payment_network,
173+
show_usage=args.show_usage,
174+
)
175+
176+
177+
if __name__ == "__main__":
178+
parser = build_parser("Render a Blender scene")
179+
parser.add_argument("--show-usage", action="store_true", help="show activity usage and cost")
180+
parser.add_argument(
181+
"--min-cpu-threads",
182+
type=int,
183+
default=1,
184+
help="require the provider nodes to have at least this number of available CPU threads",
185+
)
186+
parser.add_argument(
187+
"--image-tag", help="Image tag to use when resolving image url from Golem Registry"
188+
)
189+
parser.add_argument(
190+
"--image-hash", help="Image hash to use when resolving image url from Golem Registry"
191+
)
192+
parser.add_argument(
193+
"--image-url", help="Direct image url to use instead of resolving from Golem Registry"
194+
)
195+
parser.add_argument(
196+
"--image-use-https",
197+
help="Whether to use https when resolving image url from Golem Registry",
198+
action="store_true",
199+
)
200+
now = datetime.now().strftime("%Y-%m-%d_%H.%M.%S")
201+
parser.set_defaults(log_file=f"blender-yapapi-{now}.log")
202+
cmd_args = parser.parse_args()
203+
204+
run_golem_example(
205+
main(args=cmd_args),
206+
log_file=cmd_args.log_file,
207+
)

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ jsonrpc-base = "^1.0.3"
2929

3030
ya-aioclient = "^0.6.4"
3131
toml = "^0.10.1"
32-
srvresolver = "^0.3.5"
3332
colorama = "^0.4.4"
3433
semantic-version = "^2.8"
3534
attrs = ">=19.3"
@@ -82,7 +81,7 @@ _format_black = "black ."
8281

8382
tests_unit = {cmd = "pytest --cov=yapapi --cov-report html --cov-report term -sv --ignore tests/goth_tests", help = "Run only unit tests"}
8483
tests_integration_init = { sequence = ["_gothv_env", "_gothv_requirements", "_gothv_assets"], help="Initialize the integration test environment"}
85-
tests_integration = { cmd = ".envs/yapapi-goth/bin/python -m pytest -svx tests/goth_tests --config-override docker-compose.build-environment.use-prerelease=false --config-path tests/goth_tests/assets/goth-config.yml --ssh-verify-connection --reruns 3 --only-rerun AssertionError --only-rerun TimeoutError --only-rerun goth.runner.exceptions.TemporalAssertionError --only-rerun urllib.error.URLError --only-rerun goth.runner.exceptions.CommandError", help = "Run the integration tests"}
84+
tests_integration = { cmd = ".envs/yapapi-goth/bin/python -m pytest -svx tests/goth_tests --config-override docker-compose.build-environment.use-prerelease=false --config-path tests/goth_tests/assets/goth-config.yml --ssh-verify-connection --reruns 3 --only-rerun AssertionError --only-rerun TimeoutError --only-rerun goth.runner.exceptions.TemporalAssertionError --only-rerun urllib.error.URLError --only-rerun goth.runner.exceptions.CommandError --only-rerun requests.exceptions.ConnectionError --only-rerun OSError", help = "Run the integration tests"}
8685
_gothv_env = "python -m venv .envs/yapapi-goth"
8786
_gothv_requirements = ".envs/yapapi-goth/bin/pip install -U --extra-index-url https://test.pypi.org/simple/ goth==0.14.1 pip pytest pytest-asyncio pytest-rerunfailures pexpect"
8887
_gothv_assets = ".envs/yapapi-goth/bin/python -m goth create-assets tests/goth_tests/assets"

tests/payload/test_repo.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from unittest.mock import AsyncMock
2+
3+
import pytest
4+
5+
from yapapi.payload import vm
6+
from yapapi.payload.package import PackageException
7+
8+
_MOCK_HTTP_ADDR = "http://test.address/"
9+
_MOCK_HTTPS_ADDR = "https://test.address/"
10+
_MOCK_SHA3 = "abcdef124356789"
11+
_MOCK_SIZE = 2**24
12+
13+
14+
async def _mock_response(*args, **kwargs):
15+
mock = AsyncMock()
16+
mock.status = 200
17+
mock.json.return_value = {
18+
"http": _MOCK_HTTP_ADDR,
19+
"https": _MOCK_HTTPS_ADDR,
20+
"sha3": _MOCK_SHA3,
21+
"size": _MOCK_SIZE,
22+
}
23+
return mock
24+
25+
26+
@pytest.mark.parametrize(
27+
"image_hash, image_tag, image_url, image_use_https, "
28+
"expected_url, expected_error, expected_error_msg",
29+
(
30+
("testhash", None, None, False, f"hash:sha3:{_MOCK_SHA3}:{_MOCK_HTTP_ADDR}", None, ""),
31+
(None, "testtag", None, False, f"hash:sha3:{_MOCK_SHA3}:{_MOCK_HTTP_ADDR}", None, ""),
32+
("testhash", None, None, True, f"hash:sha3:{_MOCK_SHA3}:{_MOCK_HTTPS_ADDR}", None, ""),
33+
(None, "testtag", None, True, f"hash:sha3:{_MOCK_SHA3}:{_MOCK_HTTPS_ADDR}", None, ""),
34+
("testhash", None, "http://image", False, "hash:sha3:testhash:http://image", None, ""),
35+
(
36+
None,
37+
None,
38+
None,
39+
False,
40+
None,
41+
PackageException,
42+
"Either an image_hash or an image_tag is required "
43+
"to resolve an image URL from the Golem Registry",
44+
),
45+
(
46+
None,
47+
None,
48+
"http://image",
49+
False,
50+
None,
51+
PackageException,
52+
"An image_hash is required when using a direct image_url",
53+
),
54+
(
55+
None,
56+
"testtag",
57+
"http://image",
58+
False,
59+
None,
60+
PackageException,
61+
"An image_tag can only be used when resolving "
62+
"from Golem Registry, not with a direct image_url",
63+
),
64+
(
65+
"testhash",
66+
"testtag",
67+
None,
68+
False,
69+
None,
70+
PackageException,
71+
"Golem Registry images can be resolved by either "
72+
"an image_hash or by an image_tag but not both",
73+
),
74+
),
75+
)
76+
@pytest.mark.asyncio
77+
async def test_repo(
78+
monkeypatch,
79+
image_hash,
80+
image_tag,
81+
image_url,
82+
image_use_https,
83+
expected_url,
84+
expected_error,
85+
expected_error_msg,
86+
):
87+
monkeypatch.setattr("aiohttp.ClientSession.get", _mock_response)
88+
monkeypatch.setattr("aiohttp.ClientSession.head", _mock_response)
89+
90+
package_awaitable = vm.repo(
91+
image_hash=image_hash,
92+
image_tag=image_tag,
93+
image_url=image_url,
94+
image_use_https=image_use_https,
95+
)
96+
97+
if expected_error:
98+
with pytest.raises(expected_error) as e:
99+
_ = await package_awaitable
100+
assert expected_error_msg in str(e)
101+
else:
102+
package = await package_awaitable
103+
url = await package.resolve_url()
104+
assert url == expected_url

tests/payload/test_vm.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)