feat(nix-bindings-expr): add RecoverableError for non-memoized primop errors
Nix 2.34 memoizes primop errors by default. RecoverableError uses NIX_ERR_RECOVERABLE so transient errors allow the thunk to be retried.
This commit is contained in:
parent
cf0e8fff6a
commit
fbf5fab083
4 changed files with 126 additions and 2 deletions
|
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- `primop::RecoverableError` for primop errors that should not be memoized in the thunk, allowing retry on next force. Required by Nix >= 2.34 ([release note](https://nix.dev/manual/nix/2.34/release-notes/rl-2.34.html#c-api-changes)) for recoverable errors to remain recoverable, as Nix 2.34 memoizes errors by default.
|
||||
|
||||
## [0.2.0] - 2026-01-13
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ use nix_bindings_util::nix_version::emit_version_cfg;
|
|||
|
||||
fn main() {
|
||||
let nix_version = pkg_config::probe_library("nix-expr-c").unwrap().version;
|
||||
emit_version_cfg(&nix_version, &["2.26"]);
|
||||
emit_version_cfg(&nix_version, &["2.26", "2.34.0pre"]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2845,4 +2845,81 @@ mod tests {
|
|||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(nix_at_least = "2.34.0pre")]
|
||||
fn eval_state_primop_recoverable_error() {
|
||||
gc_registering_current_thread(|| {
|
||||
let store = Store::open(None, []).unwrap();
|
||||
let mut es = EvalState::new(store, []).unwrap();
|
||||
|
||||
let call_count = std::cell::Cell::new(0u32);
|
||||
let v = es
|
||||
.new_value_thunk(
|
||||
"recoverable_test",
|
||||
Box::new(move |es: &mut EvalState| {
|
||||
let count = call_count.get();
|
||||
call_count.set(count + 1);
|
||||
if count == 0 {
|
||||
Err(primop::RecoverableError::new("transient failure").into())
|
||||
} else {
|
||||
es.new_value_int(42)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// First force should fail with the recoverable error
|
||||
let r = es.force(&v);
|
||||
assert!(r.is_err());
|
||||
assert!(
|
||||
r.unwrap_err().to_string().contains("transient failure"),
|
||||
"Error message should contain 'transient failure'"
|
||||
);
|
||||
|
||||
// Second force should succeed because the error was recoverable
|
||||
es.force(&v).unwrap();
|
||||
let i = es.require_int(&v).unwrap();
|
||||
assert_eq!(i, 42);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(nix_at_least = "2.34.0pre")]
|
||||
fn eval_state_primop_recoverable_error_in_chain() {
|
||||
gc_registering_current_thread(|| {
|
||||
let store = Store::open(None, []).unwrap();
|
||||
let mut es = EvalState::new(store, []).unwrap();
|
||||
|
||||
let call_count = std::cell::Cell::new(0u32);
|
||||
let v = es
|
||||
.new_value_thunk(
|
||||
"recoverable_chain_test",
|
||||
Box::new(move |es: &mut EvalState| {
|
||||
let count = call_count.get();
|
||||
call_count.set(count + 1);
|
||||
if count == 0 {
|
||||
// Wrap RecoverableError in .context(), pushing it down the chain
|
||||
use anyhow::Context;
|
||||
Err(primop::RecoverableError::new("transient failure"))
|
||||
.context("wrapper context")?
|
||||
} else {
|
||||
es.new_value_int(42)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// First force should fail
|
||||
let r = es.force(&v);
|
||||
assert!(r.is_err());
|
||||
|
||||
// Second force should succeed if RecoverableError is found in the chain
|
||||
es.force(&v).unwrap();
|
||||
let i = es.require_int(&v).unwrap();
|
||||
assert_eq!(i, 42);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,35 @@ use std::ffi::{c_int, c_void, CStr, CString};
|
|||
use std::mem::ManuallyDrop;
|
||||
use std::ptr::{null, null_mut};
|
||||
|
||||
/// A primop error that is not memoized in the thunk that triggered it,
|
||||
/// allowing the thunk to be forced again.
|
||||
///
|
||||
/// Since [Nix 2.34](https://nix.dev/manual/nix/2.34/release-notes/rl-2.34.html#c-api-changes),
|
||||
/// primop errors are memoized by default: once a thunk fails, forcing it
|
||||
/// again returns the same error. Use `RecoverableError` for errors that
|
||||
/// are transient, so the caller can retry.
|
||||
///
|
||||
/// On Nix < 2.34, all errors are already recoverable, so this type has
|
||||
/// no additional effect.
|
||||
///
|
||||
/// Available since nix-bindings-expr 0.2.1.
|
||||
#[derive(Debug)]
|
||||
pub struct RecoverableError(String);
|
||||
|
||||
impl RecoverableError {
|
||||
pub fn new(msg: impl Into<String>) -> Self {
|
||||
RecoverableError(msg.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RecoverableError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RecoverableError {}
|
||||
|
||||
/// Metadata for a primop, used with `PrimOp::new`.
|
||||
pub struct PrimOpMeta<'a, const N: usize> {
|
||||
/// Name of the primop. Note that primops do not have to be registered as
|
||||
|
|
@ -36,6 +65,11 @@ impl Drop for PrimOp {
|
|||
}
|
||||
}
|
||||
impl PrimOp {
|
||||
/// Create a new primop with the given metadata and implementation.
|
||||
///
|
||||
/// When `f` returns an `Err`, the error is propagated to the Nix evaluator.
|
||||
/// To return a [recoverable error](RecoverableError), include it in the
|
||||
/// error chain (e.g. `Err(RecoverableError::new("...").into())`).
|
||||
pub fn new<const N: usize>(
|
||||
eval_state: &mut EvalState,
|
||||
meta: PrimOpMeta<N>,
|
||||
|
|
@ -108,13 +142,22 @@ unsafe extern "C" fn function_adapter(
|
|||
raw::copy_value(context_out, ret, v.raw_ptr());
|
||||
},
|
||||
Err(e) => unsafe {
|
||||
let err_code = error_code(&e);
|
||||
let cstr = CString::new(e.to_string()).unwrap_or_else(|_e| {
|
||||
CString::new("<rust nix-expr application error message contained null byte>")
|
||||
.unwrap()
|
||||
});
|
||||
raw_util::set_err_msg(context_out, raw_util::err_NIX_ERR_UNKNOWN, cstr.as_ptr());
|
||||
raw_util::set_err_msg(context_out, err_code, cstr.as_ptr());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn error_code(e: &anyhow::Error) -> raw_util::err {
|
||||
#[cfg(nix_at_least = "2.34.0pre")]
|
||||
if e.downcast_ref::<RecoverableError>().is_some() {
|
||||
return raw_util::err_NIX_ERR_RECOVERABLE;
|
||||
}
|
||||
raw_util::err_NIX_ERR_UNKNOWN
|
||||
}
|
||||
|
||||
static FUNCTION_ADAPTER: raw::PrimOpFun = Some(function_adapter);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue