isolate 0.14.0__tar.gz → 0.14.1__tar.gz

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 isolate might be problematic. Click here for more details.

Files changed (86) hide show
  1. {isolate-0.14.0 → isolate-0.14.1}/PKG-INFO +1 -1
  2. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/_isolate_version.py +2 -2
  3. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/server.py +67 -21
  4. {isolate-0.14.0 → isolate-0.14.1}/src/isolate.egg-info/PKG-INFO +1 -1
  5. {isolate-0.14.0 → isolate-0.14.1}/tests/test_server.py +107 -8
  6. {isolate-0.14.0 → isolate-0.14.1}/.github/workflows/release.yml +0 -0
  7. {isolate-0.14.0 → isolate-0.14.1}/.github/workflows/test.yml +0 -0
  8. {isolate-0.14.0 → isolate-0.14.1}/.gitignore +0 -0
  9. {isolate-0.14.0 → isolate-0.14.1}/.pre-commit-config.yaml +0 -0
  10. {isolate-0.14.0 → isolate-0.14.1}/LICENSE +0 -0
  11. {isolate-0.14.0 → isolate-0.14.1}/README.md +0 -0
  12. {isolate-0.14.0 → isolate-0.14.1}/pyproject.toml +0 -0
  13. {isolate-0.14.0 → isolate-0.14.1}/setup.cfg +0 -0
  14. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/__init__.py +0 -0
  15. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/_version.py +0 -0
  16. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/backends/__init__.py +0 -0
  17. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/backends/_base.py +0 -0
  18. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/backends/common.py +0 -0
  19. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/backends/conda.py +0 -0
  20. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/backends/container.py +0 -0
  21. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/backends/local.py +0 -0
  22. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/backends/pyenv.py +0 -0
  23. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/backends/remote.py +0 -0
  24. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/backends/settings.py +0 -0
  25. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/backends/virtualenv.py +0 -0
  26. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/common/__init__.py +0 -0
  27. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/common/timestamp.py +0 -0
  28. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/__init__.py +0 -0
  29. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/_local/__init__.py +0 -0
  30. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/_local/_base.py +0 -0
  31. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/_local/agent_startup.py +0 -0
  32. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/common.py +0 -0
  33. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/__init__.py +0 -0
  34. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/_base.py +0 -0
  35. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/agent.py +0 -0
  36. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/configuration.py +0 -0
  37. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/definitions/__init__.py +0 -0
  38. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/definitions/agent.proto +0 -0
  39. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/definitions/agent_pb2.py +0 -0
  40. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/definitions/agent_pb2.pyi +0 -0
  41. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/definitions/agent_pb2_grpc.py +0 -0
  42. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/definitions/common.proto +0 -0
  43. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/definitions/common_pb2.py +0 -0
  44. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/definitions/common_pb2.pyi +0 -0
  45. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/definitions/common_pb2_grpc.py +0 -0
  46. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/grpc/interface.py +0 -0
  47. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/ipc/__init__.py +0 -0
  48. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/ipc/_base.py +0 -0
  49. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/connections/ipc/agent.py +0 -0
  50. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/logger.py +0 -0
  51. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/logs.py +0 -0
  52. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/py.typed +0 -0
  53. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/registry.py +0 -0
  54. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/__init__.py +0 -0
  55. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/definitions/__init__.py +0 -0
  56. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/definitions/server.proto +0 -0
  57. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/definitions/server_pb2.py +0 -0
  58. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/definitions/server_pb2.pyi +0 -0
  59. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/definitions/server_pb2_grpc.py +0 -0
  60. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/health/__init__.py +0 -0
  61. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/health/health.proto +0 -0
  62. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/health/health_pb2.py +0 -0
  63. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/health/health_pb2.pyi +0 -0
  64. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/health/health_pb2_grpc.py +0 -0
  65. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/health_server.py +0 -0
  66. {isolate-0.14.0 → isolate-0.14.1}/src/isolate/server/interface.py +0 -0
  67. {isolate-0.14.0 → isolate-0.14.1}/src/isolate.egg-info/SOURCES.txt +0 -0
  68. {isolate-0.14.0 → isolate-0.14.1}/src/isolate.egg-info/dependency_links.txt +0 -0
  69. {isolate-0.14.0 → isolate-0.14.1}/src/isolate.egg-info/entry_points.txt +0 -0
  70. {isolate-0.14.0 → isolate-0.14.1}/src/isolate.egg-info/requires.txt +0 -0
  71. {isolate-0.14.0 → isolate-0.14.1}/src/isolate.egg-info/top_level.txt +0 -0
  72. {isolate-0.14.0 → isolate-0.14.1}/tests/__init__.py +0 -0
  73. {isolate-0.14.0 → isolate-0.14.1}/tests/conftest.py +0 -0
  74. {isolate-0.14.0 → isolate-0.14.1}/tests/test_backends.py +0 -0
  75. {isolate-0.14.0 → isolate-0.14.1}/tests/test_concurrency.py +0 -0
  76. {isolate-0.14.0 → isolate-0.14.1}/tests/test_connections.py +0 -0
  77. {isolate-0.14.0 → isolate-0.14.1}/tests/test_isolate.py +0 -0
  78. {isolate-0.14.0 → isolate-0.14.1}/tests/test_log.py +0 -0
  79. {isolate-0.14.0 → isolate-0.14.1}/tests/test_logger.py +0 -0
  80. {isolate-0.14.0 → isolate-0.14.1}/tests/test_serialization.py +0 -0
  81. {isolate-0.14.0 → isolate-0.14.1}/tools/Dockerfile +0 -0
  82. {isolate-0.14.0 → isolate-0.14.1}/tools/agent_requirements.txt +0 -0
  83. {isolate-0.14.0 → isolate-0.14.1}/tools/protobuf-requirements.txt +0 -0
  84. {isolate-0.14.0 → isolate-0.14.1}/tools/regen_grpc.py +0 -0
  85. {isolate-0.14.0 → isolate-0.14.1}/tools/requirements.txt +0 -0
  86. {isolate-0.14.0 → isolate-0.14.1}/tools/test_agent_requirements.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: isolate
3
- Version: 0.14.0
3
+ Version: 0.14.1
4
4
  Summary: Managed isolated environments for Python
5
5
  Author-email: Features & Labels <hello@fal.ai>
6
6
  Project-URL: Issues, https://github.com/fal-ai/isolate/issues
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.14.0'
16
- __version_tuple__ = version_tuple = (0, 14, 0)
15
+ __version__ = version = '0.14.1'
16
+ __version_tuple__ = version_tuple = (0, 14, 1)
@@ -36,6 +36,7 @@ from isolate.server.health_server import HealthServicer
36
36
  from isolate.server.interface import from_grpc, to_grpc
37
37
 
38
38
  EMPTY_MESSAGE_INTERVAL = float(os.getenv("ISOLATE_EMPTY_MESSAGE_INTERVAL", "600"))
39
+ SKIP_EMPTY_LOGS = os.getenv("ISOLATE_SKIP_EMPTY_LOGS") == "1"
39
40
  MAX_GRPC_WAIT_TIMEOUT = float(os.getenv("ISOLATE_MAX_GRPC_WAIT_TIMEOUT", "10.0"))
40
41
 
41
42
  # Whether to inherit all the packages from the current environment or not.
@@ -464,7 +465,8 @@ class IsolateServicer(definitions.IsolateServicer):
464
465
  return None
465
466
 
466
467
  def cancel_tasks(self):
467
- for task in self.background_tasks.values():
468
+ tasks_copy = self.background_tasks.copy()
469
+ for task in tasks_copy.values():
468
470
  task.cancel()
469
471
 
470
472
 
@@ -484,8 +486,9 @@ class LogHandler:
484
486
  task: RunTask
485
487
 
486
488
  def handle(self, log: Log) -> None:
487
- self.task.logger.log(log.level, log.message, source=log.source)
488
- self._add_log_to_queue(log)
489
+ if not SKIP_EMPTY_LOGS or log.message.strip():
490
+ self.task.logger.log(log.level, log.message, source=log.source)
491
+ self._add_log_to_queue(log)
489
492
 
490
493
  def _add_log_to_queue(self, log: Log) -> None:
491
494
  grpc_log = cast(definitions.Log, to_grpc(log))
@@ -534,6 +537,16 @@ class SingleTaskInterceptor(ServerBoundInterceptor):
534
537
  """Sets server to terminate after the first Submit/Run task."""
535
538
 
536
539
  _done: bool = False
540
+ _task_id: str | None = None
541
+
542
+ def __init__(self):
543
+ def terminate(request: Any, context: grpc.ServicerContext) -> Any:
544
+ context.abort(
545
+ grpc.StatusCode.RESOURCE_EXHAUSTED,
546
+ "Server has already served one Run/Submit task.",
547
+ )
548
+
549
+ self._terminator = grpc.unary_unary_rpc_method_handler(terminate)
537
550
 
538
551
  def intercept_service(self, continuation, handler_call_details):
539
552
  handler = continuation(handler_call_details)
@@ -542,29 +555,62 @@ class SingleTaskInterceptor(ServerBoundInterceptor):
542
555
  is_run = handler_call_details.method == "/Isolate/Run"
543
556
  is_new_task = is_submit or is_run
544
557
 
545
- if is_new_task and self._done:
546
- raise grpc.RpcError(
547
- grpc.StatusCode.UNAVAILABLE,
548
- "Server has already served one Run/Submit task.",
549
- )
550
- elif is_new_task:
551
- self._done = True
552
- else:
558
+ if not is_new_task:
553
559
  # Let other requests like List/Cancel/etc pass through
554
- return continuation(handler_call_details)
560
+ return handler
561
+
562
+ if self._done:
563
+ # Fail the request if the server has already served or is serving
564
+ # a Run/Submit task.
565
+ return self._terminator
566
+
567
+ self._done = True
555
568
 
556
569
  def wrapper(method_impl):
557
570
  @functools.wraps(method_impl)
558
- def _wrapper(request, context):
559
- def _stop():
560
- if is_submit:
561
- # Wait for the task to finish
562
- while self.server.servicer.background_tasks:
571
+ def _wrapper(request: Any, context: grpc.ServicerContext) -> Any:
572
+ def termination() -> None:
573
+ if is_run:
574
+ print("Stopping server since run is finished")
575
+ # Stop the server after the Run task is finished
576
+ self.server.stop(grace=0.1)
577
+
578
+ elif is_submit:
579
+ # Wait until the task_id is assigned
580
+ while self._task_id is None:
563
581
  time.sleep(0.1)
564
- self.server.stop(grace=0.1)
565
582
 
566
- context.add_callback(_stop)
567
- return method_impl(request, context)
583
+ # Get the task from the background tasks
584
+ task = self.servicer.background_tasks.get(self._task_id)
585
+
586
+ if task is not None:
587
+ # Wait until the task future is assigned
588
+ tries = 0
589
+ while task.future is None:
590
+ time.sleep(0.1)
591
+ tries += 1
592
+ if tries > 100:
593
+ raise RuntimeError(
594
+ "Task future was not assigned in time."
595
+ )
596
+
597
+ def _stop(*args):
598
+ # Small sleep to make sure the cancellation is processed
599
+ time.sleep(0.1)
600
+ print("Stopping server since the task is finished")
601
+ self.server.stop(grace=0.1)
602
+
603
+ # Add a callback which will stop the server
604
+ # after the task is finished
605
+ task.future.add_done_callback(_stop)
606
+
607
+ context.add_callback(termination)
608
+ res = method_impl(request, context)
609
+
610
+ if is_submit:
611
+ self._task_id = cast(definitions.SubmitResponse, res).task_id
612
+
613
+ return res
568
614
 
569
615
  return _wrapper
570
616
 
@@ -598,7 +644,7 @@ def main(argv: list[str] | None = None) -> None:
598
644
  server = grpc.server(
599
645
  futures.ThreadPoolExecutor(max_workers=options.num_workers),
600
646
  options=get_default_options(),
601
- interceptors=interceptors,
647
+ interceptors=interceptors, # type: ignore
602
648
  )
603
649
 
604
650
  for interceptor in interceptors:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: isolate
3
- Version: 0.14.0
3
+ Version: 0.14.1
4
4
  Summary: Managed isolated environments for Python
5
5
  Author-email: Features & Labels <hello@fal.ai>
6
6
  Project-URL: Issues, https://github.com/fal-ai/isolate/issues
@@ -5,7 +5,7 @@ from contextlib import contextmanager
5
5
  from dataclasses import dataclass
6
6
  from functools import partial
7
7
  from pathlib import Path
8
- from typing import Any, List, Optional, cast
8
+ from typing import Any, Iterator, List, Optional, cast
9
9
 
10
10
  import grpc
11
11
  import pytest
@@ -15,7 +15,12 @@ from isolate.logs import Log, LogLevel, LogSource
15
15
  from isolate.server import definitions, health
16
16
  from isolate.server.health_server import HealthServicer
17
17
  from isolate.server.interface import from_grpc, to_serialized_object
18
- from isolate.server.server import BridgeManager, IsolateServicer
18
+ from isolate.server.server import (
19
+ BridgeManager,
20
+ IsolateServicer,
21
+ ServerBoundInterceptor,
22
+ SingleTaskInterceptor,
23
+ )
19
24
 
20
25
  REPO_DIR = Path(__file__).parent.parent
21
26
  assert (
@@ -34,14 +39,32 @@ class Stubs:
34
39
  health_stub: health.HealthStub
35
40
 
36
41
 
42
+ @pytest.fixture
43
+ def interceptors():
44
+ return []
45
+
46
+
37
47
  @contextmanager
38
- def make_server(tmp_path):
48
+ def make_server(
49
+ tmp_path: Path, interceptors: Optional[List[ServerBoundInterceptor]] = None
50
+ ) -> Iterator[Stubs]:
51
+ interceptors = interceptors or []
39
52
  server = grpc.server(
40
- futures.ThreadPoolExecutor(max_workers=1), options=get_default_options()
53
+ futures.ThreadPoolExecutor(max_workers=1),
54
+ options=get_default_options(),
55
+ interceptors=interceptors, # type: ignore
41
56
  )
57
+
58
+ for interceptor in interceptors:
59
+ interceptor.register_server(server)
60
+
42
61
  test_settings = IsolateSettings(cache_dir=tmp_path / "cache")
43
62
  with BridgeManager() as bridge:
44
63
  servicer = IsolateServicer(bridge, test_settings)
64
+
65
+ for interceptor in interceptors:
66
+ interceptor.register_servicer(servicer)
67
+
45
68
  definitions.register_isolate(servicer, server)
46
69
  health.register_health(HealthServicer(), server)
47
70
  host, port = "localhost", server.add_insecure_port("[::]:0")
@@ -69,14 +92,14 @@ def make_server(tmp_path):
69
92
 
70
93
 
71
94
  @pytest.fixture
72
- def stub(tmp_path):
73
- with make_server(tmp_path) as stubs:
95
+ def stub(tmp_path, interceptors):
96
+ with make_server(tmp_path, interceptors) as stubs:
74
97
  yield stubs.isolate_stub
75
98
 
76
99
 
77
100
  @pytest.fixture
78
- def health_stub(tmp_path):
79
- with make_server(tmp_path) as stubs:
101
+ def health_stub(tmp_path, interceptors):
102
+ with make_server(tmp_path, interceptors) as stubs:
80
103
  yield stubs.health_stub
81
104
 
82
105
 
@@ -719,3 +742,79 @@ def test_server_submit_server(
719
742
  stub.Cancel(definitions.CancelRequest(task_id=task_id))
720
743
 
721
744
  assert not list(stub.List(definitions.ListRequest()).tasks)
745
+
746
+
747
+ @pytest.mark.parametrize(
748
+ "interceptors",
749
+ [
750
+ [SingleTaskInterceptor()],
751
+ ],
752
+ )
753
+ def test_server_single_use_submit(
754
+ stub: definitions.IsolateStub,
755
+ monkeypatch: Any,
756
+ ) -> None:
757
+ import time
758
+
759
+ inherit_from_local(monkeypatch)
760
+
761
+ request = definitions.SubmitRequest(function=prepare_request(myserver))
762
+ task_id = stub.Submit(request).task_id
763
+
764
+ tasks = [task.task_id for task in stub.List(definitions.ListRequest()).tasks]
765
+ assert task_id in tasks
766
+
767
+ # Now try to Submit again
768
+ with pytest.raises(grpc.RpcError) as exc_info:
769
+ stub.Submit(request)
770
+ assert exc_info.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED
771
+
772
+ # And try to Run a task
773
+ with pytest.raises(grpc.RpcError) as exc_info:
774
+ run_request(stub, prepare_request(myserver))
775
+ assert exc_info.value.code() == grpc.StatusCode.RESOURCE_EXHAUSTED
776
+
777
+ stub.Cancel(definitions.CancelRequest(task_id=task_id))
778
+ time.sleep(1)
779
+
780
+ with pytest.raises(grpc.RpcError) as exc_info:
781
+ stub.List(definitions.ListRequest())
782
+
783
+ # Server should be shutting down
784
+ assert exc_info.value.code() == grpc.StatusCode.UNAVAILABLE
785
+
786
+
787
+ @pytest.mark.parametrize(
788
+ "interceptors",
789
+ [
790
+ [SingleTaskInterceptor()],
791
+ ],
792
+ )
793
+ def test_server_single_use_run(
794
+ stub: definitions.IsolateStub,
795
+ monkeypatch: Any,
796
+ ) -> None:
797
+ import time
798
+
799
+ inherit_from_local(monkeypatch)
800
+
801
+ run_function(stub, check_machine)
802
+ time.sleep(1)
803
+
804
+ # Now try to Submit again
805
+ with pytest.raises(grpc.RpcError) as exc_info:
806
+ submit_request = definitions.SubmitRequest(function=prepare_request(myserver))
807
+ stub.Submit(submit_request)
808
+
809
+ assert exc_info.value.code() == grpc.StatusCode.UNAVAILABLE
810
+
811
+ # And try to Run a task
812
+ with pytest.raises(grpc.RpcError) as exc_info:
813
+ run_function(stub, check_machine)
814
+
815
+ assert exc_info.value.code() == grpc.StatusCode.UNAVAILABLE
816
+
817
+ with pytest.raises(grpc.RpcError) as exc_info:
818
+ stub.List(definitions.ListRequest())
819
+
820
+ assert exc_info.value.code() == grpc.StatusCode.UNAVAILABLE
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes