Skip to content
Merged
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
16 changes: 13 additions & 3 deletions examples/nokv-authority-store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,19 @@ run is Stage 2A single-node storage conformance evidence only.
## Inputs

Use a current NoKV Python environment. Keep the client configuration in an
ignored local file; do not commit credentials. Static routing is valid for a
single-node NoKV deployment—etcd is not required by this probe. The following
shape is illustrative:
ignored local file; do not commit credentials. The helper accepts three routing
kinds and passes each to the matching `RoutingConfig` constructor of the
installed SDK: `etcd` and `static` (the 0.11.0 release wheel) and `seeds`
(`{"kind": "seeds", "endpoints": ["IP:PORT", ...]}`, the NoKV
metadata-runtimes line, which names serving owners directly and drops the etcd
constructor). Static routing is valid for a single-node NoKV deployment; etcd
is not required by this probe. A routing kind the installed wheel cannot build
fails the open handshake with `nokv_sdk_capability_mismatch` before any client
is constructed, so a seeds configuration against a 0.11.0 wheel (or an etcd
configuration against a metadata-runtimes wheel) is reported as the wrong
wheel, not as an outage. The `ready` handshake echoes `nokv_protocol_schema`:
the SDK's `WORKSPACE_PROTOCOL_SCHEMA` when the wheel exports one, otherwise
`null` (the 0.11.0 release does not). The following shape is illustrative:

```json
{
Expand Down
58 changes: 52 additions & 6 deletions loopx/control_plane/coordination/nokv_jsonl_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@
}
)
_ETCD_ROUTING_KEYS = frozenset({"kind", "endpoints", "key_prefix", "lease_ttl_seconds"})
# Seed routing names one or more serving NoKV owners directly (numeric IP:port);
# the SDK rejects hostnames, empty lists and unspecified ports itself.
_SEEDS_ROUTING_KEYS = frozenset({"kind", "endpoints"})
# Every routing kind this helper can express. A kind outside this set is a
# configuration error; a kind inside it that the installed SDK cannot build is
# a capability mismatch between the wheel and the configuration.
_ROUTING_KINDS = frozenset({"etcd", "seeds", "static"})
_STATIC_ROUTING_KEYS = frozenset(
{
"kind",
Expand Down Expand Up @@ -67,6 +74,14 @@ class RequestError(ValueError):
"""The JSON-lines caller violated the raw storage protocol."""


class SdkCapabilityMismatch(RequestError):
"""The installed NoKV SDK lacks the constructor this configuration needs.

Raised only after the configuration itself validated, so the caller can
tell "wrong wheel for this routing kind" apart from "invalid config".
"""


class ProviderProtocolError(RuntimeError):
"""The NoKV SDK returned a shape that violates its reviewed contract."""

Expand Down Expand Up @@ -420,6 +435,8 @@ def build_client(config_value: object) -> Any:
_require_exact_keys(config, _CONFIG_KEYS, "config")
routing_value = _mapping(config.get("routing"), "routing")
routing_kind = _required_string(routing_value, "kind")
if routing_kind not in _ROUTING_KINDS:
raise RequestError(f"unsupported routing kind {routing_kind!r}")
if routing_kind == "etcd":
_require_exact_keys(routing_value, _ETCD_ROUTING_KEYS, "routing")
routing_arguments: tuple[object, ...] = (
Expand All @@ -443,7 +460,8 @@ def build_client(config_value: object) -> Any:
_generation(routing_value.get("owner_epoch"), "owner_epoch"),
)
else:
raise RequestError(f"unsupported routing kind {routing_kind!r}")
_require_exact_keys(routing_value, _SEEDS_ROUTING_KEYS, "routing")
routing_arguments = (_string_list(routing_value, "endpoints"),)

object_value = _mapping(config.get("object_store"), "object_store")
object_kind = _required_string(object_value, "kind")
Expand Down Expand Up @@ -516,12 +534,17 @@ def build_client(config_value: object) -> Any:
except AttributeError as error:
raise RequestError("the NoKV Python SDK surface is incomplete") from error

try:
routing = (
RoutingConfig.etcd(*routing_arguments)
if routing_kind == "etcd"
else RoutingConfig.static(*routing_arguments)
# The kind was checked against _ROUTING_KINDS above, so this attribute
# lookup never reaches an arbitrary caller-chosen name. The 0.11.0 release
# wheel provides etcd/static; the metadata-runtimes line provides seeds.
routing_constructor = getattr(RoutingConfig, routing_kind, None)
if not callable(routing_constructor):
raise SdkCapabilityMismatch(
"the installed NoKV Python SDK does not provide "
f"RoutingConfig.{routing_kind}"
)
try:
routing = routing_constructor(*routing_arguments)
except (TypeError, ValueError) as error:
raise RequestError("NoKV routing configuration is invalid") from error
try:
Expand Down Expand Up @@ -550,6 +573,19 @@ def build_client(config_value: object) -> Any:
raise RequestError("NoKV client configuration is invalid") from error


def _sdk_protocol_schema() -> str | None:
"""Return the wire schema the imported SDK declares, if it declares one.

The 0.11.0 release wheel has no such attribute; newer wheels export
``WORKSPACE_PROTOCOL_SCHEMA`` so a deployment can compare it against the
server before trusting a nominally equal ``__version__``.
"""
schema = getattr(sys.modules.get("nokv"), "WORKSPACE_PROTOCOL_SCHEMA", None)
if isinstance(schema, str) and schema and schema.strip() == schema:
return schema
return None


def main() -> int:
first = sys.stdin.readline()
try:
Expand All @@ -561,6 +597,15 @@ def main() -> int:
client = build_client(values.get("config"))
except json.JSONDecodeError as error:
result = _failure(None, "failed", "invalid_json", error)
except SdkCapabilityMismatch as error:
result = _failure(
_request_id(value.get("request_id"))
if isinstance(value, Mapping)
else None,
"failed",
"nokv_sdk_capability_mismatch",
error,
)
except RequestError as error:
result = _failure(
_request_id(value.get("request_id"))
Expand Down Expand Up @@ -592,6 +637,7 @@ def main() -> int:
"ready",
nokv_sdk_version=QUALIFIED_NOKV_SDK_VERSION,
nokv_api_version=QUALIFIED_NOKV_API_VERSION,
nokv_protocol_schema=_sdk_protocol_schema(),
),
sort_keys=True,
separators=(",", ":"),
Expand Down
40 changes: 33 additions & 7 deletions tests/control_plane_ts/nokv_jsonl_transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,22 @@ const FAKE_SDK_ROOT = fileURLToPath(
new URL("../fixtures/nokv_fake_sdk", import.meta.url),
);

async function openSdkHelper() {
const ETCD_ROUTING = {
kind: "etcd",
endpoints: ["http://127.0.0.1:2379"],
key_prefix: "/nokv/control",
lease_ttl_seconds: 10,
};
// Seed routing names serving owners directly; it is the routing kind of the
// NoKV metadata-runtimes line, which drops the etcd constructor.
const SEEDS_ROUTING = { kind: "seeds", endpoints: ["127.0.0.1:7750"] };

async function openSdkHelper(routing: Record<string, unknown> = ETCD_ROUTING) {
return await NoKVJsonLinesTransport.open({
argv: [PYTHON, SDK_HELPER],
config: {
root_id: "0".repeat(32),
routing: {
kind: "etcd",
endpoints: ["http://127.0.0.1:2379"],
key_prefix: "/nokv/control",
lease_ttl_seconds: 10,
},
routing,
object_store: { kind: "memory" },
},
env: {
Expand Down Expand Up @@ -201,3 +206,24 @@ test("NoKV AuthorityStore preserves helper protocol failure as failed, not missi
assert.equal(loaded.reason_code, "provider_protocol_violation");
}
});

test("JSON-lines transport opens the real helper with seed routing", async () => {
const transport = await openSdkHelper(SEEDS_ROUTING);
try {
const identity = await transport.storeIdentity("authority-workbench");
assert.equal(identity.status, "available");
if (identity.status !== "available") throw new Error("unreachable");
assert.equal(identity.store_identity, `nokv:authority-workbench:${"a".repeat(32)}`);
} finally {
await transport.close();
}
});

test("JSON-lines transport surfaces an unknown routing kind as a typed protocol failure", async () => {
await assert.rejects(
openSdkHelper({ kind: "gossip", endpoints: ["127.0.0.1:7750"] }),
(error: unknown) =>
error instanceof NoKVTransportProtocolError && /routing kind/.test(error.message),
);
});

Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ class RoutingConfig:
def etcd(*values):
return ("etcd", values)

@staticmethod
def seeds(*values):
return ("seeds", values)

@staticmethod
def static(*values):
return ("static", values)
Expand Down
6 changes: 6 additions & 0 deletions tests/fixtures/nokv_fake_sdk/nokv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@


class RoutingConfig:
# Union of the two real wheels the helper is qualified against: the 0.11.0
# release provides etcd/static, the metadata-runtimes line provides seeds.
@staticmethod
def seeds(endpoints: list[str]) -> object:
return ("seeds", endpoints)

@staticmethod
def etcd(endpoints: list[str], key_prefix: str, lease_ttl_seconds: int) -> object:
return ("etcd", endpoints, key_prefix, lease_ttl_seconds)
Expand Down
Loading
Loading