mod ipc;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use connectrpc::client::ClientTransport;
use datafusion::execution::memory_pool::FairSpillPool;
use datafusion::execution::runtime_env::RuntimeEnvBuilder;
use datafusion::execution::session_state::SessionStateBuilder;
use futures::{Stream, StreamExt as _};
use polyc_crypto::signing_role::{RoleTrustSet, TurnReadRole};
use polyc_query_model::{
DataFrame, ErrorClass, QueryOutcome, QueryRequest, ResultFrame, SchemaFrame, TerminalFrame,
Truncation,
};
use polyc_state::id::{Audience, NamespaceId, OwnerId};
use polyc_state::immutable::AtRestProtection;
use polyc_state::projection::artifact::{ManifestTrust, ObjectNamespace};
use polyc_state::query_audit::{QueryId, RequesterId};
use polyc_state_connect::journal::client::JournalClient;
use polyc_state_connect::projection::client::ProjectionCatalogClient;
use polyc_state_connect::query_audit::client::QueryAuditClient;
use polyc_state_connect::wire::DeclaredCall;
use polyc_storage_gcs::GcsReadClient;
use crate::authority::{PrincipalError, Scoping};
use crate::core_execution::{
CoreArtifactAuthority, CoreExecutionAdmission, CoreExecutionAdmissionInput, CoreExecutionError,
CoreResultStream, classify_error, classify_resolution,
};
use crate::core_production::{
ConnectCoreMetadata, FleetGcsSource, GcsReadNamespace, VisibleGcsSource, fleet_gcs_artifacts,
visible_gcs_artifacts,
};
use crate::core_resolution::{
CatalogCompiler, CoreAuditContext, CoreConsistency, CoreMetadataAuthority, CoreParameter,
CorePlanOutcome, CorePlanningAuthority, CoreQueryRequest, CoreRequestedBounds,
CoreResolutionError, ProjectedCorePolicy, ProjectedCorePolicyInput,
};
use crate::credential::{CredentialAuthority, CredentialWitness, SystemUnixClock, UnixClock};
use crate::engine::QueryLimits;
pub use crate::credential::SessionVerification;
pub use ipc::FrameEncodeError;
use ipc::FrameEncoder;
pub struct QueryCredential(String);
impl QueryCredential {
#[must_use]
pub const fn from_bearer(token: String) -> Self {
Self(token)
}
}
impl fmt::Debug for QueryCredential {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("QueryCredential([REDACTED])")
}
}
#[derive(Clone)]
pub struct ArtifactNamespace {
pub namespace: String,
pub protection: AtRestProtection,
}
impl fmt::Debug for ArtifactNamespace {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ArtifactNamespace")
.field("protection", &self.protection)
.finish_non_exhaustive()
}
}
pub enum QueryServiceRealm {
Visible {
artifacts: GcsReadClient,
namespaces: Vec<ArtifactNamespace>,
fleet_namespaces: Vec<String>,
},
Fleet {
visible_artifacts: GcsReadClient,
visible_namespaces: Vec<ArtifactNamespace>,
fleet_artifacts: GcsReadClient,
fleet_namespaces: Vec<ArtifactNamespace>,
},
}
impl fmt::Debug for QueryServiceRealm {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Visible { .. } => "visible",
Self::Fleet { .. } => "fleet",
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct QueryServicePolicy {
pub result_release_bytes: u64,
pub response_frame_bytes: u64,
pub manifest_bytes: u64,
pub artifact_file_bytes: u64,
pub artifact_range_bytes: u64,
pub source_decode_bytes: u64,
pub max_concurrent_executions: usize,
pub revalidation_interval: Duration,
pub execution_memory_bytes: usize,
}
#[derive(Debug, thiserror::Error)]
pub enum QueryServiceError {
#[error("the query service composition is invalid")]
InvalidComposition,
#[error("the presented credential does not authorize this query")]
Unauthorized,
#[error("the query could not be planned")]
Resolution(ErrorClass),
#[error("this query identity was already recorded")]
AlreadyRecorded,
#[error("the query could not be executed")]
Execution(ErrorClass),
#[error("the result could not be encoded")]
Encode(#[source] FrameEncodeError),
#[error("the result source evidence could not be reported")]
Evidence,
}
pub struct CredentialSource {
pub persona: Arc<dyn crate::authority::PersonaSource>,
pub turn_read_trust: RoleTrustSet<TurnReadRole>,
pub sessions: Arc<dyn SessionVerification>,
}
impl fmt::Debug for CredentialSource {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CredentialSource")
.finish_non_exhaustive()
}
}
pub struct ProjectedCoreComposition<T> {
pub namespace: String,
pub projection_owner: String,
pub policy: QueryServicePolicy,
pub limits: QueryLimits,
pub realm: QueryServiceRealm,
pub trust: Arc<dyn ManifestTrust>,
pub journal: JournalClient<T>,
pub projections: ProjectionCatalogClient<T>,
pub audit: QueryAuditClient<T>,
}
impl<T> fmt::Debug for ProjectedCoreComposition<T> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ProjectedCoreComposition")
.field("policy", &self.policy)
.field("realm", &self.realm)
.finish_non_exhaustive()
}
}
pub struct ProjectedCoreService {
credential: Arc<CredentialAuthority>,
clock: Arc<dyn UnixClock>,
planning: CorePlanningAuthority,
compiler: CatalogCompiler,
realm: ComposedRealm,
trust: Arc<dyn ManifestTrust>,
admission: Arc<CoreExecutionAdmission>,
limits: QueryLimits,
revalidation_interval: Duration,
}
impl fmt::Debug for ProjectedCoreService {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ProjectedCoreService")
.field("realm", &self.realm.kind())
.field("revalidation_interval", &self.revalidation_interval)
.finish_non_exhaustive()
}
}
enum ComposedRealm {
Visible {
artifacts: GcsReadClient,
namespaces: Vec<GcsReadNamespace>,
fleet_namespaces: Vec<ObjectNamespace>,
},
Fleet {
visible_artifacts: GcsReadClient,
visible_namespaces: Vec<GcsReadNamespace>,
fleet_artifacts: GcsReadClient,
fleet_namespaces: Vec<GcsReadNamespace>,
},
}
impl ComposedRealm {
const fn kind(&self) -> &'static str {
match self {
Self::Visible { .. } => "visible",
Self::Fleet { .. } => "fleet",
}
}
fn authority(
&self,
trust: Arc<dyn ManifestTrust>,
witness: Arc<CredentialWitness>,
revalidation_interval: Duration,
admission: Arc<CoreExecutionAdmission>,
) -> Result<CoreArtifactAuthority, CoreExecutionError> {
match self {
Self::Visible {
artifacts,
namespaces,
fleet_namespaces,
} => visible_gcs_artifacts(
VisibleGcsSource::new(artifacts.clone(), namespaces.clone()),
fleet_namespaces.clone(),
trust,
witness,
revalidation_interval,
admission,
),
Self::Fleet {
visible_artifacts,
visible_namespaces,
fleet_artifacts,
fleet_namespaces,
} => fleet_gcs_artifacts(
VisibleGcsSource::new(visible_artifacts.clone(), visible_namespaces.clone()),
FleetGcsSource::new(fleet_artifacts.clone(), fleet_namespaces.clone()),
trust,
witness,
revalidation_interval,
admission,
),
}
}
}
fn read_namespaces(
namespaces: Vec<ArtifactNamespace>,
) -> Result<Vec<GcsReadNamespace>, QueryServiceError> {
if namespaces.is_empty() {
return Err(QueryServiceError::InvalidComposition);
}
namespaces
.into_iter()
.map(|entry| {
ObjectNamespace::try_new(entry.namespace)
.map(|namespace| GcsReadNamespace::new(namespace, entry.protection))
.map_err(|_invalid| QueryServiceError::InvalidComposition)
})
.collect()
}
fn topology_namespaces(namespaces: Vec<String>) -> Result<Vec<ObjectNamespace>, QueryServiceError> {
namespaces
.into_iter()
.map(|namespace| {
ObjectNamespace::try_new(namespace)
.map_err(|_invalid| QueryServiceError::InvalidComposition)
})
.collect()
}
impl ProjectedCoreService {
pub fn try_new<T>(
credential: CredentialSource,
composition: ProjectedCoreComposition<T>,
) -> Result<Self, QueryServiceError>
where
T: ClientTransport + Send + Sync + 'static,
<T::ResponseBody as connectrpc::http_body::Body>::Error: fmt::Display,
{
let ProjectedCoreComposition {
namespace,
projection_owner,
policy,
limits,
realm,
trust,
journal,
projections,
audit,
} = composition;
let namespace = namespace.as_str();
let projection_owner = projection_owner.as_str();
let core_policy = ProjectedCorePolicy::try_from(ProjectedCorePolicyInput {
result_release_bytes: policy.result_release_bytes,
response_frame_bytes: policy.response_frame_bytes,
manifest_bytes: policy.manifest_bytes,
artifact_file_bytes: policy.artifact_file_bytes,
artifact_range_bytes: policy.artifact_range_bytes,
source_decode_bytes: policy.source_decode_bytes,
})
.map_err(|_invalid| QueryServiceError::InvalidComposition)?;
let metadata: Arc<dyn CoreMetadataAuthority> =
Arc::new(ConnectCoreMetadata::new(journal, projections, audit));
let planning = CorePlanningAuthority::new(
NamespaceId::new(namespace),
OwnerId::new(projection_owner),
core_policy,
metadata,
)
.map_err(|_invalid| QueryServiceError::InvalidComposition)?;
let realm = match realm {
QueryServiceRealm::Visible {
artifacts,
namespaces,
fleet_namespaces,
} => ComposedRealm::Visible {
artifacts,
namespaces: read_namespaces(namespaces)?,
fleet_namespaces: topology_namespaces(fleet_namespaces)?,
},
QueryServiceRealm::Fleet {
visible_artifacts,
visible_namespaces,
fleet_artifacts,
fleet_namespaces,
} => ComposedRealm::Fleet {
visible_artifacts,
visible_namespaces: read_namespaces(visible_namespaces)?,
fleet_artifacts,
fleet_namespaces: read_namespaces(fleet_namespaces)?,
},
};
let admission = Arc::new(
CoreExecutionAdmission::try_from(CoreExecutionAdmissionInput {
max_concurrent_executions: policy.max_concurrent_executions,
})
.map_err(|_invalid| QueryServiceError::InvalidComposition)?,
);
if policy.revalidation_interval.is_zero() || policy.execution_memory_bytes == 0 {
return Err(QueryServiceError::InvalidComposition);
}
let runtime = RuntimeEnvBuilder::new()
.with_memory_pool(Arc::new(FairSpillPool::new(policy.execution_memory_bytes)))
.build_arc()
.map_err(|_datafusion| QueryServiceError::InvalidComposition)?;
let session = SessionStateBuilder::new()
.with_runtime_env(runtime)
.with_default_features()
.build();
Ok(Self {
credential: Arc::new(CredentialAuthority::verifying(
credential.persona,
credential.turn_read_trust,
credential.sessions,
)),
clock: Arc::new(SystemUnixClock),
planning,
compiler: CatalogCompiler::new(session),
realm,
trust,
admission,
limits,
revalidation_interval: policy.revalidation_interval,
})
}
pub async fn start_projected(
&self,
credential: QueryCredential,
query_id: &str,
request: &QueryRequest,
) -> Result<ProjectedResultStream, QueryServiceError> {
let (witness, scoping) = CredentialWitness::admit_bearer(
credential.0,
Arc::clone(&self.credential),
Arc::clone(&self.clock),
)
.await
.map_err(refusal)?;
let requester = requester_of(&scoping)?;
let caller_bounds = requested_bounds(request);
let bounds = self
.planning
.effective_bounds(&self.limits, caller_bounds)
.map_err(|error| resolution_refusal(&error))?;
let declared = DeclaredCall::live(Audience::new("state"), bounds.timeout());
let audit =
CoreAuditContext::from_scoped(QueryId::new(query_id), requester, &declared, bounds);
let core_request = CoreQueryRequest::new(
request.sql().to_owned(),
request.parameters().iter().map(parameter_of).collect(),
consistency_of(request),
caller_bounds,
);
let prepared = match self
.planning
.plan(
&self.compiler,
&self.limits,
&scoping.scope,
scoping.allow_explain,
audit,
core_request,
)
.await
.map_err(|error| resolution_refusal(&error))?
{
CorePlanOutcome::Granted(prepared) => *prepared,
CorePlanOutcome::AlreadyRecorded(_) => {
return Err(QueryServiceError::AlreadyRecorded);
}
};
let artifacts = self
.realm
.authority(
Arc::clone(&self.trust),
Arc::new(witness),
self.revalidation_interval,
Arc::clone(&self.admission),
)
.map_err(|error| execution_refusal(&error))?;
let bound = artifacts.bind(prepared).await;
drop(artifacts);
let rows = bound.map_err(|error| execution_refusal(&error))?.execute();
ProjectedResultStream::start(
rows,
bounds.response_frame_bytes(),
bounds.result_release_bytes(),
)
}
}
fn resolution_refusal(error: &CoreResolutionError) -> QueryServiceError {
tracing::warn!(?error, "the projected read path could not resolve a query");
QueryServiceError::Resolution(crate::core_evidence::error_class_of(classify_resolution(
error,
)))
}
fn execution_refusal(error: &CoreExecutionError) -> QueryServiceError {
tracing::warn!(%error, source = ?std::error::Error::source(error), "the projected read path could not execute a query");
QueryServiceError::Execution(crate::core_evidence::error_class_of(classify_error(error)))
}
const fn refusal(_error: PrincipalError) -> QueryServiceError {
QueryServiceError::Unauthorized
}
fn requester_of(scoping: &Scoping) -> Result<RequesterId, QueryServiceError> {
scoping.caller_identity.as_ref().map_or_else(
|| {
scoping
.conversation_id
.as_ref()
.map_or(Err(QueryServiceError::Unauthorized), |conversation| {
Ok(RequesterId::new(format!("conversation:{conversation}")))
})
},
|persona| Ok(RequesterId::new(format!("persona:{persona}"))),
)
}
const fn consistency_of(request: &QueryRequest) -> CoreConsistency {
match request.consistency() {
polyc_query_model::Consistency::Projected => CoreConsistency::Projected,
polyc_query_model::Consistency::RequireProjectedThrough(position) => {
CoreConsistency::RequireProjectedThrough(polyc_state::revision::JournalPosition::new(
position,
))
}
}
}
fn parameter_of(parameter: &polyc_query_model::Parameter) -> CoreParameter {
match parameter {
polyc_query_model::Parameter::Utf8(value) => CoreParameter::Utf8(value.clone()),
polyc_query_model::Parameter::UInt64(value) => CoreParameter::UInt64(*value),
polyc_query_model::Parameter::Boolean(value) => CoreParameter::Boolean(*value),
polyc_query_model::Parameter::Null => CoreParameter::Null,
}
}
const fn requested_bounds(request: &QueryRequest) -> CoreRequestedBounds {
let bounds = request.bounds();
CoreRequestedBounds::from_requested(
bounds.timeout(),
bounds.rows(),
bounds.result_bytes(),
bounds.frame_bytes(),
)
}
pub struct ProjectedResultStream {
rows: CoreResultStream,
encoder: FrameEncoder,
queued: Option<ResultFrame>,
stage: Stage,
released_rows: u64,
released_bytes: u64,
release_ceiling: u64,
byte_truncated: bool,
}
impl fmt::Debug for ProjectedResultStream {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ProjectedResultStream")
.field("stage", &self.stage)
.field("queued", &self.queued.is_some())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Stage {
Streaming,
Terminated,
}
impl ProjectedResultStream {
pub(crate) fn start(
rows: CoreResultStream,
frame_ceiling: u64,
release_ceiling: u64,
) -> Result<Self, QueryServiceError> {
let Ok(ceiling) = usize::try_from(frame_ceiling) else {
let _ = rows.report_failure(polyc_state::query_audit::ErrorClass::Internal);
return Err(QueryServiceError::InvalidComposition);
};
let (encoder, schema) = match FrameEncoder::start(rows.schema().clone(), ceiling) {
Ok(started) => started,
Err(error) => {
let class = error.class();
tracing::warn!(%error, ?class, "the result schema does not fit the frame ceiling");
let _ = rows.report_failure(crate::core_evidence::durable_class_of(class));
return Err(QueryServiceError::Encode(error));
}
};
rows.account_at_consumer();
Ok(Self {
rows,
encoder,
queued: Some(ResultFrame::Schema(schema)),
stage: Stage::Streaming,
released_rows: 0,
released_bytes: 0,
release_ceiling,
byte_truncated: false,
})
}
fn release_terminal(
&mut self,
outcome: QueryOutcome,
) -> Result<ResultFrame, QueryServiceError> {
self.stage = Stage::Terminated;
self.terminal(outcome)
}
fn terminal(&self, outcome: QueryOutcome) -> Result<ResultFrame, QueryServiceError> {
let evidence = crate::core_evidence::evidence_of(self.rows.source())
.map_err(|_unrepresentable| QueryServiceError::Evidence)?;
let truncation = if self.byte_truncated || self.rows.delivered().truncated() {
Truncation::TruncatedAt(self.released_rows)
} else {
Truncation::Complete
};
Ok(ResultFrame::Terminal(TerminalFrame::new(
outcome,
self.rows.elapsed(),
self.released_rows,
self.released_bytes,
truncation,
evidence,
)))
}
fn settle_at_release_ceiling(&mut self) -> Option<Result<ResultFrame, QueryServiceError>> {
let settled = self.rows.settle_consumer_bound()?;
self.byte_truncated = true;
debug_assert_eq!(settled.rows(), self.released_rows);
Some(self.release_terminal(QueryOutcome::Succeeded))
}
fn release_next_data_frame(&mut self) -> Option<Result<ResultFrame, QueryServiceError>> {
let data = match self.encoder.next() {
Ok(Some(data)) => data,
Ok(None) => return None,
Err(error) => {
let class = error.class();
let selected = self
.rows
.report_failure(crate::core_evidence::durable_class_of(class));
tracing::warn!(%error, ?class, "a released batch could not be framed");
if selected {
return Some(self.release_terminal(QueryOutcome::Failed(class)));
}
self.encoder.discard();
return None;
}
};
let bytes = data.arrow_ipc().len() as u64;
let rows = data.rows();
let total = self.released_bytes.saturating_add(bytes);
if total > self.release_ceiling {
let settled = self.settle_at_release_ceiling();
if settled.is_none() {
self.encoder.discard();
}
return settled;
}
if !self.rows.admit_release(rows, bytes) {
self.encoder.discard();
return None;
}
self.released_rows = self.released_rows.saturating_add(rows);
self.released_bytes = total;
Some(Ok(ResultFrame::Data(data)))
}
pub async fn next_frame(&mut self) -> Option<Result<ResultFrame, QueryServiceError>> {
loop {
if let Some(frame) = self.queued.take() {
return Some(Ok(frame));
}
if self.stage == Stage::Terminated {
return None;
}
if self.encoder.has_rows() {
if let Some(frame) = self.release_next_data_frame() {
return Some(frame);
}
continue;
}
match self.rows.next().await {
Some(Ok(batch)) => self.encoder.begin(batch),
Some(Err(error)) => {
let outcome = QueryOutcome::Failed(crate::core_evidence::error_class_of(
classify_error(&error),
));
return Some(self.release_terminal(outcome));
}
None => {
return Some(self.release_terminal(QueryOutcome::Succeeded));
}
}
}
}
#[cfg(test)]
pub(crate) const fn frames_built(&self) -> u64 {
self.encoder.built()
}
#[cfg(test)]
pub(crate) fn request_buffered_batch(&mut self) {
self.rows.request_buffered_batch();
}
#[cfg(test)]
pub(crate) fn buffered_frames(&self) -> usize {
self.rows.buffered_frames()
}
#[cfg(test)]
pub(crate) fn terminal_selected(&self) -> bool {
self.rows.terminal_selected()
}
}
#[derive(Debug)]
pub struct MaterializedResult {
frames: Vec<DataFrame>,
schema: SchemaFrame,
terminal: TerminalFrame,
}
impl MaterializedResult {
#[must_use]
pub const fn schema(&self) -> &SchemaFrame {
&self.schema
}
#[must_use]
pub fn frames(&self) -> &[DataFrame] {
&self.frames
}
#[must_use]
pub const fn terminal(&self) -> &TerminalFrame {
&self.terminal
}
}
impl ProjectedResultStream {
pub async fn materialize(mut self) -> Result<MaterializedResult, QueryServiceError> {
let mut schema = None;
let mut frames = Vec::new();
let mut terminal = None;
while let Some(frame) = self.next_frame().await {
match frame? {
ResultFrame::Schema(value) => schema = Some(value),
ResultFrame::Data(value) => frames.push(value),
ResultFrame::Terminal(value) => terminal = Some(value),
}
}
match (schema, terminal) {
(Some(schema), Some(terminal)) => Ok(MaterializedResult {
frames,
schema,
terminal,
}),
_ => Err(QueryServiceError::Evidence),
}
}
}
pub fn frames(
stream: ProjectedResultStream,
) -> impl Stream<Item = Result<ResultFrame, QueryServiceError>> {
futures::stream::unfold(Some(stream), |state| async move {
let mut stream = state?;
let frame = stream.next_frame().await?;
Some((frame, Some(stream)))
})
}