Skip to content

Commit 49f0105

Browse files
authored
Merge pull request #288 from RayforceDB/dev
v2.1.6
2 parents a729406 + ecac337 commit 49f0105

26 files changed

Lines changed: 486 additions & 92 deletions

.github/requirements-docs.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mkdocs-material>=9.5

.github/workflows/pages.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ on:
1414
- 'docs/**'
1515
- 'overrides/**'
1616
- 'mkdocs.yml'
17-
- 'requirements-docs.txt'
17+
- '.github/requirements-docs.txt'
1818
- '.github/workflows/pages.yml'
1919
workflow_dispatch:
2020

@@ -36,9 +36,9 @@ jobs:
3636
with:
3737
python-version: '3.12'
3838
cache: pip
39-
cache-dependency-path: requirements-docs.txt
39+
cache-dependency-path: .github/requirements-docs.txt
4040
- name: Install MkDocs Material
41-
run: pip install -r requirements-docs.txt
41+
run: pip install -r .github/requirements-docs.txt
4242
- name: Build site (MkDocs)
4343
run: mkdocs build --strict
4444
- uses: actions/configure-pages@v4

docs/docs/language/control-flow.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,20 @@ Recursive lambdas use `self` to refer to the enclosing function:
7979

8080
If no error is raised, `try` returns the result of the body expression normally. Works inside lambdas compiled to bytecode.
8181

82+
### Fallback value
83+
84+
If the second argument is **not** a function, it is returned as-is as the fallback value on error (evaluated only when the body fails). Because lambdas do not capture closures, this is the only way to surface an outer binding from the failure branch:
85+
86+
```lisp
87+
‣ (try (raise "boom") 0)
88+
0
89+
90+
‣ ((fn [data] (try (raise "boom") data)) 123)
91+
123
92+
```
93+
94+
A handler must accept the single error argument, so only a lambda or a unary builtin is *called* with the error; any other value (including a multi-argument builtin) is treated as a fallback value.
95+
8296
## Early Return: return
8397

8498
`return` exits the innermost enclosing compiled lambda early with the given value:

docs/docs/language/functions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ Cross-temporal comparisons are supported: dates, times, and timestamps are all c
316316
| `if` | variadic, special | Conditional (if/then/else) | `(if (> x 0) "pos" "neg")` |
317317
| `do` | variadic, special | Sequential execution, returns last | `(do (set x 1) (set y 2) (+ x y))` |
318318
| `fn` | variadic, special | Create lambda function | `(fn [x] (* x x))` |
319-
| `try` | binary, special | Error handling (expr handler) | `(try (/ 1 0) (fn [e] 0))` |
319+
| `try` | binary, special | Error handling (expr handler-or-fallback) | `(try (/ 1 0) (fn [e] 0))` |
320320
| `raise` | unary | Throw an error | `(raise "bad input")` |
321321
| `return` | variadic | Early return from compiled lambda (0 args → null) | `(return 42)` |
322322
| `quote` | variadic, special | Return argument unevaluated; a bare name becomes a literal symbol (`(quote x)``'x`) | `(quote (+ 1 2))``(+ 1 2)` |

docs/docs/namespaces/ipc.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,25 @@ Connect to a Rayforce server (`./rayforce -p <port>`) and exchange messages over
1212

1313
| Function | Arity | Flags | Description |
1414
|---|---|---|---|
15-
| [`.ipc.open`](#ipc-open) | unary | restricted | Open a TCP connection; return an i64 handle. |
15+
| [`.ipc.open`](#ipc-open) | variadic | restricted | Open a TCP connection (optional connect timeout); return an i64 handle. |
1616
| [`.ipc.send`](#ipc-send) | binary | restricted | Send a message synchronously; return the server's result. |
1717
| [`.ipc.post`](#ipc-post) | binary | restricted | Send a message asynchronously (fire-and-forget); return the null object. |
1818
| [`.ipc.close`](#ipc-close) | unary | restricted | Close a connection handle. |
1919
| [`.ipc.handle`](#ipc-handle) | variadic || The current connection's handle inside a server-side hook; `-1` otherwise. |
2020

2121
## `.ipc.open` { #ipc-open }
2222

23-
Signature: `(.ipc.open "host:port")` or `(.ipc.open "host:port:user:password")`.
23+
Signature: `(.ipc.open "host:port")` or `(.ipc.open "host:port:user:password")`, with an optional trailing connect timeout in milliseconds: `(.ipc.open "host:port" 2000)`.
2424

2525
Returns: an `i64` handle. Negative handles never escape — errors are surfaced as Rayfall error objects:
2626

27-
- `type` — argument is not a string.
28-
- `domain` — malformed address (missing port, port out of `(0, 65535]`, oversized host/user/password).
27+
- `type` — address argument is not a string, or the timeout argument is not an integer.
28+
- `rank` — called with fewer than 1 or more than 2 arguments.
29+
- `domain` — malformed address (missing port, port out of `(0, 65535]`, oversized host/user/password), or a negative timeout.
2930
- `access` — server requires auth and you didn't supply credentials, **or** the password is wrong.
30-
- `io` — connection refused / network error.
31+
- `io` — connection refused / network error, or `connection timed out` when the connect did not complete within the timeout.
32+
33+
The optional timeout bounds **both** the TCP connect and the handshake I/O. A blocking `connect()` ignores socket send/receive timeouts, so without an explicit bound a dead or packet-filtered peer would otherwise hang for the operating-system default (often minutes). When omitted, a default budget of 5 seconds applies.
3134

3235
The handshake exchanges a 2-byte `{wire_version, auth_flag}` greeting. A wire-version mismatch closes the connection before any payload is exchanged.
3336

@@ -37,6 +40,9 @@ The handshake exchanges a 2-byte `{wire_version, auth_flag}` greeting. A wire-ve
3740
3841
;; With credentials (server started with -u or -U)
3942
(set h (.ipc.open "127.0.0.1:5000:admin:secret123"))
43+
44+
;; Fail fast if the peer doesn't answer within 2 seconds
45+
(set h (.ipc.open "127.0.0.1:5000" 2000))
4046
```
4147

4248
## `.ipc.send` { #ipc-send }

docs/docs/reference/all-functions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ Special forms receive their arguments unevaluated. These are the core language p
231231
| `if` | variadic | special | Conditional: (if cond then else) | `(if (> x 0) "pos" "neg")` |
232232
| `do` | variadic | special | Sequential execution, returns last value | `(do (set x 1) (set y 2) (+ x y))` |
233233
| `fn` | variadic | special | Create lambda function | `(fn [x y] (+ x y))` |
234-
| `try` | binary | special | Error handling: (try expr handler-fn) | `(try (/ 1 0) (fn [e] 0))` |
234+
| `try` | binary | special | Error handling: (try expr handler-fn-or-fallback-value) | `(try (/ 1 0) (fn [e] 0))` |
235235
| `raise` | unary || Throw an error with message | `(raise "bad input")` |
236236
| `return` | unary || Early return from function body | `(return 42)` |
237237
| `quote` | variadic | special | Return argument unevaluated; a bare name yields a literal symbol (`(quote x)``'x`) | `(quote (+ 1 2))``(+ 1 2)` |
@@ -530,7 +530,7 @@ TCP-based IPC for connecting to remote Rayforce instances. Uses binary serializa
530530

531531
| Function | Type | Flags | Description | Example |
532532
|---|---|---|---|---|
533-
| `.ipc.open` | unary | restricted | Open TCP connection to host:port, returns handle | `(.ipc.open "localhost:5000")` |
533+
| `.ipc.open` | variadic | restricted | Open TCP connection to host:port (optional connect timeout in ms), returns handle | `(.ipc.open "localhost:5000" 2000)` |
534534
| `.ipc.close` | unary | restricted | Close an IPC connection handle | `(.ipc.close h)` |
535535
| `.ipc.send` | binary | restricted | Send a value over an IPC handle (sync request) | `(.ipc.send h "(sum (til 100))")` |
536536
| `.ipc.handle` | variadic || Current connection handle inside any `.ipc.on.*` hook, `-1` outside | `(.ipc.handle)` |

include/rayforce.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,8 @@ ray_t* ray_fmt(ray_t* obj, int mode);
647647
* exchange; ray_ipc_send_async sends a fire-and-forget frame. */
648648

649649
int64_t ray_ipc_connect(const char* host, uint16_t port,
650-
const char* user, const char* password);
650+
const char* user, const char* password,
651+
int timeout_ms);
651652
void ray_ipc_close(int64_t handle);
652653
ray_t* ray_ipc_send(int64_t handle, ray_t* msg);
653654
ray_err_t ray_ipc_send_async(int64_t handle, ray_t* msg);

src/app/repl.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ ray_t* ray_repl_connect_fn(ray_t* host_port_str) {
573573
* and the connect-error-to-rayfall-error mapping; reusing it
574574
* keeps .repl.connect a one-line wrapper instead of a parallel
575575
* implementation that drifts. */
576-
ray_t* opened = ray_hopen_fn(host_port_str);
576+
ray_t* opened = ray_hopen_fn(&host_port_str, 1);
577577
if (!opened || RAY_IS_ERR(opened)) return opened;
578578
if (!ray_is_atom(opened) ||
579579
(opened->type != -RAY_I64 && opened->type != -RAY_I32)) {

src/core/ipc.c

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -339,10 +339,24 @@ static int hook_call_auth(ray_poll_t* poll, int64_t handle,
339339
static void send_response(ray_sock_t fd, ray_t* result)
340340
{
341341
int64_t ser_size = ray_serde_size(result);
342-
if (ser_size <= 0) return;
342+
343+
/* A result we cannot serialize must never leave the client waiting on
344+
* a reply that never arrives. Substitute a serializable error so the
345+
* caller observes a clean failure instead of a silent infinite hang
346+
* (issue #285). ray_error frames serialize, so the substitute always
347+
* goes out unless even that fails — in which case there is nothing we
348+
* can put on the wire and we drop as before. */
349+
ray_t* fallback = NULL;
350+
if (ser_size <= 0) {
351+
fallback = ray_error("type", "result of type %s is not serializable over IPC",
352+
result ? ray_type_name(result->type) : "null");
353+
result = fallback;
354+
ser_size = ray_serde_size(result);
355+
if (ser_size <= 0) { if (fallback) ray_error_free(fallback); return; }
356+
}
343357

344358
uint8_t* payload = (uint8_t*)ray_sys_alloc((size_t)ser_size);
345-
if (!payload) return;
359+
if (!payload) { if (fallback) ray_error_free(fallback); return; }
346360
ray_ser_raw(payload, result);
347361

348362
uint8_t* send_buf = NULL;
@@ -387,6 +401,7 @@ static void send_response(ray_sock_t fd, ray_t* result)
387401

388402
ray_sys_free(send_buf);
389403
if (payload) ray_sys_free(payload);
404+
if (fallback) ray_error_free(fallback);
390405
}
391406

392407
/* Decompress (when flagged) + de-serialize one framed payload into an
@@ -501,6 +516,15 @@ static ray_t* eval_payload_core(uint8_t* payload, size_t payload_len,
501516
ray_release(msg);
502517
}
503518
}
519+
/* A lazy result is an internal deferred-DAG representation that cannot
520+
* be serialized — force it to a concrete value before it reaches the
521+
* wire. The direct ray_eval(msg) path (non-STR payloads, e.g. an
522+
* expression list `(first v)`) returns lazy chains verbatim; the
523+
* ray_eval_str path already materializes, so this is a no-op there.
524+
* Without this, send_response cannot serialize the result and the
525+
* client blocks forever waiting for a reply (issue #285). */
526+
if (result && ray_is_lazy(result))
527+
result = ray_lazy_materialize(result); /* consumes the retain */
504528
return result ? result : RAY_NULL_OBJ;
505529
}
506530

@@ -1401,16 +1425,20 @@ static int64_t conn_write_msg(ray_sock_t fd, ray_t* msg, uint8_t msgtype,
14011425
}
14021426

14031427
int64_t ray_ipc_connect(const char* host, uint16_t port,
1404-
const char* user, const char* password)
1428+
const char* user, const char* password,
1429+
int timeout_ms)
14051430
{
14061431
/* The connection lives in the active poll's selector table — its
14071432
* selector id IS the handle. No poll, no handle namespace: refuse
14081433
* up front rather than hand out an integer nothing can resolve. */
14091434
ray_poll_t* poll = ipc_active_poll();
14101435
if (!poll) return -1;
14111436

1412-
ray_sock_t fd = ray_sock_connect(host, port, 5000);
1413-
if (fd == RAY_INVALID_SOCK) return -1;
1437+
/* Default the connect/handshake budget to 5s when the caller gives
1438+
* no explicit timeout, matching the long-standing handshake timeout. */
1439+
int connect_to = timeout_ms > 0 ? timeout_ms : 5000;
1440+
ray_sock_t fd = ray_sock_connect(host, port, connect_to);
1441+
if (fd == RAY_INVALID_SOCK) return (errno == ETIMEDOUT) ? -5 : -1;
14141442

14151443
uint8_t hs[2] = { RAY_SERDE_WIRE_VERSION, 0x00 };
14161444
if (ray_sock_send(fd, hs, 2) < 0) {

src/core/ipc.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,13 @@ int ray_ipc_poll(ray_ipc_server_t* srv, int timeout_ms);
114114
* the connection's rx machinery while waiting, dispatching any
115115
* interleaved async/sync frames from the peer. */
116116

117+
/* timeout_ms > 0 bounds the TCP connect and the handshake I/O; <= 0 uses
118+
* the default budget. Returns the handle (>= 0) or a negative code:
119+
* -1 refused/error, -2 auth required, -3 auth failed, -4 wire mismatch,
120+
* -5 connect timed out. */
117121
int64_t ray_ipc_connect(const char* host, uint16_t port,
118-
const char* user, const char* password);
122+
const char* user, const char* password,
123+
int timeout_ms);
119124
void ray_ipc_close(int64_t handle);
120125
ray_t* ray_ipc_send(int64_t handle, ray_t* msg);
121126
ray_err_t ray_ipc_send_async(int64_t handle, ray_t* msg);

0 commit comments

Comments
 (0)