Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This catalog is the foundation for generating language bindings (Python, Java, R
- [Getting started](#getting-started)
- [Output format](#output-format)
- [Adding metadata](#adding-metadata)
- [Runtime server](#runtime-server)

## How it works

Expand Down Expand Up @@ -83,3 +84,34 @@ A typical function entry looks like this:
## Adding metadata

Manual annotations (ownership rules, additional documentation, deprecation flags, etc.) live in `meta/meos-meta.json`. The merger applies them on top of the libclang-parsed structure when generating the final catalog.

## Runtime server

The same enriched catalog also drives a runtime HTTP server — the projection
that **executes** rather than just describes:

```bash
python run.py # produce the enriched catalog
python serve.py # 127.0.0.1:8080 (StubEngine)
MEOS_LIBRARY_PATH=/path/libmeos.so python serve.py # real engine
```

Each *stateless-exposable* function is served as `POST /{function}`:
validate the JSON body, `decode` each serialized string to an opaque handle,
`invoke` the function, `encode` the result, reply `{"result": …}` (`204`
void, `400 {"error","code"}` on failure). All MEOS work sits behind a
pluggable `Engine`: `CtypesEngine` (`dlopen` a built `libmeos`, every opaque
value an anonymous `void *`) or `StubEngine` (no build needed; routing and
validation still work).

Built from the live MobilityDB `master` catalog this is **1963 operations**
(91% of the public API; internal `meos_internal*.h` policy-excluded);
generation, routing, validation and dispatch are exercised end-to-end over
real HTTP (`tests/test_server.py`), and the full stack is validated against
an installed `/usr/local/lib/libmeos.so` — `POST /temporal_copy` →
`200 {"result":"{t@…, f@…}"}` (decode → invoke → `temporal_out(maxdd=15)`),
`floatset_value_n` n=2 → `2.5` via a byref out-parameter, a malformed body
→ `400` with the real MEOS message, and the server survives it
(`tests/test_engine_integration.py`, skipped unless `MEOS_LIBRARY_PATH` is
set). See [`docs/server.md`](docs/server.md). Stdlib only (`http.server`);
no new dependencies.
104 changes: 104 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Runtime server

`server/` is the projection that **executes**. It builds its entire routing,
request-validation and dispatch table from the *enriched* catalog
(`network` / `wire` — see [`enrichment.md`](enrichment.md)), the same single
source the OpenAPI and MCP generators consume.

```bash
python run.py # enriched catalog
python serve.py # serve on 127.0.0.1:8080
MEOS_LIBRARY_PATH=/path/libmeos.so python serve.py 0.0.0.0 9000
```

Per `POST /{function}` it runs the universal pipeline the `wire` model
implies:

1. validate the JSON body against the parameter model;
2. `engine.decode` each serialized string → opaque handle;
3. `engine.invoke` the function with scalars + handles;
4. `engine.encode` an opaque result → string;
5. reply `{"result": …}` · `204` for void · `400 {"error","code"}` on a
MEOS/validation error · `404` for an unknown operation.

`GET /healthz` reports engine and operation count. Stdlib `http.server`
only (no new dependencies) — a reference/embeddable server, not a tuned
production stack.

## The engine seam

All MEOS work is behind `server/engine.py`:

| Engine | Use |
|---|---|
| `CtypesEngine` | **Real.** `dlopen`s a built `libmeos` and calls `x-meos.decode` / function / `x-meos.encode` by symbol. Every opaque value is an anonymous `void *` — no struct layout is ever needed, because the catalog already reduced every exposable function to *scalars + decode/encode of opaque pointers*. Selected when `MEOS_LIBRARY_PATH` is set. |
| `StubEngine` | No MEOS build: routes/validation/error-mapping run; MEOS calls return deterministic placeholders. Default, and what makes the server runnable/testable without a compiled MEOS. |

## What is validated

- **Generation, routing, validation, dispatch, error mapping** — built from
the live MobilityDB `master` catalog (**1963 operations** = 91% of the
public API; internal `meos_internal*.h` is policy-excluded; 0 malformed),
exercised end-to-end over real HTTP sockets with a recording engine
(`tests/test_server.py`).
- **Real `libmeos` end-to-end** — against an installed
`/usr/local/lib/libmeos.so` (`tests/test_engine_integration.py`,
skipped unless `MEOS_LIBRARY_PATH` is set):

```
POST /temporal_copy {"temp":"{t@2000-01-01, f@2000-01-03, t@2000-01-05}"}
-> 200 {"result":"{t@2000-01-01 00:00:00+01, f@..., t@...}"}
# full path: decode(tbool_in) -> invoke -> encode(temporal_out, maxdd=15)
POST /temporal_num_instants {"temp":"{t@2000-01-01, f@2000-01-03, ...}"}
-> 200 {"result": 3}
POST /temporal_num_instants {"temp":"garbage"}
-> 400 {"error":"Missing delimeter character '@': garbage","code":22}
GET /healthz (after the bad request) -> 200 # process survived

floatset_value_n(decode "{1.0, 2.5, 3.0}", n=2, double *result)
-> present=True, result=2.5 # scalar value via byref out-parameter
-> n=99: present=False # -> HTTP 204 (no value)
geoset_value_n(decode "{Point(1 1), Point(2 2)}", n=1, GSERIALIZED **result)
-> present=True, encode(geo_as_ewkt) -> "POINT(1 1)" # opaque out-param
temporal_merge_array(["t@2000-01-01","f@2000-01-03"]) # JSON list
-> decode each -> C array -> 200 {"result":"{t@..., f@...}"}
temporal_sequences("{[t@..., f@...], [t@...]}") # Elem **+count
-> byref count -> 200 {"result":["...","..."]} # JSON array
```

The whole pipeline runs on real MEOS, including the **generic
`temporal_out`** with its `maxdd` aux defaulted — `test_engine_
integration` round-trips a `tbool` *and* a `tfloat` through it, proving
it serialises any subtype. A malformed input becomes a `400` (the
installed non-fatal error handler) instead of `exit()`ing the server.
- `CtypesEngine` marshalling (including aux args) is additionally
unit-tested against a fake library.

## Limitations / roadmap

- **Polymorphic decoding is subtype-narrow (input side only).** Coverage
is **1963/2161 = 91% of the public API**: formatting aux args are
defaulted (generic `temporal_out`, `*_out(.., maxdd)`), scalar *and*
opaque out-parameter accessors are projected through their byref result,
input-array builders take a JSON list, array returns (`Elem **`+count)
become a JSON array, and the internal
`meos_internal*.h` programmer API is
policy-excluded. The remaining limitation: polymorphic types whose only
generic decoder needs a semantic type tag (`temporal_in(str, meosType)`,
the typed-set decoders) decode a serialized **argument** with a typed
helper (`tbool_in`, a `bigint`-set parser). A mismatched subtype yields a
clean MEOS `400`, never a crash or wrong answer. Carrying the subtype on
the wire for universal decoding is the remaining future work toward full
parity; the residual non-exposable set is otherwise genuinely
non-stateless (array/multi-out builders, `Datum`-internal, plumbing).
- Response is wrapped `{"result": …}` (matches the MCP `outputSchema`
envelope); an unwrapped mode matching the bare OpenAPI 200 schema is a
trivial follow-up.
- `CtypesEngine` integer width per function uses `c_long`; functions needing
exact `int32`/`size_t` widths may need per-function refinement (an
enrichment-side concern).
- Stdlib server is single-process; production deployment (ASGI/WSGI,
concurrency, auth) is intentionally out of scope — the value here is the
*correct-by-construction* contract execution, not the transport.
- Memory ownership: results are encoded then dropped; wiring MEOS
`pfree`/free of returned pointers into `CtypesEngine` is a follow-up.
48 changes: 48 additions & 0 deletions serve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Run the contract-driven MEOS HTTP server.
#
# Usage:
# python run.py # produce the enriched catalog
# python serve.py # serve output/meos-idl.json on :8080
# python serve.py catalog.json 0.0.0.0 9000
#
# Engine: set MEOS_LIBRARY_PATH=/path/to/libmeos.so for the real ctypes
# engine; otherwise a non-computing StubEngine is used (routes/validation
# work, MEOS calls return placeholders).

import json
import sys
from pathlib import Path

from server.app import make_server
from server.engine import from_env

IN_PATH = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("output/meos-idl.json")
HOST = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1"
PORT = int(sys.argv[3]) if len(sys.argv) > 3 else 8080


def main() -> None:
if not IN_PATH.exists():
sys.exit(f"Catalog not found: {IN_PATH} — run `python run.py` first.")
catalog = json.loads(IN_PATH.read_text())
if not any("network" in f for f in catalog.get("functions", [])):
sys.exit(f"{IN_PATH} is not enriched (no `network` fields).")

engine = from_env()
srv = make_server(catalog, engine, HOST, PORT)
n = sum(1 for f in catalog["functions"]
if f.get("network", {}).get("exposable"))
print(f"MEOS server on http://{HOST}:{PORT} "
f"({n} operations, engine={getattr(engine, 'name', '?')})",
file=sys.stderr)
try:
srv.serve_forever()
except KeyboardInterrupt:
pass
finally:
srv.server_close()
engine.close()


if __name__ == "__main__":
main()
Loading