blob: cf5c2e5a57f8d203416d79603d679fe2656fec9a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Bounded native observations, owned by the original renderer resource.
//! Queue admission transfers custody; only the original logical commit confirms
//! publication. Candidate rejection retains the exact observation and its ACK.
use crate::{
Error, Result,
backend::Job,
budget::{Kind, NativeBudget, Permit},
};
use serde_json::Value;
use std::{
cell::RefCell,
collections::{BTreeMap, VecDeque},
future::Future,
pin::Pin,
rc::Rc,
task::{Context, Poll, Wake, Waker},
};
use tokio::sync::oneshot;
const MAX_PENDING: usize = 128;
struct Observation {
key: String,
value: Value,
replaceable: bool,
input: bool,
rejected: Option<(u64, u64)>,
_bytes: Permit,
acknowledgement: oneshot::Sender<Job<()>>,
}
struct State {
reserved: BTreeMap<String, VecDeque<Permit>>,
pending: VecDeque<Observation>,
publishing: Option<(usize, (u64, u64), Job<()>)>,
closed: bool,
}
/// Capacity prepared by a trusted immutable recipe before its native commands.
/// Dropping a failed candidate releases it without touching the committed queue.
pub struct Capacity(Vec<(String, Permit)>);
struct Notification(std::sync::Arc<dyn Fn() + Send + Sync>);
impl Wake for Notification {
fn wake(self: std::sync::Arc<Self>) {
(self.0)();
}
fn wake_by_ref(self: &std::sync::Arc<Self>) {
(self.0)();
}
}
#[derive(Clone)]
pub struct Observations {
state: Rc<RefCell<State>>,
budget: NativeBudget,
wake: Waker,
}
impl Observations {
pub fn new(budget: NativeBudget, notify: impl Fn() + Send + Sync + 'static) -> Self {
let wake = Waker::from(std::sync::Arc::new(Notification(std::sync::Arc::new(
notify,
))));
Self {
state: Rc::new(RefCell::new(State {
reserved: BTreeMap::new(),
pending: VecDeque::new(),
publishing: None,
closed: false,
})),
budget,
wake,
}
}
pub fn prepare(
&self,
requirements: impl IntoIterator<Item = (&'static str, usize)>,
) -> Result<Capacity> {
let state = self.state.borrow();
if state.closed {
return Err(Error::Invalid(
"original native publication owner closed".into(),
));
}
let mut required = BTreeMap::<&str, (usize, usize)>::new();
for (key, bytes) in requirements {
let capacity = required.entry(key).or_default();
capacity.0 += 1;
capacity.1 = capacity.1.max(bytes);
}
let mut reserved = Vec::new();
let mut retained_total: usize = state.reserved.values().map(VecDeque::len).sum();
for (key, (slots, bytes)) in required {
let retained = state.reserved.get(key).map_or(0, VecDeque::len);
let extra = slots.saturating_sub(retained);
retained_total += extra;
if state.pending.len() + retained_total > MAX_PENDING {
return Err(Error::Invalid(
"native publication capacity exhausted before command".into(),
));
}
for _ in 0..extra {
reserved.push((
key.to_owned(),
self.budget
.reserve(Kind::Encoded, bytes)
.map_err(|e| Error::Invalid(e.to_string()))?,
));
}
}
Ok(Capacity(reserved))
}
pub fn adopt(&self, capacity: Capacity) {
let mut state = self.state.borrow_mut();
assert!(!state.closed, "original native publication owner");
for (key, permit) in capacity.0 {
state.reserved.entry(key).or_default().push_back(permit);
}
}
/// Capture at the native effect boundary, before a later command can change
/// its receipt. Identical evidence already in custody needs no second ACK.
pub fn capture(&self, key: &str, value: Value, replaceable: bool) -> Result<Option<Job<()>>> {
self.enqueue(key, value, replaceable, false)
}
/// Independent user intents keep their original order and ACK even when
/// payloads match. They follow observations already captured by this owner.
pub fn capture_input(&self, key: &str, value: Value) -> Result<Job<()>> {
self.enqueue(key, value, false, true)?
.ok_or_else(|| Error::Invalid("original native input owner retired".into()))
}
fn enqueue(
&self,
key: &str,
value: Value,
replaceable: bool,
input: bool,
) -> Result<Option<Job<()>>> {
let mut state = self.state.borrow_mut();
if state.closed {
return Err(Error::Invalid(
"original native publication owner closed".into(),
));
}
// Repeated producer polls do not add a second copy while other keys
// interleave. User intents are never evidence deduplication candidates.
if !input
&& state
.pending
.iter()
.rev()
.find(|entry| entry.key == key && !entry.input)
.is_some_and(|entry| entry.value == value)
{
return Ok(None);
}
let bytes = serde_json::to_vec(&value)
.map_err(|e| Error::Invalid(e.to_string()))?
.len();
let mut permit = if !input
&& !replaceable
&& state
.reserved
.get(key)
.is_some_and(|slots| !slots.is_empty())
{
state.reserved.get_mut(key).unwrap().pop_front().unwrap()
} else {
self.budget
.reserve(Kind::Encoded, bytes)
.map_err(|e| Error::Invalid(e.to_string()))?
};
permit.shrink_to(bytes);
if replaceable
&& state
.pending
.back()
.is_some_and(|entry| entry.key == key && entry.replaceable && !entry.input)
&& state
.publishing
.as_ref()
.is_none_or(|(index, _, _)| *index + 1 != state.pending.len())
{
if let Some(entry) = state.pending.pop_back() {
let _ = entry.acknowledgement.send(Job::ready(Err(Error::Invalid(
"replaceable native observation superseded".into(),
))));
}
} else if state.pending.len() + state.reserved.values().map(VecDeque::len).sum::<usize>()
>= MAX_PENDING
{
return Err(Error::Invalid(
"native publication capacity exhausted".into(),
));
}
let (acknowledgement, result) = oneshot::channel();
state.pending.push_back(Observation {
key: key.to_owned(),
value,
replaceable,
input,
rejected: None,
_bytes: permit,
acknowledgement,
});
drop(state);
self.wake.wake_by_ref();
Ok(Some(Job::new(async move {
result
.await
.map_err(|_| Error::Invalid("original native publication owner stopped".into()))?
.await
})))
}
/// This original owner publishes one ordered item at a time. A rejected
/// observation remains in custody until a real business/source basis changes;
/// rejected user intents finish with their original error and never replay.
pub fn advance<C>(
&self,
revision: (u64, u64),
context: &mut C,
mut send: impl FnMut(&str, Value, &mut C) -> Result<Job<()>>,
mut committed: impl FnMut(&str, &Value, &mut C),
) -> Vec<Error> {
let mut state = self.state.borrow_mut();
if state.closed {
return Vec::new();
}
let mut cx = Context::from_waker(&self.wake);
let mut errors = Vec::new();
if let Some((index, basis, mut job)) = state.publishing.take() {
match Pin::new(&mut job).poll(&mut cx) {
Poll::Pending => state.publishing = Some((index, basis, job)),
Poll::Ready(Ok(())) => {
let entry = state
.pending
.remove(index)
.expect("original publishing observation");
committed(&entry.key, &entry.value, context);
let _ = entry.acknowledgement.send(Job::ready(Ok(())));
}
Poll::Ready(Err(error)) => {
Self::reject(&mut state, index, basis, &error);
errors.push(error);
}
}
}
if state.publishing.is_none()
&& let Some(index) = Self::eligible(&state, revision)
{
let entry = &state.pending[index];
match send(&entry.key, entry.value.clone(), context) {
Ok(mut job) => match Pin::new(&mut job).poll(&mut cx) {
Poll::Pending => state.publishing = Some((index, revision, job)),
Poll::Ready(Ok(())) => {
let entry = state
.pending
.remove(index)
.expect("original publishing observation");
committed(&entry.key, &entry.value, context);
let _ = entry.acknowledgement.send(Job::ready(Ok(())));
}
Poll::Ready(Err(error)) => {
Self::reject(&mut state, index, revision, &error);
errors.push(error);
}
},
Err(error) => {
Self::reject(&mut state, index, revision, &error);
errors.push(error);
}
}
}
if state.publishing.is_none() && Self::eligible(&state, revision).is_some() {
self.wake.wake_by_ref();
}
errors
}
fn eligible(state: &State, revision: (u64, u64)) -> Option<usize> {
state.pending.iter().enumerate().position(|(index, entry)| {
entry.rejected != Some(revision)
&& (entry.input
|| !state
.pending
.iter()
.take(index)
.any(|prior| !prior.input && prior.key == entry.key))
})
}
fn reject(state: &mut State, index: usize, basis: (u64, u64), error: &Error) {
if state.pending[index].input {
let entry = state
.pending
.remove(index)
.expect("original rejected input");
let _ = entry
.acknowledgement
.send(Job::ready(Self::rejection(error)));
return;
}
// A first attempt fences the inputs already captured behind its
// unpublished evidence. Later independent inputs may run while this
// exact rejected observation remains retained for a changed basis.
let first_attempt = state.pending[index].rejected.is_none();
state.pending[index].rejected = Some(basis);
if !first_attempt {
return;
}
let mut retained = VecDeque::new();
let mut position = 0;
while let Some(entry) = state.pending.pop_front() {
if position > index && entry.input {
let _ = entry
.acknowledgement
.send(Job::ready(Self::rejection(error)));
} else {
retained.push_back(entry);
}
position += 1;
}
state.pending = retained;
}
fn rejection(error: &Error) -> Result<()> {
Err(
if matches!(error, Error::Core(maka_plugin_core::Error::Retired)) {
maka_plugin_core::Error::Retired.into()
} else {
Error::Invalid(error.to_string())
},
)
}
/// A removed declaration cannot reinterpret the accepted input on a new
/// control. Resolve its original custody with retirement, never fake success.
pub fn retain(&self, mut declared: impl FnMut(&str) -> bool) {
let mut state = self.state.borrow_mut();
let mut publishing = state.publishing.take();
let mut next_publishing = None;
let mut retained = VecDeque::new();
for (index, entry) in state.pending.drain(..).enumerate() {
if declared(&entry.key) {
if publishing
.as_ref()
.is_some_and(|(current, _, _)| *current == index)
{
next_publishing = Some(retained.len());
}
retained.push_back(entry);
} else {
Self::acknowledge_removed(entry, index, &mut publishing);
}
}
state.pending = retained;
if let (Some((_, basis, job)), Some(index)) = (publishing, next_publishing) {
state.publishing = Some((index, basis, job));
}
state.reserved.retain(|key, _| declared(key));
}
pub fn close(&self) {
let mut state = self.state.borrow_mut();
state.closed = true;
let mut publishing = state.publishing.take();
for (index, entry) in state.pending.drain(..).enumerate() {
Self::acknowledge_removed(entry, index, &mut publishing);
}
state.reserved.clear();
}
fn acknowledge_removed(
entry: Observation,
index: usize,
publishing: &mut Option<(usize, (u64, u64), Job<()>)>,
) {
let job = if publishing
.as_ref()
.is_some_and(|(current, _, _)| *current == index)
{
// Native ownership can close before the already accepted logical
// transaction settles. Its original ACK waiter takes custody of the
// same job; no retired native resource is published or reactivated.
publishing.take().expect("original removed publisher").2
} else {
Job::ready(Err(maka_plugin_core::Error::Retired.into()))
};
let _ = entry.acknowledgement.send(job);
}
pub fn contains(&self, key: &str) -> bool {
self.state
.borrow()
.pending
.iter()
.any(|entry| entry.key == key)
}
pub fn pending(&self) -> bool {
!self.state.borrow().pending.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn complete_native_publications_keep_original_acknowledgements_and_release_budget() {
let budget = NativeBudget::default();
let observations = Observations::new(budget.clone(), || {});
let text = format!(
"PUBLICATION START{}PUBLICATION END",
"保留😀z".repeat(100_000)
);
let value = serde_json::json!({"text":text});
let bytes = serde_json::to_vec(&value).unwrap().len();
assert!(bytes > 1024 * 1024);
let capacity = observations.prepare([("observed", bytes)]);
let prepared = capacity.is_ok();
if let Ok(capacity) = capacity {
observations.adopt(capacity);
}
let captured = observations.capture("observed", value.clone(), false);
let input = observations.capture_input("physical", value.clone());
let mut committed = Vec::new();
let mut errors = Vec::new();
for _ in 0..2 {
errors.extend(observations.advance(
(1, 1),
&mut committed,
|_, _, _| Ok(Job::ready(Ok(()))),
|key, value, committed| committed.push((key.to_owned(), value.clone())),
));
}
observations.close();
assert_eq!(budget.usage(), (0, 0, 0));
assert!(
prepared,
"complete observation must reserve its actual native budget"
);
let acknowledgement = captured
.expect("complete observation must enqueue")
.expect("original observation must retain an acknowledgement");
acknowledgement.await.unwrap();
input
.expect("complete original physical input must enqueue")
.await
.unwrap();
assert!(errors.is_empty());
assert_eq!(committed.len(), 2);
assert_eq!(committed[0].0, "observed");
assert_eq!(committed[1].0, "physical");
assert!(
committed.iter().all(|(_, actual)| actual == &value),
"complete original native publication bytes changed"
);
assert!(!observations.pending());
}
}