Skip to main content

Bridge

Struct Bridge 

Source
pub struct Bridge { /* private fields */ }
Expand description

kevy-lua per-shard bridge. One Bridge lives in each shard’s runtime; it owns the per-dialect VM pool, the SHA1 cache, and the kevy-side dispatch callback that redis.call invokes.

The bridge is intentionally NOT Send / Sync — same constraint as luna-core’s Vm, which is !Send + !Sync by design. kevy’s thread-per-core model means every shard owns its bridge exclusively.

let mut bridge = kevy_lua::Bridge::with_no_dispatch();
// no shebang runs under Lua 5.1; `#!lua version=5.3` opts into 5.3
assert_eq!(bridge.eval(b"return 7 / 2", &[], &[]), b"$3\r\n3.5\r\n");
assert_eq!(bridge.eval(b"#!lua version=5.3\nreturn 7 // 2", &[], &[]), b":3\r\n");

Implementations§

Source§

impl Bridge

Source

pub fn new<F>(dispatch: F) -> Self
where F: Fn(&[&[u8]], bool) -> Vec<u8> + 'static,

Create a fresh bridge with dispatch as the host callback behind redis.call. No Vms are spawned until the first EVAL.

The dispatch closure receives the script’s argv (&[&[u8]], command name at index 0) plus a read_only flag and must return RESP reply bytes. When read_only is true the dispatcher MUST reject write commands with -READONLY can't write against a read-only script\r\n so EVAL_RO / EVALSHA_RO deliver Redis semantics. kevy-rt owns the canonical command-flag table and does this check natively in production; tests provide a stub dispatcher hard-coding a few write commands (see tests/integration.rs).

For embedders that don’t need real keyspace access (e.g. pure-computation EVAL), Bridge::with_no_dispatch installs a default that returns -ERR redis.call: no dispatch wired for every call.

// a host that answers every command with its own name
let mut b = kevy_lua::Bridge::new(|argv: &[&[u8]], _read_only: bool| {
    let name = argv[0];
    [format!("${}\r\n", name.len()).as_bytes(), name, b"\r\n"].concat()
});
assert_eq!(b.eval(b"return redis.call('ECHO')", &[], &[]), b"$4\r\nECHO\r\n");
Source

pub fn set_instr_budget(&mut self, n: i64)

Override the per-Vm instruction budget (~5 s ≈ 200 M instr by default). 0 disables the cap (unlimited execution).

Setting it does NOT affect already-spawned VMs in the pool — you can pair the call with Bridge::script_flush to force a respawn under the new budget, or leave existing VMs as-is and only catch new dialects.

let mut b = kevy_lua::Bridge::with_no_dispatch();
b.set_instr_budget(10_000);
// a runaway script is stopped and reported, not left spinning
assert!(b.eval(b"while true do end", &[], &[]).starts_with(b"-"));
Source

pub fn with_no_dispatch() -> Self

Bridge with a no-op dispatcher: every redis.call returns a RESP error. Convenience for embedders that want EVAL but don’t have the host dispatch wired yet (e.g. pure-computation scripts during early development).

let mut b = kevy_lua::Bridge::with_no_dispatch();
assert_eq!(b.eval(b"return 6 * 7", &[], &[]), b":42\r\n");
assert!(b.eval(b"return redis.call('GET', 'k')", &[], &[]).starts_with(b"-"));
Source

pub fn set_allowed_dialects(&mut self, versions: &[LuaVersion])

Restrict which Lua dialects this bridge will spawn VMs for. An EVAL with #!lua version=N for a non-allowed dialect is rejected with a -ERR reply. The 5.1 default is always accessible via scripts with no shebang regardless of this setting (you can’t disable the ecosystem-default dialect without taking a different with_allowed_dialects API).

Passing an empty slice = no restriction = all five dialects permitted (the constructor default).

use kevy_lua::{Bridge, LuaVersion};
let mut b = Bridge::with_no_dispatch();
b.set_allowed_dialects(&[LuaVersion::Lua51]);
assert!(b.eval(b"#!lua version=5.4\nreturn 1", &[], &[]).starts_with(b"-ERR"));
assert_eq!(b.eval(b"return 1", &[], &[]), b":1\r\n");
Source

pub fn eval(&mut self, script: &[u8], keys: &[&[u8]], args: &[&[u8]]) -> Reply

Run a script and marshal its first return value into a RESP reply: the #!lua version=N shebang picks the dialect (5.1 when absent), KEYS and ARGV are bound for this call, and the script enters the SHA1 cache before it runs, as Redis does. Every failure — a bad shebang, a disabled dialect, a Lua error — is a RESP error reply, never a panic.

let mut b = kevy_lua::Bridge::with_no_dispatch();
assert_eq!(b.eval(b"return 1", &[], &[]), b":1\r\n");
assert!(b.eval(b"error('boom')", &[], &[]).starts_with(b"-"));
Source

pub fn eval_ro( &mut self, script: &[u8], keys: &[&[u8]], args: &[&[u8]], ) -> Reply

Read-only variant of Bridge::eval. The dispatcher receives read_only = true for every redis.call from this script; kevy-rt rejects write commands with -READONLY can't write against a read-only script\r\n. Redis 7.0+ EVAL_RO.

All other semantics (KEYS / ARGV / SHA1 cache fill / dialect routing) are identical to eval.

// the host sees the read-only flag and refuses writes under it
let mut b = kevy_lua::Bridge::new(|argv: &[&[u8]], read_only: bool| {
    if read_only && argv[0] == b"SET" {
        b"-READONLY can't write against a read-only script\r\n".to_vec()
    } else {
        b"+OK\r\n".to_vec()
    }
});
let script = b"return redis.pcall('SET', KEYS[1], 'v')";
assert_eq!(b.eval(script, &[b"k"], &[]), b"+OK\r\n");
assert!(b.eval_ro(script, &[b"k"], &[]).starts_with(b"-READONLY"));
Source

pub fn evalsha_ro( &mut self, sha1: ScriptSha1, keys: &[&[u8]], args: &[&[u8]], ) -> Reply

Read-only variant of Bridge::evalsha. Redis 7.0+ EVALSHA_RO.

let mut b = kevy_lua::Bridge::new(|_argv: &[&[u8]], read_only: bool| {
    if read_only { b":1\r\n".to_vec() } else { b":0\r\n".to_vec() }
});
let sha = b.script_load(b"return redis.call('PING')");
assert_eq!(b.evalsha_ro(sha, &[], &[]), b":1\r\n");
assert_eq!(b.evalsha(sha, &[], &[]), b":0\r\n");
Source

pub fn evalsha( &mut self, sha1: ScriptSha1, keys: &[&[u8]], args: &[&[u8]], ) -> Reply

Run a previously-cached script by SHA1 hex.

Returns -NOSCRIPT ... if the script isn’t in the cache. Identical to running eval with the cached bytes — the same shebang routing, KEYS/ARGV binding, and redis.* host plumbing apply.

let mut b = kevy_lua::Bridge::with_no_dispatch();
let sha = b.script_load(b"return ARGV[1]");
assert_eq!(b.evalsha(sha, &[], &[b"hi"]), b"$2\r\nhi\r\n");
assert!(b.evalsha([0; 20], &[], &[]).starts_with(b"-NOSCRIPT"));
Source

pub fn script_load(&mut self, script: &[u8]) -> ScriptSha1

Cache a script without running it. Returns the SHA1 digest; the operator-side wire layer hex-encodes it for the Redis SCRIPT LOAD reply.

let mut b = kevy_lua::Bridge::with_no_dispatch();
let sha = b.script_load(b"return 1");
assert_eq!(&kevy_lua::sha1::hex(&sha), b"e0e1f9fabfc9d4800c877a703b823ac0578ff8db");
Source

pub fn script_exists(&self, sha1s: &[ScriptSha1]) -> Vec<bool>

Test which of the given SHA1s are in the cache. Returns a vector with true/false for each input SHA1 in order.

let mut b = kevy_lua::Bridge::with_no_dispatch();
let loaded = b.script_load(b"return 1");
assert_eq!(b.script_exists(&[loaded, [0; 20]]), [true, false]);
Source

pub fn script_flush(&mut self, _mode: FlushMode)

Drop the SHA1 cache + all per-dialect VMs. ASYNC and SYNC are currently both implemented as synchronous; the tag is preserved for future differentiation.

let mut b = kevy_lua::Bridge::with_no_dispatch();
let sha = b.script_load(b"return 1");
b.script_flush(kevy_lua::FlushMode::default());
assert!(b.evalsha(sha, &[], &[]).starts_with(b"-NOSCRIPT"));

Trait Implementations§

Source§

impl Debug for Bridge

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Reports the bridge’s configuration and how much of it is live, without touching the VMs or the dispatch closure.

Neither can be printed: luna-core’s Vm has no Debug, and dispatch is an Rc<dyn Fn> with no identity worth showing. The VMs are reported as a count of spawned slots, which is the thing worth knowing about them from outside — whether a dialect has been used yet.

Source§

impl Default for Bridge

Source§

fn default() -> Self

Equivalent to Bridge::with_no_dispatch — the safe default for embedders that don’t have a host dispatch wired yet.

Auto Trait Implementations§

§

impl !RefUnwindSafe for Bridge

§

impl !Send for Bridge

§

impl !Sync for Bridge

§

impl !UnwindSafe for Bridge

§

impl Freeze for Bridge

§

impl Unpin for Bridge

§

impl UnsafeUnpin for Bridge

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.