truss 0.10.13__py3-none-any.whl → 0.11.1rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of truss might be problematic. Click here for more details.

Files changed (27) hide show
  1. truss/cli/chains_commands.py +1 -1
  2. truss/cli/train/core.py +82 -31
  3. truss/contexts/image_builder/serving_image_builder.py +7 -0
  4. truss/contexts/local_loader/docker_build_emulator.py +32 -8
  5. truss/remote/baseten/custom_types.py +7 -0
  6. truss/templates/base.Dockerfile.jinja +35 -6
  7. truss/templates/cache.Dockerfile.jinja +8 -7
  8. truss/templates/control/control/endpoints.py +72 -32
  9. truss/templates/copy_cache_files.Dockerfile.jinja +1 -1
  10. truss/templates/docker_server/supervisord.conf.jinja +1 -0
  11. truss/templates/server/truss_server.py +3 -3
  12. truss/templates/server.Dockerfile.jinja +33 -19
  13. truss/tests/cli/train/test_train_cli_core.py +254 -1
  14. truss/tests/contexts/image_builder/test_serving_image_builder.py +1 -1
  15. truss/tests/templates/control/control/test_endpoints.py +22 -14
  16. truss/tests/templates/control/control/test_server_integration.py +62 -41
  17. truss/tests/templates/server/test_truss_server.py +19 -12
  18. truss/tests/test_data/server.Dockerfile +13 -10
  19. truss/tests/test_model_inference.py +4 -2
  20. {truss-0.10.13.dist-info → truss-0.11.1rc1.dist-info}/METADATA +1 -1
  21. {truss-0.10.13.dist-info → truss-0.11.1rc1.dist-info}/RECORD +27 -27
  22. truss_chains/deployment/deployment_client.py +4 -2
  23. truss_chains/public_types.py +1 -0
  24. truss_chains/remote_chainlet/utils.py +8 -0
  25. {truss-0.10.13.dist-info → truss-0.11.1rc1.dist-info}/WHEEL +0 -0
  26. {truss-0.10.13.dist-info → truss-0.11.1rc1.dist-info}/entry_points.txt +0 -0
  27. {truss-0.10.13.dist-info → truss-0.11.1rc1.dist-info}/licenses/LICENSE +0 -0
@@ -11,7 +11,6 @@
11
11
  {% if config.base_image %}
12
12
  {%- if not config.docker_server %}
13
13
  ENV PYTHONUNBUFFERED="True"
14
- ENV DEBIAN_FRONTEND="noninteractive"
15
14
 
16
15
  {# Install common dependencies #}
17
16
  RUN apt update && \
@@ -20,7 +19,7 @@ RUN apt update && \
20
19
  && apt-get clean -y \
21
20
  && rm -rf /var/lib/apt/lists/*
22
21
 
23
- COPY ./{{ base_server_requirements_filename }} {{ base_server_requirements_filename }}
22
+ COPY --chown={{ default_owner }} ./{{ base_server_requirements_filename }} {{ base_server_requirements_filename }}
24
23
  RUN {{ sys_pip_install_command }} -r {{ base_server_requirements_filename }} --no-cache-dir
25
24
  {%- endif %} {#- endif not config.docker_server #}
26
25
 
@@ -38,7 +37,7 @@ RUN ln -sf {{ config.base_image.python_executable_path }} /usr/local/bin/python
38
37
 
39
38
  {% block install_requirements %}
40
39
  {%- if should_install_server_requirements %}
41
- COPY ./{{ server_requirements_filename }} {{ server_requirements_filename }}
40
+ COPY --chown={{ default_owner }} ./{{ server_requirements_filename }} {{ server_requirements_filename }}
42
41
  RUN {{ sys_pip_install_command }} -r {{ server_requirements_filename }} --no-cache-dir
43
42
  {%- endif %} {#- endif should_install_server_requirements #}
44
43
  {{ super() }}
@@ -47,7 +46,7 @@ RUN {{ sys_pip_install_command }} -r {{ server_requirements_filename }} --no-cac
47
46
 
48
47
  {% block app_copy %}
49
48
  {%- if model_cache_v1 %}
50
- # Copy data before code for better caching
49
+ {# Copy data before code for better caching #}
51
50
  {%- include "copy_cache_files.Dockerfile.jinja" -%}
52
51
  {%- endif %} {#- endif model_cache_v1 #}
53
52
 
@@ -65,47 +64,55 @@ RUN {% for secret,path in config.build.secret_to_path_mapping.items() %} --mount
65
64
 
66
65
  {# Copy data before code for better caching #}
67
66
  {%- if data_dir_exists %}
68
- COPY ./{{ config.data_dir }} /app/data
67
+ COPY --chown={{ default_owner }} ./{{ config.data_dir }} ${APP_HOME}/data
69
68
  {%- endif %} {#- endif data_dir_exists #}
70
69
 
71
70
  {%- if model_cache_v2 %}
72
- # v0.0.9, keep synced with server_requirements.txt
71
+ {# v0.0.9, keep synced with server_requirements.txt #}
73
72
  RUN curl -sSL --fail --retry 5 --retry-delay 2 -o /usr/local/bin/truss-transfer-cli https://github.com/basetenlabs/truss/releases/download/v0.10.11rc1/truss-transfer-cli-v0.10.11rc1-linux-x86_64-unknown-linux-musl
74
73
  RUN chmod +x /usr/local/bin/truss-transfer-cli
75
74
  RUN mkdir /static-bptr
76
75
  RUN echo "hash {{model_cache_hash}}"
77
- COPY ./bptr-manifest /static-bptr/static-bptr-manifest.json
76
+ COPY --chown={{ default_owner }} ./bptr-manifest /static-bptr/static-bptr-manifest.json
78
77
  {%- endif %} {#- endif model_cache_v2 #}
79
78
 
80
79
  {%- if not config.docker_server %}
81
- COPY ./server /app
80
+ COPY --chown={{ default_owner }} ./server ${APP_HOME}
82
81
  {%- endif %} {#- endif not config.docker_server #}
83
82
 
84
83
  {%- if use_local_src %}
85
84
  {# This path takes precedence over site-packages. #}
86
- COPY ./truss_chains /app/truss_chains
87
- COPY ./truss /app/truss
85
+ COPY --chown={{ default_owner }} ./truss_chains ${APP_HOME}/truss_chains
86
+ COPY --chown={{ default_owner }} ./truss ${APP_HOME}/truss
88
87
  {%- endif %} {#- endif use_local_src #}
89
88
 
90
- COPY ./config.yaml /app/config.yaml
89
+ COPY --chown={{ default_owner }} ./config.yaml ${APP_HOME}/config.yaml
91
90
  {%- if requires_live_reload %}
92
91
  RUN uv python install {{ control_python_version }}
93
92
  RUN uv venv /control/.env --python {{ control_python_version }}
94
93
 
95
- COPY ./control /control
94
+ COPY --chown={{ default_owner }} ./control /control
96
95
  RUN uv pip install -r /control/requirements.txt --python /control/.env/bin/python --no-cache-dir
97
96
  {%- endif %} {#- endif requires_live_reload #}
98
97
 
99
98
  {%- if model_dir_exists %}
100
- COPY ./{{ config.model_module_dir }} /app/model
99
+ COPY --chown={{ default_owner }} ./{{ config.model_module_dir }} ${APP_HOME}/model
101
100
  {%- endif %} {#- endif model_dir_exists #}
102
101
  {% endblock %} {#- endblock app_copy #}
103
102
 
104
103
  {% block run %}
104
+ {# Macro to change ownership of directories and switch to regular user #}
105
+ {%- macro chown_and_switch_to_regular_user_if_enabled(additional_chown_dirs=[]) -%}
106
+ {%- if non_root_user %}
107
+ RUN chown -R {{ app_username }}:{{ app_username }} {% for dir in additional_chown_dirs %}{{ dir }} {% endfor %}${HOME} ${APP_HOME}
108
+ USER {{ app_username }}
109
+ {%- endif %} {#- endif non_root_user #}
110
+ {%- endmacro -%}
111
+
105
112
  {%- if config.docker_server %}
106
- RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
113
+ RUN apt-get update -y && apt-get install -y --no-install-recommends \
107
114
  curl nginx && rm -rf /var/lib/apt/lists/*
108
- COPY ./docker_server_requirements.txt /app/docker_server_requirements.txt
115
+ COPY --chown={{ default_owner }} ./docker_server_requirements.txt ${APP_HOME}/docker_server_requirements.txt
109
116
 
110
117
  {# NB(nikhil): Use the same python version for custom server proxy as the control server, for consistency. #}
111
118
  RUN uv python install {{ control_python_version }}
@@ -113,23 +120,30 @@ RUN uv venv /docker_server/.venv --python {{ control_python_version }}
113
120
  RUN uv pip install --python /docker_server/.venv/bin/python -r /app/docker_server_requirements.txt --no-cache-dir
114
121
  {% set proxy_config_path = "/etc/nginx/conf.d/proxy.conf" %}
115
122
  {% set supervisor_config_path = "/etc/supervisor/supervisord.conf" %}
116
- {% set supervisor_log_dir = "/var/log/supervisor" %}
117
123
  {% set supervisor_server_url = "http://localhost:8080" %}
118
- COPY ./proxy.conf {{ proxy_config_path }}
119
- RUN mkdir -p {{ supervisor_log_dir }}
120
- COPY supervisord.conf {{ supervisor_config_path }}
124
+ COPY --chown={{ default_owner }} ./proxy.conf {{ proxy_config_path }}
125
+ COPY --chown={{ default_owner }} ./supervisord.conf {{ supervisor_config_path }}
121
126
  ENV SUPERVISOR_SERVER_URL="{{ supervisor_server_url }}"
122
127
  ENV SERVER_START_CMD="/docker_server/.venv/bin/supervisord -c {{ supervisor_config_path }}"
128
+ {#- default configuration uses port 80, which requires root privileges, so we remove it #}
129
+ RUN rm -f /etc/nginx/sites-enabled/default
130
+ {#- nginx writes to /var/lib/nginx, /var/log/nginx, and /run directories #}
131
+ {{ chown_and_switch_to_regular_user_if_enabled(["/var/lib/nginx", "/var/log/nginx", "/run"]) }}
123
132
  ENTRYPOINT ["/docker_server/.venv/bin/supervisord", "-c", "{{ supervisor_config_path }}"]
133
+
124
134
  {%- elif requires_live_reload %} {#- elif requires_live_reload #}
125
135
  ENV HASH_TRUSS="{{ truss_hash }}"
126
136
  ENV CONTROL_SERVER_PORT="8080"
127
137
  ENV INFERENCE_SERVER_PORT="8090"
128
138
  ENV SERVER_START_CMD="/control/.env/bin/python /control/control/server.py"
139
+ {{ chown_and_switch_to_regular_user_if_enabled() }}
129
140
  ENTRYPOINT ["/control/.env/bin/python", "/control/control/server.py"]
141
+
130
142
  {%- else %} {#- else (default inference server) #}
131
143
  ENV INFERENCE_SERVER_PORT="8080"
132
144
  ENV SERVER_START_CMD="{{ python_executable }} /app/main.py"
145
+ {{ chown_and_switch_to_regular_user_if_enabled() }}
133
146
  ENTRYPOINT ["{{ python_executable }}", "/app/main.py"]
134
147
  {%- endif %} {#- endif config.docker_server / live_reload #}
148
+
135
149
  {% endblock %} {#- endblock run #}
@@ -1,6 +1,11 @@
1
1
  from unittest.mock import Mock, patch
2
2
 
3
- from truss.cli.train.core import view_training_job_metrics
3
+ from truss.cli.train.core import (
4
+ calculate_directory_sizes,
5
+ create_file_summary_with_directory_sizes,
6
+ view_training_job_metrics,
7
+ )
8
+ from truss.remote.baseten.custom_types import FileSummary
4
9
 
5
10
 
6
11
  @patch("truss.cli.train.metrics_watcher.time.sleep")
@@ -189,3 +194,251 @@ def test_view_training_job_metrics(time_sleep, capfd):
189
194
  out, err = capfd.readouterr()
190
195
  assert "Training job completed successfully" in out
191
196
  assert "Error fetching metrics" not in out
197
+
198
+
199
+ def test_calculate_directory_sizes():
200
+ """Test calculate_directory_sizes function with various file structures."""
201
+ # Create test files with a nested directory structure
202
+ files = [
203
+ FileSummary(
204
+ path="/root",
205
+ size_bytes=0,
206
+ modified="2023-01-01T00:00:00Z",
207
+ file_type="directory",
208
+ permissions="drwxr-xr-x",
209
+ ),
210
+ FileSummary(
211
+ path="/root/file1.txt",
212
+ size_bytes=100,
213
+ modified="2023-01-01T00:00:00Z",
214
+ file_type="file",
215
+ permissions="-rw-r--r--",
216
+ ),
217
+ FileSummary(
218
+ path="/root/subdir",
219
+ size_bytes=0,
220
+ modified="2023-01-01T00:00:00Z",
221
+ file_type="directory",
222
+ permissions="drwxr-xr-x",
223
+ ),
224
+ FileSummary(
225
+ path="/root/subdir/file2.txt",
226
+ size_bytes=200,
227
+ modified="2023-01-01T00:00:00Z",
228
+ file_type="file",
229
+ permissions="-rw-r--r--",
230
+ ),
231
+ FileSummary(
232
+ path="/root/subdir/file3.txt",
233
+ size_bytes=300,
234
+ modified="2023-01-01T00:00:00Z",
235
+ file_type="file",
236
+ permissions="-rw-r--r--",
237
+ ),
238
+ FileSummary(
239
+ path="/root/other_file.txt",
240
+ size_bytes=50,
241
+ modified="2023-01-01T00:00:00Z",
242
+ file_type="file",
243
+ permissions="-rw-r--r--",
244
+ ),
245
+ ]
246
+
247
+ result = calculate_directory_sizes(files)
248
+
249
+ # Check that directory sizes are calculated correctly
250
+ assert result["/root/subdir"] == 500 # 200 + 300
251
+ assert result["/root"] == 650 # 100 + 200 + 300 + 50
252
+
253
+ # Check that files are not included in the result (only directories)
254
+ assert "/root/file1.txt" not in result
255
+ assert "/root/subdir/file2.txt" not in result
256
+ assert "/root/subdir/file3.txt" not in result
257
+ assert "/root/other_file.txt" not in result
258
+
259
+
260
+ def test_calculate_directory_sizes_empty_list():
261
+ """Test calculate_directory_sizes with empty file list."""
262
+ result = calculate_directory_sizes([])
263
+ assert result == {}
264
+
265
+
266
+ def test_calculate_directory_sizes_no_directories():
267
+ """Test calculate_directory_sizes with only files (no directories)."""
268
+ files = [
269
+ FileSummary(
270
+ path="/file1.txt",
271
+ size_bytes=100,
272
+ modified="2023-01-01T00:00:00Z",
273
+ file_type="file",
274
+ permissions="-rw-r--r--",
275
+ ),
276
+ FileSummary(
277
+ path="/file2.txt",
278
+ size_bytes=200,
279
+ modified="2023-01-01T00:00:00Z",
280
+ file_type="file",
281
+ permissions="-rw-r--r--",
282
+ ),
283
+ ]
284
+
285
+ result = calculate_directory_sizes(files)
286
+ assert result == {}
287
+
288
+
289
+ def test_create_file_summary_with_directory_sizes():
290
+ """Test create_file_summary_with_directory_sizes function."""
291
+ files = [
292
+ FileSummary(
293
+ path="/root",
294
+ size_bytes=0,
295
+ modified="2023-01-01T00:00:00Z",
296
+ file_type="directory",
297
+ permissions="drwxr-xr-x",
298
+ ),
299
+ FileSummary(
300
+ path="/root/file1.txt",
301
+ size_bytes=100,
302
+ modified="2023-01-01T00:00:00Z",
303
+ file_type="file",
304
+ permissions="-rw-r--r--",
305
+ ),
306
+ FileSummary(
307
+ path="/root/subdir",
308
+ size_bytes=0,
309
+ modified="2023-01-01T00:00:00Z",
310
+ file_type="directory",
311
+ permissions="drwxr-xr-x",
312
+ ),
313
+ FileSummary(
314
+ path="/root/subdir/file2.txt",
315
+ size_bytes=200,
316
+ modified="2023-01-01T00:00:00Z",
317
+ file_type="file",
318
+ permissions="-rw-r--r--",
319
+ ),
320
+ ]
321
+
322
+ result = create_file_summary_with_directory_sizes(files)
323
+
324
+ # Check that we get the correct number of FileSummaryWithTotalSize objects
325
+ assert len(result) == 4
326
+
327
+ # Check that files have their original size as total_size
328
+ file1_summary = next(f for f in result if f.file_summary.path == "/root/file1.txt")
329
+ assert file1_summary.total_size == 100
330
+
331
+ file2_summary = next(
332
+ f for f in result if f.file_summary.path == "/root/subdir/file2.txt"
333
+ )
334
+ assert file2_summary.total_size == 200
335
+
336
+ # Check that directories have calculated total sizes
337
+ subdir_summary = next(f for f in result if f.file_summary.path == "/root/subdir")
338
+ assert subdir_summary.total_size == 200 # Only file2.txt
339
+
340
+ root_summary = next(f for f in result if f.file_summary.path == "/root")
341
+ assert root_summary.total_size == 300 # file1.txt + file2.txt
342
+
343
+
344
+ def test_create_file_summary_with_directory_sizes_empty_list():
345
+ """Test create_file_summary_with_directory_sizes with empty file list."""
346
+ result = create_file_summary_with_directory_sizes([])
347
+ assert result == []
348
+
349
+
350
+ def test_calculate_directory_sizes_max_depth():
351
+ """Test that calculate_directory_sizes respects the max_depth parameter.
352
+
353
+ The max_depth parameter controls how many parent directories up from each file
354
+ the algorithm will traverse to add the file's size to parent directories.
355
+ """
356
+ # Create a deep directory structure: /root/level1/level2/level3/level4/level5/file.txt
357
+ files = [
358
+ # Root directory
359
+ FileSummary(
360
+ path="/root",
361
+ size_bytes=0,
362
+ modified="2023-01-01T00:00:00Z",
363
+ file_type="directory",
364
+ permissions="drwxr-xr-x",
365
+ ),
366
+ # Level 1 directory
367
+ FileSummary(
368
+ path="/root/level1",
369
+ size_bytes=0,
370
+ modified="2023-01-01T00:00:00Z",
371
+ file_type="directory",
372
+ permissions="drwxr-xr-x",
373
+ ),
374
+ # Level 2 directory
375
+ FileSummary(
376
+ path="/root/level1/level2",
377
+ size_bytes=0,
378
+ modified="2023-01-01T00:00:00Z",
379
+ file_type="directory",
380
+ permissions="drwxr-xr-x",
381
+ ),
382
+ # Level 3 directory
383
+ FileSummary(
384
+ path="/root/level1/level2/level3",
385
+ size_bytes=0,
386
+ modified="2023-01-01T00:00:00Z",
387
+ file_type="directory",
388
+ permissions="drwxr-xr-x",
389
+ ),
390
+ # Level 4 directory
391
+ FileSummary(
392
+ path="/root/level1/level2/level3/level4",
393
+ size_bytes=0,
394
+ modified="2023-01-01T00:00:00Z",
395
+ file_type="directory",
396
+ permissions="drwxr-xr-x",
397
+ ),
398
+ # Level 5 directory
399
+ FileSummary(
400
+ path="/root/level1/level2/level3/level4/level5",
401
+ size_bytes=0,
402
+ modified="2023-01-01T00:00:00Z",
403
+ file_type="directory",
404
+ permissions="drwxr-xr-x",
405
+ ),
406
+ # File at level 1
407
+ FileSummary(
408
+ path="/root/level1/file1.txt",
409
+ size_bytes=100,
410
+ modified="2023-01-01T00:00:00Z",
411
+ file_type="file",
412
+ permissions="-rw-r--r--",
413
+ ),
414
+ # File at level 2
415
+ FileSummary(
416
+ path="/root/level1/level2/file2.txt",
417
+ size_bytes=200,
418
+ modified="2023-01-01T00:00:00Z",
419
+ file_type="file",
420
+ permissions="-rw-r--r--",
421
+ ),
422
+ # File at level 3
423
+ FileSummary(
424
+ path="/root/level1/level2/level3/file3.txt",
425
+ size_bytes=300,
426
+ modified="2023-01-01T00:00:00Z",
427
+ file_type="file",
428
+ permissions="-rw-r--r--",
429
+ ),
430
+ ]
431
+
432
+ result_depth_0 = calculate_directory_sizes(files, max_depth=0)
433
+ assert result_depth_0["/root"] == 0
434
+ assert result_depth_0["/root/level1"] == 0
435
+ assert result_depth_0["/root/level1/level2"] == 0
436
+ assert result_depth_0["/root/level1/level2/level3"] == 0
437
+
438
+ # ensure that we stop early if the max depth is reached
439
+ result_depth_2 = calculate_directory_sizes(files, max_depth=2)
440
+
441
+ assert result_depth_2["/root"] == 0
442
+ assert result_depth_2["/root/level1"] == 100 # file1.txt only
443
+ assert result_depth_2["/root/level1/level2"] == 200 # file2.txt only
444
+ assert result_depth_2["/root/level1/level2/level3"] == 300 # file3.txt only
@@ -466,7 +466,7 @@ def test_model_cache_dockerfile_v2(test_data_path):
466
466
  print(gen_docker_file)
467
467
  assert "truss-transfer" in gen_docker_file
468
468
  assert (
469
- "COPY ./bptr-manifest /static-bptr/static-bptr-manifest.json"
469
+ "COPY --chown= ./bptr-manifest /static-bptr/static-bptr-manifest.json"
470
470
  in gen_docker_file
471
471
  ), "bptr-manifest copy not found in Dockerfile"
472
472
  assert "cache_warmer.py" not in gen_docker_file
@@ -1,4 +1,5 @@
1
- from unittest.mock import AsyncMock, MagicMock, patch
1
+ import asyncio
2
+ from unittest.mock import AsyncMock, MagicMock, call, patch
2
3
 
3
4
  import pytest
4
5
  from fastapi import FastAPI, WebSocket
@@ -31,33 +32,40 @@ def client_ws(app):
31
32
 
32
33
  @pytest.mark.asyncio
33
34
  async def test_proxy_ws_bidirectional_messaging(client_ws):
34
- """Test that both directions of communication work and clean up properly"""
35
- client_ws.receive.side_effect = [
36
- {"type": "websocket.receive", "text": "msg1"},
37
- {"type": "websocket.receive", "text": "msg2"},
38
- {"type": "websocket.disconnect"},
39
- ]
35
+ client_queue = asyncio.Queue()
36
+ client_ws.receive = client_queue.get
40
37
 
38
+ server_queue = asyncio.Queue()
41
39
  mock_server_ws = AsyncMock(spec=AsyncWebSocketSession)
42
- mock_server_ws.receive.side_effect = [
43
- TextMessage(data="response1"),
44
- TextMessage(data="response2"),
45
- None, # server closing connection
46
- ]
40
+ mock_server_ws.receive = server_queue.get
47
41
  mock_server_ws.__aenter__.return_value = mock_server_ws
48
42
  mock_server_ws.__aexit__.return_value = None
49
43
 
44
+ client_queue.put_nowait({"type": "websocket.receive", "text": "msg1"})
45
+ client_queue.put_nowait({"type": "websocket.receive", "text": "msg2"})
46
+ server_queue.put_nowait(TextMessage(data="response1"))
47
+ server_queue.put_nowait(TextMessage(data="response2"))
48
+
50
49
  with patch(
51
50
  "truss.templates.control.control.endpoints.aconnect_ws",
52
51
  return_value=mock_server_ws,
53
52
  ):
54
- await proxy_ws(client_ws)
53
+ proxy_task = asyncio.create_task(proxy_ws(client_ws))
54
+ await asyncio.sleep(0.5)
55
+
56
+ client_queue.put_nowait(
57
+ {"type": "websocket.disconnect", "code": 1002, "reason": "test-closure"}
58
+ )
59
+
60
+ await proxy_task
55
61
 
56
62
  assert mock_server_ws.send_text.call_count == 2
57
63
  assert mock_server_ws.send_text.call_args_list == [(("msg1",),), (("msg2",),)]
58
64
  assert client_ws.send_text.call_count == 2
59
65
  assert client_ws.send_text.call_args_list == [(("response1",),), (("response2",),)]
60
- client_ws.close.assert_called_once()
66
+
67
+ assert mock_server_ws.close.call_args_list[0] == call(1002, "test-closure")
68
+ client_ws.close.assert_called()
61
69
 
62
70
 
63
71
  @pytest.mark.asyncio
@@ -21,6 +21,54 @@ from prometheus_client.parser import text_string_to_metric_families
21
21
  PATCH_PING_MAX_DELAY_SECS = 3
22
22
 
23
23
 
24
+ def _start_truss_server(
25
+ stdout_capture_file_path: str,
26
+ truss_control_container_fs: Path,
27
+ with_patch_ping_flow: bool,
28
+ patch_ping_server_port: int,
29
+ ctrl_port: int,
30
+ inf_port: int,
31
+ ):
32
+ """Module-level function to avoid pickling issues with multiprocessing."""
33
+ if with_patch_ping_flow:
34
+ os.environ["PATCH_PING_URL_TRUSS"] = (
35
+ f"http://localhost:{patch_ping_server_port}"
36
+ )
37
+ sys.stdout = open(stdout_capture_file_path, "w")
38
+ app_path = truss_control_container_fs / "app"
39
+ sys.path.append(str(app_path))
40
+ control_path = truss_control_container_fs / "control" / "control"
41
+ sys.path.append(str(control_path))
42
+
43
+ from server import ControlServer
44
+
45
+ control_server = ControlServer(
46
+ python_executable_path=sys.executable,
47
+ inf_serv_home=str(app_path),
48
+ control_server_port=ctrl_port,
49
+ inference_server_port=inf_port,
50
+ )
51
+ control_server.run()
52
+
53
+
54
+ def _start_patch_ping_server(patch_ping_server_port: int):
55
+ """Module-level function to avoid pickling issues with multiprocessing."""
56
+ import json
57
+ import random
58
+ import time
59
+ from http.server import BaseHTTPRequestHandler, HTTPServer
60
+
61
+ class Handler(BaseHTTPRequestHandler):
62
+ def do_POST(self):
63
+ time.sleep(random.uniform(0, PATCH_PING_MAX_DELAY_SECS))
64
+ self.send_response(200)
65
+ self.end_headers()
66
+ self.wfile.write(bytes(json.dumps({"is_current": True}), encoding="utf-8"))
67
+
68
+ httpd = HTTPServer(("localhost", patch_ping_server_port), Handler)
69
+ httpd.serve_forever()
70
+
71
+
24
72
  @dataclass
25
73
  class ControlServerDetails:
26
74
  control_server_process: Process
@@ -270,51 +318,24 @@ def _configured_control_server(
270
318
  inf_port = ctrl_port + 1
271
319
  patch_ping_server_port = ctrl_port + 2
272
320
 
273
- def start_truss_server(stdout_capture_file_path):
274
- if with_patch_ping_flow:
275
- os.environ["PATCH_PING_URL_TRUSS"] = (
276
- f"http://localhost:{patch_ping_server_port}"
277
- )
278
- sys.stdout = open(stdout_capture_file_path, "w")
279
- app_path = truss_control_container_fs / "app"
280
- sys.path.append(str(app_path))
281
- control_path = truss_control_container_fs / "control" / "control"
282
- sys.path.append(str(control_path))
283
-
284
- from server import ControlServer
285
-
286
- control_server = ControlServer(
287
- python_executable_path=sys.executable,
288
- inf_serv_home=str(app_path),
289
- control_server_port=ctrl_port,
290
- inference_server_port=inf_port,
291
- )
292
- control_server.run()
293
-
294
- def start_patch_ping_server():
295
- import json
296
- import random
297
- import time
298
- from http.server import BaseHTTPRequestHandler, HTTPServer
299
-
300
- class Handler(BaseHTTPRequestHandler):
301
- def do_POST(self):
302
- time.sleep(random.uniform(0, PATCH_PING_MAX_DELAY_SECS))
303
- self.send_response(200)
304
- self.end_headers()
305
- self.wfile.write(
306
- bytes(json.dumps({"is_current": True}), encoding="utf-8")
307
- )
308
-
309
- httpd = HTTPServer(("localhost", patch_ping_server_port), Handler)
310
- httpd.serve_forever()
311
-
312
321
  stdout_capture_file = tempfile.NamedTemporaryFile()
313
- subproc = Process(target=start_truss_server, args=(stdout_capture_file.name,))
322
+ subproc = Process(
323
+ target=_start_truss_server,
324
+ args=(
325
+ stdout_capture_file.name,
326
+ truss_control_container_fs,
327
+ with_patch_ping_flow,
328
+ patch_ping_server_port,
329
+ ctrl_port,
330
+ inf_port,
331
+ ),
332
+ )
314
333
  subproc.start()
315
334
  proc_id = subproc.pid
316
335
  if with_patch_ping_flow:
317
- patch_ping_server_proc = Process(target=start_patch_ping_server)
336
+ patch_ping_server_proc = Process(
337
+ target=_start_patch_ping_server, args=(patch_ping_server_port,)
338
+ )
318
339
  patch_ping_server_proc.start()
319
340
  try:
320
341
  time.sleep(2.0)
@@ -10,23 +10,30 @@ from pathlib import Path
10
10
  import pytest
11
11
 
12
12
 
13
- @pytest.mark.integration
14
- def test_truss_server_termination(truss_container_fs):
15
- port = 10123
13
+ def _start_truss_server(
14
+ stdout_capture_file_path: str, truss_container_fs: Path, port: int
15
+ ):
16
+ """Module-level function to avoid pickling issues with multiprocessing."""
17
+ sys.stdout = open(stdout_capture_file_path, "w")
18
+ app_path = truss_container_fs / "app"
19
+ sys.path.append(str(app_path))
20
+ os.chdir(app_path)
21
+
22
+ from truss_server import TrussServer
16
23
 
17
- def start_truss_server(stdout_capture_file_path):
18
- sys.stdout = open(stdout_capture_file_path, "w")
19
- app_path = truss_container_fs / "app"
20
- sys.path.append(str(app_path))
21
- os.chdir(app_path)
24
+ server = TrussServer(http_port=port, config_or_path=app_path / "config.yaml")
25
+ server.start()
22
26
 
23
- from truss_server import TrussServer
24
27
 
25
- server = TrussServer(http_port=port, config_or_path=app_path / "config.yaml")
26
- server.start()
28
+ @pytest.mark.integration
29
+ def test_truss_server_termination(truss_container_fs):
30
+ port = 10123
27
31
 
28
32
  stdout_capture_file = tempfile.NamedTemporaryFile()
29
- subproc = Process(target=start_truss_server, args=(stdout_capture_file.name,))
33
+ subproc = Process(
34
+ target=_start_truss_server,
35
+ args=(stdout_capture_file.name, truss_container_fs, port),
36
+ )
30
37
  subproc.start()
31
38
  proc_id = subproc.pid
32
39
  time.sleep(2.0)
@@ -1,6 +1,11 @@
1
1
  ARG PYVERSION=py39
2
2
  FROM baseten/truss-server-base:3.9-v0.4.3 AS truss_server
3
3
  ENV PYTHON_EXECUTABLE="/usr/local/bin/python3"
4
+ ENV HOME=/home/app
5
+ ENV APP_HOME=/app
6
+ RUN mkdir -p ${APP_HOME} /control
7
+ RUN useradd -u 60000 -ms /bin/bash app
8
+ ENV DEBIAN_FRONTEND=noninteractive
4
9
  RUN grep -w 'ID=debian\|ID_LIKE=debian' /etc/os-release || { echo "ERROR: Supplied base image is not a debian image"; exit 1; }
5
10
  RUN /usr/local/bin/python3 -c "import sys; \
6
11
  sys.exit(0) \
@@ -13,25 +18,23 @@ RUN if ! command -v uv >/dev/null 2>&1; then \
13
18
  command -v curl >/dev/null 2>&1 || (apt update && apt install -y curl) && \
14
19
  curl -LsSf --retry 5 --retry-delay 5 https://astral.sh/uv/0.7.19/install.sh | sh; \
15
20
  fi
16
- ENV PATH="/root/.local/bin:$PATH"
21
+ ENV PATH=${PATH}:${HOME}/.local/bin
17
22
  ENV PYTHONUNBUFFERED="True"
18
- ENV DEBIAN_FRONTEND="noninteractive"
19
23
  RUN apt update && \
20
24
  apt install -y bash build-essential git curl ca-certificates \
21
25
  && apt-get autoremove -y \
22
26
  && apt-get clean -y \
23
27
  && rm -rf /var/lib/apt/lists/*
24
- COPY ./base_server_requirements.txt base_server_requirements.txt
28
+ COPY --chown= ./base_server_requirements.txt base_server_requirements.txt
25
29
  RUN UV_HTTP_TIMEOUT=${UV_HTTP_TIMEOUT:-300} uv pip install --index-strategy unsafe-best-match --python /usr/local/bin/python3 -r base_server_requirements.txt --no-cache-dir
26
- COPY ./requirements.txt requirements.txt
30
+ COPY --chown= ./requirements.txt requirements.txt
27
31
  RUN UV_HTTP_TIMEOUT=${UV_HTTP_TIMEOUT:-300} uv pip install --index-strategy unsafe-best-match --python /usr/local/bin/python3 -r requirements.txt --no-cache-dir
28
- ENV APP_HOME="/app"
29
32
  WORKDIR $APP_HOME
30
- COPY ./data /app/data
31
- COPY ./server /app
32
- COPY ./config.yaml /app/config.yaml
33
- COPY ./model /app/model
34
- COPY ./packages /packages
33
+ COPY --chown= ./data ${APP_HOME}/data
34
+ COPY --chown= ./server ${APP_HOME}
35
+ COPY --chown= ./config.yaml ${APP_HOME}/config.yaml
36
+ COPY --chown= ./model ${APP_HOME}/model
37
+ COPY --chown= ./packages /packages
35
38
  ENV INFERENCE_SERVER_PORT="8080"
36
39
  ENV SERVER_START_CMD="/usr/local/bin/python3 /app/main.py"
37
40
  ENTRYPOINT ["/usr/local/bin/python3", "/app/main.py"]