chalk-remote-call-python 1.10.5__cp315-cp315-win_amd64.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.
Files changed (45) hide show
  1. chalk_remote_call/__init__.py +4 -0
  2. chalk_remote_call/__main__.py +3 -0
  3. chalk_remote_call/_gen/__init__.py +0 -0
  4. chalk_remote_call/_gen/chalk/__init__.py +0 -0
  5. chalk_remote_call/_gen/chalk/auth/__init__.py +0 -0
  6. chalk_remote_call/_gen/chalk/auth/v1/__init__.py +0 -0
  7. chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2.py +115 -0
  8. chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2_grpc.py +4 -0
  9. chalk_remote_call/_gen/chalk/common/__init__.py +0 -0
  10. chalk_remote_call/_gen/chalk/common/v1/__init__.py +0 -0
  11. chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2.py +33 -0
  12. chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2_grpc.py +4 -0
  13. chalk_remote_call/_gen/chalk/runtime/__init__.py +0 -0
  14. chalk_remote_call/_gen/chalk/runtime/v1/__init__.py +0 -0
  15. chalk_remote_call/_gen/chalk/runtime/v1/fnq_consumer_pb2.py +53 -0
  16. chalk_remote_call/_gen/chalk/runtime/v1/fnq_consumer_pb2_grpc.py +138 -0
  17. chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2.py +104 -0
  18. chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2_grpc.py +363 -0
  19. chalk_remote_call/_gen/chalk/utils/__init__.py +0 -0
  20. chalk_remote_call/_gen/chalk/utils/v1/__init__.py +0 -0
  21. chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2.py +32 -0
  22. chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2_grpc.py +4 -0
  23. chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2.py +29 -0
  24. chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2_grpc.py +4 -0
  25. chalk_remote_call/_gen/chalk/utils/v1/large_pb2.py +26 -0
  26. chalk_remote_call/_gen/chalk/utils/v1/large_pb2_grpc.py +4 -0
  27. chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2.py +26 -0
  28. chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2_grpc.py +4 -0
  29. chalk_remote_call/_native.cp315-win_amd64.pyd +0 -0
  30. chalk_remote_call/_native.pyi +13 -0
  31. chalk_remote_call/_version.py +1 -0
  32. chalk_remote_call/arrow_utils.py +21 -0
  33. chalk_remote_call/cli.py +160 -0
  34. chalk_remote_call/errors.py +118 -0
  35. chalk_remote_call/handler_loader.py +57 -0
  36. chalk_remote_call/input_transform.py +50 -0
  37. chalk_remote_call/logging.py +29 -0
  38. chalk_remote_call/server.py +117 -0
  39. chalk_remote_call/servicer.py +400 -0
  40. chalk_remote_call/tracing.py +445 -0
  41. chalk_remote_call_python-1.10.5.dist-info/METADATA +288 -0
  42. chalk_remote_call_python-1.10.5.dist-info/RECORD +45 -0
  43. chalk_remote_call_python-1.10.5.dist-info/WHEEL +5 -0
  44. chalk_remote_call_python-1.10.5.dist-info/entry_points.txt +2 -0
  45. chalk_remote_call_python-1.10.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import os
6
+ import sys
7
+
8
+ from chalk_remote_call.handler_loader import load_function, load_handler
9
+ from chalk_remote_call.logging import JsonFormatter
10
+ from chalk_remote_call.server import serve
11
+
12
+
13
+ def _env_int(name: str) -> int | None:
14
+ """Read an optional int from the environment; return None if unset/empty."""
15
+ raw = os.environ.get(name)
16
+ if raw is None or raw.strip() == "":
17
+ return None
18
+ try:
19
+ return int(raw)
20
+ except ValueError as e:
21
+ raise ValueError(f"{name} must be an integer, got: {raw!r}") from e
22
+
23
+
24
+ def main() -> None:
25
+ log_handler = logging.StreamHandler(sys.stderr)
26
+ log_handler.setFormatter(JsonFormatter())
27
+ logging.basicConfig(level=logging.INFO, handlers=[log_handler], force=True)
28
+ try:
29
+ _run()
30
+ except Exception:
31
+ logging.getLogger(__name__).exception("Remote-call server failed")
32
+ sys.exit(1)
33
+
34
+
35
+ def _run() -> None:
36
+ parser = argparse.ArgumentParser(
37
+ prog="chalk-remote-call",
38
+ description="Start a gRPC server implementing the Chalk RemoteCallService.",
39
+ )
40
+ parser.add_argument(
41
+ "--handler",
42
+ required=True,
43
+ help="Dotted path to the handler function (e.g. my_module.handler)",
44
+ )
45
+ parser.add_argument(
46
+ "--port",
47
+ type=int,
48
+ default=int(os.environ.get("CHALK_REMOTE_CALL_PORT", "6666")),
49
+ help="Port to listen on (default: 6666, env: CHALK_REMOTE_CALL_PORT)",
50
+ )
51
+ parser.add_argument(
52
+ "--host",
53
+ default=os.environ.get("CHALK_REMOTE_CALL_HOST", "[::]"),
54
+ help="Host to bind to (default: [::], env: CHALK_REMOTE_CALL_HOST)",
55
+ )
56
+ parser.add_argument(
57
+ "--workers",
58
+ type=int,
59
+ default=int(os.environ.get("CHALK_REMOTE_CALL_WORKERS", "10")),
60
+ help="Number of worker threads (default: 10, env: CHALK_REMOTE_CALL_WORKERS)",
61
+ )
62
+ parser.add_argument(
63
+ "--on-startup",
64
+ default=None,
65
+ help="Dotted path to a startup function (e.g. my_module.setup)",
66
+ )
67
+ parser.add_argument(
68
+ "--on-shutdown",
69
+ default=None,
70
+ help="Dotted path to a shutdown function (e.g. my_module.cleanup)",
71
+ )
72
+ parser.add_argument(
73
+ "--max-batching-size",
74
+ type=int,
75
+ default=_env_int("CHALK_REMOTE_CALL_MAX_BATCHING_SIZE"),
76
+ help=(
77
+ "Max concurrent requests to coalesce into a single handler "
78
+ "invocation. Unset disables batching. "
79
+ "(env: CHALK_REMOTE_CALL_MAX_BATCHING_SIZE)"
80
+ ),
81
+ )
82
+ parser.add_argument(
83
+ "--max-buffer-duration-ms",
84
+ type=int,
85
+ default=_env_int("CHALK_REMOTE_CALL_MAX_BUFFER_DURATION_MS"),
86
+ help=(
87
+ "Max milliseconds to buffer concurrent requests before flushing, "
88
+ "even if max-batching-size hasn't been reached. Only effective "
89
+ "when --max-batching-size is also set. Defaults to 1000ms when "
90
+ "batching is enabled. "
91
+ "(env: CHALK_REMOTE_CALL_MAX_BUFFER_DURATION_MS)"
92
+ ),
93
+ )
94
+ parser.add_argument(
95
+ "--batching-mode",
96
+ choices=["per_caller", "combined"],
97
+ default=os.environ.get("CHALK_REMOTE_CALL_BATCHING_MODE") or None,
98
+ help=(
99
+ "Coalescing variant when --max-batching-size is set. "
100
+ "'per_caller' (default) passes events as a list of dicts; "
101
+ "'combined' passes a single concatenated RecordBatch with "
102
+ "offsets. (env: CHALK_REMOTE_CALL_BATCHING_MODE)"
103
+ ),
104
+ )
105
+ parser.add_argument(
106
+ "--log-level",
107
+ default="INFO",
108
+ choices=["DEBUG", "INFO", "WARNING", "ERROR"],
109
+ help="Log level (default: INFO)",
110
+ )
111
+ args = parser.parse_args()
112
+
113
+ logging.getLogger().setLevel(args.log_level)
114
+
115
+ from chalk_remote_call.input_transform import parse_input_args
116
+
117
+ # Load handler
118
+ try:
119
+ handler, auto_startup, auto_shutdown = load_handler(args.handler)
120
+ except Exception:
121
+ logging.getLogger(__name__).exception("Error loading handler")
122
+ sys.exit(1)
123
+
124
+ # Determine startup function
125
+ on_startup = None
126
+ if args.on_startup:
127
+ try:
128
+ on_startup = load_function(args.on_startup, label="Startup")
129
+ except Exception:
130
+ logging.getLogger(__name__).exception("Error loading startup function")
131
+ sys.exit(1)
132
+ elif auto_startup is not None:
133
+ on_startup = auto_startup
134
+
135
+ # Determine shutdown function
136
+ on_shutdown = None
137
+ if args.on_shutdown:
138
+ try:
139
+ on_shutdown = load_function(args.on_shutdown, label="Shutdown")
140
+ except Exception:
141
+ logging.getLogger(__name__).exception("Error loading shutdown function")
142
+ sys.exit(1)
143
+ elif auto_shutdown is not None:
144
+ on_shutdown = auto_shutdown
145
+
146
+ # Parse input args
147
+ arg_names = parse_input_args()
148
+
149
+ serve(
150
+ handler=handler,
151
+ host=args.host,
152
+ port=args.port,
153
+ workers=args.workers,
154
+ on_startup=on_startup,
155
+ on_shutdown=on_shutdown,
156
+ arg_names=arg_names,
157
+ max_batching_size=args.max_batching_size,
158
+ max_buffer_duration_ms=args.max_buffer_duration_ms,
159
+ batching_mode=args.batching_mode,
160
+ )
@@ -0,0 +1,118 @@
1
+ """Versioned, data-only remote exception transport shared by all call modes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import json
7
+ import traceback
8
+ from types import TracebackType
9
+
10
+
11
+ def _user_stack(tb: TracebackType | None) -> traceback.StackSummary:
12
+ # Start at the outermost bridge's callee, so Python callers above the
13
+ # dispatch boundary cannot leak into user diagnostics or make async runner
14
+ # frames look like user code. Stop at the first bridge to preserve user
15
+ # frames if a handler itself invokes another bridge.
16
+ dispatch_tb = tb
17
+ while dispatch_tb is not None:
18
+ frame = dispatch_tb.tb_frame
19
+ if frame.f_globals.get("__name__") == "chalk_remote_call.servicer" and frame.f_code.co_name in {
20
+ "process_batches",
21
+ "process_batches_coalesced",
22
+ "process_batches_coalesced_combined",
23
+ }:
24
+ tb = dispatch_tb.tb_next
25
+ break
26
+ dispatch_tb = dispatch_tb.tb_next
27
+
28
+ frames = []
29
+ entered_user_code = False
30
+ while tb is not None:
31
+ frame = tb.tb_frame
32
+ module = str(frame.f_globals.get("__name__", ""))
33
+ runner_frame = not entered_user_code and module in {
34
+ "asyncio.base_events",
35
+ "asyncio.runners",
36
+ "concurrent.futures._base",
37
+ "concurrent.futures.thread",
38
+ }
39
+ if not (
40
+ runner_frame
41
+ or module == "chalk_remote_call.servicer"
42
+ or module == "chalk_remote_call.errors"
43
+ or module.startswith("chalkcompute.")
44
+ or frame.f_globals.get("__chalk_generated_handler__") is True
45
+ ):
46
+ entered_user_code = True
47
+ filename, lineno = frame.f_code.co_filename, tb.tb_lineno
48
+ source_map = frame.f_globals.get("__chalk_source_map__", {})
49
+ mapped = source_map.get(lineno) if isinstance(source_map, dict) else None
50
+ if isinstance(mapped, tuple) and len(mapped) == 3:
51
+ filename, lineno, source = mapped
52
+ else:
53
+ source = None
54
+ frames.append(traceback.FrameSummary(filename, lineno, frame.f_code.co_name, line=source))
55
+ tb = tb.tb_next
56
+ return traceback.StackSummary.from_list(frames)
57
+
58
+
59
+ def _filter_traceback(summary: traceback.TracebackException, error: BaseException) -> None:
60
+ summary.stack = _user_stack(error.__traceback__)
61
+ if summary.__cause__ is not None and error.__cause__ is not None:
62
+ _filter_traceback(summary.__cause__, error.__cause__)
63
+ if summary.__context__ is not None and error.__context__ is not None:
64
+ _filter_traceback(summary.__context__, error.__context__)
65
+ children = getattr(error, "exceptions", ())
66
+ for child_summary, child in zip(getattr(summary, "exceptions", None) or (), children, strict=False):
67
+ _filter_traceback(child_summary, child)
68
+
69
+
70
+ def serialize_exception(error: BaseException) -> str:
71
+ """Keep chains and groups, without pickling exceptions or their locals."""
72
+ summary = traceback.TracebackException.from_exception(error, capture_locals=False)
73
+ internal = "".join(summary.format())
74
+ _filter_traceback(summary, error)
75
+ payload: dict[str, object] = {
76
+ "chalk_remote_error": 1,
77
+ "exception": {
78
+ "kind": type(error).__name__[:256],
79
+ "message": _safe_message(error),
80
+ "stacktrace": "".join(summary.format()),
81
+ "internal_stacktrace": internal,
82
+ },
83
+ }
84
+ # Include a trace ID when serialization runs with an active trace context.
85
+ try:
86
+ trace = importlib.import_module("opentelemetry.trace")
87
+ context = trace.get_current_span().get_span_context()
88
+ if context.is_valid:
89
+ payload["trace_id"] = format(context.trace_id, "032x")
90
+ except ImportError:
91
+ pass
92
+ # gRPC status messages live in headers. Keep the complete diagnostic in
93
+ # server logs, and bound the wire payload below common 8 KiB header limits.
94
+ encoded = json.dumps(payload, ensure_ascii=True)
95
+ exception = payload["exception"]
96
+ assert isinstance(exception, dict)
97
+ while _status_header_size(encoded) > 6000:
98
+ field = max(exception, key=lambda key: _status_header_size(json.dumps(exception[key], ensure_ascii=True)))
99
+ text = exception[field]
100
+ keep = len(text) // 4
101
+ if keep < 20:
102
+ break
103
+ exception[field] = text[:keep] + "\n... remote traceback truncated ...\n" + text[-keep:]
104
+ encoded = json.dumps(payload, ensure_ascii=True)
105
+ return encoded
106
+
107
+
108
+ def _status_header_size(message: str) -> int:
109
+ # Tonic percent-encodes these ASCII characters in grpc-message. The JSON
110
+ # serializer already escapes controls and non-ASCII code points.
111
+ return len(message) + 2 * sum(char in ' "#%<>`?{}' for char in message)
112
+
113
+
114
+ def _safe_message(error: BaseException) -> str:
115
+ try:
116
+ return str(error)
117
+ except Exception:
118
+ return "<exception message unavailable>"
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
7
+
8
+ def load_handler(
9
+ dotted_path: str,
10
+ ) -> tuple[Callable[..., Any], Callable[[], None] | None, Callable[[], None] | None]:
11
+ """Load a handler function from a dotted module path.
12
+
13
+ Returns (handler_fn, on_startup_fn_or_None, on_shutdown_fn_or_None).
14
+ The on_startup/on_shutdown functions are auto-discovered if the handler's
15
+ module defines top-level callables with those names.
16
+ """
17
+ if "." not in dotted_path:
18
+ raise ValueError(f"Handler path must be a dotted path like 'my_module.handler', got: {dotted_path!r}")
19
+
20
+ module_path, func_name = dotted_path.rsplit(".", 1)
21
+ module = importlib.import_module(module_path)
22
+ handler = getattr(module, func_name, None)
23
+ if handler is None:
24
+ raise AttributeError(f"Module {module_path!r} has no attribute {func_name!r}")
25
+ if not callable(handler):
26
+ raise TypeError(f"{dotted_path!r} is not callable")
27
+
28
+ on_startup = getattr(module, "on_startup", None)
29
+ if on_startup is not None and not callable(on_startup):
30
+ on_startup = None
31
+
32
+ on_shutdown = getattr(module, "on_shutdown", None)
33
+ if on_shutdown is not None and not callable(on_shutdown):
34
+ on_shutdown = None
35
+
36
+ return handler, on_startup, on_shutdown
37
+
38
+
39
+ def load_function(dotted_path: str, label: str = "Function") -> Callable[[], None]:
40
+ """Load a callable from a dotted module path.
41
+
42
+ Args:
43
+ dotted_path: e.g. 'my_module.setup'
44
+ label: Human-readable label for error messages (e.g. "Startup", "Shutdown").
45
+ """
46
+ if "." not in dotted_path:
47
+ raise ValueError(f"{label} path must be a dotted path like 'my_module.func', got: {dotted_path!r}")
48
+
49
+ module_path, func_name = dotted_path.rsplit(".", 1)
50
+ module = importlib.import_module(module_path)
51
+ func = getattr(module, func_name, None)
52
+ if func is None:
53
+ raise AttributeError(f"Module {module_path!r} has no attribute {func_name!r}")
54
+ if not callable(func):
55
+ raise TypeError(f"{dotted_path!r} is not callable")
56
+
57
+ return func
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections.abc import Sequence
5
+
6
+ import pyarrow as pa
7
+
8
+ # Columns the caller attaches as framework side channels rather than handler arguments.
9
+ # They are identified by name and are optional per request, so they must be held out of the
10
+ # positional CHALK_INPUT_ARGS mapping, which describes only the handler's declared arguments.
11
+ RESERVED_COLUMNS = frozenset({"__chalk_row_metadata__"})
12
+
13
+
14
+ def resolve_column_names(field_names: Sequence[str], arg_names: Sequence[str]) -> list[str]:
15
+ """Map `arg_names` positionally onto the non-reserved columns of `field_names`.
16
+
17
+ Reserved columns keep their own name and consume no entry from `arg_names`, so a request
18
+ that carries one maps its arguments exactly as a request without one does.
19
+ """
20
+ positional = [name for name in field_names if name not in RESERVED_COLUMNS]
21
+ if len(arg_names) != len(positional):
22
+ raise ValueError(
23
+ f"CHALK_INPUT_ARGS specifies {len(arg_names)} names but RecordBatch has {len(positional)} columns"
24
+ )
25
+ remaining = iter(arg_names)
26
+ return [name if name in RESERVED_COLUMNS else next(remaining) for name in field_names]
27
+
28
+
29
+ def parse_input_args() -> list[str] | None:
30
+ """Parse CHALK_INPUT_ARGS env var into a list of column names.
31
+
32
+ Returns None if the env var is unset or empty.
33
+ """
34
+ raw = os.environ.get("CHALK_INPUT_ARGS", "").strip()
35
+ if not raw:
36
+ return None
37
+ return [name.strip() for name in raw.split(",") if name.strip()]
38
+
39
+
40
+ def transform(batch: pa.RecordBatch, arg_names: list[str] | None) -> dict[str, pa.Array]:
41
+ """Transform a RecordBatch into a dict of named arrays.
42
+
43
+ If arg_names is provided, columns are renamed by index to the given names
44
+ (ignoring the original column names), except for reserved columns, which keep
45
+ their own name. If arg_names is None, the original column names are used.
46
+ """
47
+ if arg_names is not None:
48
+ names = resolve_column_names(batch.schema.names, arg_names)
49
+ return {name: batch.column(i) for i, name in enumerate(names)}
50
+ return {batch.schema.field(i).name: batch.column(i) for i in range(batch.num_columns)}
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+ _RECORD_FIELDS = frozenset(logging.makeLogRecord({}).__dict__) | {"message", "asctime"}
9
+
10
+
11
+ class JsonFormatter(logging.Formatter):
12
+ """Emit one JSON object per line using fields recognized by Chalk's log collector."""
13
+
14
+ def format(self, record: logging.LogRecord) -> str:
15
+ fields: dict[str, Any] = {key: value for key, value in vars(record).items() if key not in _RECORD_FIELDS}
16
+ fields.update(
17
+ timestamp=datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
18
+ severity=record.levelname,
19
+ logger=record.name,
20
+ message=record.getMessage(),
21
+ )
22
+ # Keep tracebacks out of the message and escape their newlines so the collector sees one event.
23
+ if record.exc_info:
24
+ fields["exception.stacktrace"] = self.formatException(record.exc_info)
25
+ elif record.exc_text:
26
+ fields["exception.stacktrace"] = record.exc_text
27
+ if record.stack_info:
28
+ fields["stack_info"] = self.formatStack(record.stack_info)
29
+ return json.dumps(fields, default=str)
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from collections.abc import Callable
6
+ from typing import Any, Literal
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ _BATCHING_MODE_ENV = "CHALK_REMOTE_CALL_BATCHING_MODE"
11
+ _VALID_BATCHING_MODES = ("per_caller", "combined")
12
+
13
+
14
+ def _resolve_batching_mode(kwarg: str | None) -> str:
15
+ """Resolve batching mode from kwarg or env var.
16
+
17
+ Precedence: explicit kwarg > env var > default "per_caller". The env var
18
+ is the canonical opt-in for combined mode; the kwarg exists so embedders
19
+ can override programmatically.
20
+ """
21
+ if kwarg is not None:
22
+ mode = kwarg
23
+ else:
24
+ mode = (os.environ.get(_BATCHING_MODE_ENV, "") or "per_caller").strip() or "per_caller"
25
+ if mode not in _VALID_BATCHING_MODES:
26
+ raise ValueError(f"batching_mode must be one of {_VALID_BATCHING_MODES}, got {mode!r}")
27
+ return mode
28
+
29
+
30
+ def serve(
31
+ handler: Callable[..., Any],
32
+ host: str = "[::]",
33
+ port: int = 6666,
34
+ workers: int = 10,
35
+ on_startup: Callable[[], None] | None = None,
36
+ on_shutdown: Callable[[], None] | None = None,
37
+ arg_names: list[str] | None = None,
38
+ max_batching_size: int | None = None,
39
+ max_buffer_duration_ms: int | None = None,
40
+ batching_mode: Literal["per_caller", "combined"] | None = None,
41
+ ) -> None:
42
+ """Start the gRPC server implementing RemoteCallService.
43
+
44
+ Args:
45
+ handler: The user's handler function.
46
+ Single-request mode (default): (event, context) -> result.
47
+ Coalesced per-caller mode (when max_batching_size is set and
48
+ batching_mode == "per_caller"):
49
+ (events: list[dict], contexts: list[dict]) -> list[result],
50
+ one result per input.
51
+ Coalesced combined mode (when max_batching_size is set and
52
+ batching_mode == "combined"):
53
+ (combined: pa.RecordBatch, offsets: list[int], contexts: list[dict])
54
+ -> pa.RecordBatch with combined.num_rows rows.
55
+ host: The host to bind to.
56
+ port: The port to bind to.
57
+ workers: Number of worker threads for the tokio runtime.
58
+ on_startup: Optional startup hook called before the server starts accepting requests.
59
+ on_shutdown: Optional shutdown hook called after the server stops accepting requests.
60
+ arg_names: Parsed CHALK_INPUT_ARGS column names, or None.
61
+ max_batching_size: Maximum number of concurrent requests to coalesce
62
+ before invoking the handler. None/0 disables coalescing.
63
+ max_buffer_duration_ms: Max time in milliseconds to buffer incoming
64
+ requests before flushing even if max_batching_size hasn't been
65
+ reached. Defaults to 1000ms when batching is enabled.
66
+ batching_mode: "per_caller" (default) or "combined". Only effective
67
+ when max_batching_size > 0. Overrides the
68
+ CHALK_REMOTE_CALL_BATCHING_MODE env var when set; otherwise the
69
+ env var (or "per_caller") wins.
70
+ """
71
+ # Keep package imports lightweight so the CLI can configure logging before dependency failures.
72
+ from chalk_remote_call._native import start_server
73
+ from chalk_remote_call.servicer import (
74
+ process_batches,
75
+ process_batches_coalesced,
76
+ process_batches_coalesced_combined,
77
+ )
78
+
79
+ if on_startup is not None:
80
+ logger.info("Running startup hook...")
81
+ on_startup()
82
+
83
+ mode = _resolve_batching_mode(batching_mode)
84
+
85
+ if max_batching_size is not None and max_batching_size > 0:
86
+ process_fn = process_batches_coalesced_combined if mode == "combined" else process_batches_coalesced
87
+ batch_size = max_batching_size
88
+ duration_ms = max_buffer_duration_ms if max_buffer_duration_ms is not None else 1000
89
+ logger.info(
90
+ "Batching enabled: mode=%s max_batching_size=%d max_buffer_duration_ms=%d",
91
+ mode,
92
+ batch_size,
93
+ duration_ms,
94
+ )
95
+ else:
96
+ if mode == "combined":
97
+ raise ValueError("batching_mode='combined' requires max_batching_size > 0")
98
+ process_fn = process_batches
99
+ batch_size = 0
100
+ duration_ms = 0
101
+
102
+ logger.info("Starting server on %s:%d", host, port)
103
+ try:
104
+ start_server(
105
+ handler=handler,
106
+ process_fn=process_fn,
107
+ host=host,
108
+ port=port,
109
+ workers=workers,
110
+ arg_names=arg_names,
111
+ max_batching_size=batch_size,
112
+ max_buffer_duration_ms=duration_ms,
113
+ )
114
+ finally:
115
+ if on_shutdown is not None:
116
+ logger.info("Running shutdown hook...")
117
+ on_shutdown()