use crate::content::SegmentProvider;
use crate::content::node::NodeState;
use crate::index::definition::{IndexDefinition, IndexType};
use crate::index::inventory::IndexInventory;
use crate::index::lanes::AsyncLanes;
use crate::segment::record::RecordIdentifier;
const ASYNC_REINDEX_LANE: &str = "async-reindex";
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum IndexingState {
Head,
LaneCheckpoint {
lane: String,
checkpoint: String,
},
ResetForReplay {
lane: String,
},
}
pub struct SelectedIndex {
pub definition: IndexDefinition,
pub definition_record: RecordIdentifier,
pub state_root: Option<RecordIdentifier>,
pub state: IndexingState,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum SelectionRefusal {
LuceneWithoutBinaryTextPolicy {
path: String,
},
LuceneDefinitionUnsupported {
path: String,
reason: String,
},
LuceneSynchronous {
path: String,
},
ExternalIndex {
path: String,
index_type: String,
},
NoEditor {
path: String,
index_type: String,
},
Unmodellable {
path: String,
reason: String,
},
ValuePatternNotSupported {
path: String,
},
PathFilterUnconstructable {
path: String,
reason: String,
},
HybridCounter {
path: String,
lane: String,
},
MountFragmentPresent {
path: String,
child_name: String,
},
NestedDefinition {
path: String,
},
NotADefinition {
path: String,
},
DanglingLaneCheckpoint {
path: String,
lane: String,
checkpoint: String,
},
LaneAbsent {
path: String,
lane: String,
},
LuceneParkedOnTheReindexLane {
path: String,
},
ReindexLaneInProgress {
path: String,
},
}
impl SelectionRefusal {
#[must_use]
pub fn path(&self) -> &str {
match self {
Self::LuceneWithoutBinaryTextPolicy { path }
| Self::LuceneDefinitionUnsupported { path, .. }
| Self::LuceneSynchronous { path }
| Self::ExternalIndex { path, .. }
| Self::NoEditor { path, .. }
| Self::Unmodellable { path, .. }
| Self::ValuePatternNotSupported { path }
| Self::MountFragmentPresent { path, .. }
| Self::HybridCounter { path, .. }
| Self::PathFilterUnconstructable { path, .. }
| Self::NestedDefinition { path }
| Self::NotADefinition { path }
| Self::DanglingLaneCheckpoint { path, .. }
| Self::LaneAbsent { path, .. }
| Self::LuceneParkedOnTheReindexLane { path }
| Self::ReindexLaneInProgress { path } => path,
}
}
}
impl std::fmt::Display for SelectionRefusal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LuceneWithoutBinaryTextPolicy { path } => write!(
formatter,
"{path} is a lucene definition and no binary-text policy was given; froe \
extracts no binary text, so the run needs one before it can rebuild it"
),
Self::LuceneDefinitionUnsupported { path, reason } => {
write!(formatter, "{path} cannot be rebuilt natively: {reason}")
}
Self::LuceneSynchronous { path } => write!(
formatter,
"{path} is a lucene definition with no async property, which Oak documents as \
required and provides no oracle for"
),
Self::ExternalIndex { path, index_type } => write!(
formatter,
"{path} is of type {index_type}, whose data lives outside the repository"
),
Self::NoEditor { path, index_type } => write!(
formatter,
"{path} is of type {index_type}, which Oak has no editor for"
),
Self::Unmodellable { path, reason } => {
write!(formatter, "{path} could not be read: {reason}")
}
Self::ValuePatternNotSupported { path } => write!(
formatter,
"{path} carries a valuePattern froe cannot evaluate, so its entries \
cannot be derived"
),
Self::PathFilterUnconstructable { path, reason } => write!(
formatter,
"{path} has a path filter Oak cannot construct ({reason}), so Oak's own \
cycle skips it and leaves its reindex flag set"
),
Self::HybridCounter { path, lane } => write!(
formatter,
"{path} is a counter maintained both synchronously and on lane {lane}; \
rebuilding it from either state and letting the other replay would \
double every count"
),
Self::MountFragmentPresent { path, child_name } => write!(
formatter,
"{path} carries {child_name}, a composite-store mount's index data that \
a rebuild would remove and cannot reproduce"
),
Self::NestedDefinition { path } => write!(
formatter,
"{path} is nested under a content node rather than /oak:index; Oak runs \
a child cycle whose paths are relative to that node"
),
Self::NotADefinition { path } => {
write!(formatter, "{path} is not an oak:QueryIndexDefinition")
}
Self::DanglingLaneCheckpoint {
path,
lane,
checkpoint,
} => write!(
formatter,
"{path} indexes on lane {lane}, whose checkpoint {checkpoint} no longer \
exists; rerun with --from-head to authorize a rebuild from the head"
),
Self::LaneAbsent { path, lane } => write!(
formatter,
"{path} indexes on lane {lane}, which has no state on /:async; rerun \
with --from-head to authorize a rebuild from the head"
),
Self::LuceneParkedOnTheReindexLane { path } => write!(
formatter,
"{path} is a lucene definition parked on the async-reindex lane, which no \
ordinary indexing cycle maintains; restore the lane it belongs on before \
rebuilding it"
),
Self::ReindexLaneInProgress { path } => write!(
formatter,
"{path} is parked on the async-reindex lane and that lane has a \
checkpoint, so a reindex is already in progress"
),
}
}
}
pub struct Selection {
pub selected: Vec<SelectedIndex>,
pub refused: Vec<SelectionRefusal>,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct SelectionOptions {
pub requested_paths: Vec<String>,
pub from_head: bool,
pub has_binary_text_policy: bool,
}
pub fn select(
provider: &dyn SegmentProvider,
super_root: &NodeState<'_>,
inventory: &IndexInventory,
options: &SelectionOptions,
) -> crate::Result<Selection> {
let head_root = super_root
.child_node("root")?
.ok_or_else(|| crate::Error::InvalidFormat {
details: "the super-root has no \"root\" child node".to_owned(),
})?;
let lanes = AsyncLanes::read(&head_root).map_err(index_error_to_store_error)?;
let mut selection = Selection {
selected: Vec::new(),
refused: Vec::new(),
};
for info in &inventory.indexes {
let named = options
.requested_paths
.iter()
.any(|path| path == &info.path);
if !options.requested_paths.is_empty() && !named {
continue;
}
let Some(definition) = info.definition.clone() else {
selection
.refused
.push(refuse_unmodellable(&head_root, info)?);
continue;
};
if !named && !definition.reindex.flagged {
continue;
}
if let Some(refusal) = refuse_by_shape(&definition) {
selection.refused.push(refusal);
continue;
}
if definition.index_type.as_ref() == Some(&IndexType::Lucene)
&& let Some(refusal) = refuse_lucene(&head_root, &definition, options)?
{
selection.refused.push(refusal);
continue;
}
match resolve_state(
provider,
super_root,
&head_root,
&lanes,
&definition,
options,
)? {
Ok((state, state_root)) => {
let Some(node) = descend(&head_root, &definition.path)? else {
selection.refused.push(SelectionRefusal::Unmodellable {
path: definition.path.clone(),
reason: "the definition node vanished between the listing and the \
selection"
.to_owned(),
});
continue;
};
selection.selected.push(SelectedIndex {
definition_record: node.record_identifier(),
definition,
state_root,
state,
});
}
Err(refusal) => selection.refused.push(refusal),
}
}
answer_every_named_path(&head_root, inventory, options, &mut selection)?;
Ok(selection)
}
fn answer_every_named_path(
head_root: &NodeState<'_>,
inventory: &IndexInventory,
options: &SelectionOptions,
selection: &mut Selection,
) -> crate::Result<()> {
for requested in &options.requested_paths {
if inventory.indexes.iter().any(|info| &info.path == requested) {
continue;
}
let Some(node) = descend(head_root, requested)? else {
selection.refused.push(SelectionRefusal::Unmodellable {
path: requested.clone(),
reason: "there is no node at this path".to_owned(),
});
continue;
};
let refusal = match IndexDefinition::read(&node, requested) {
Ok(definition) => {
refuse_by_shape(&definition).unwrap_or(SelectionRefusal::Unmodellable {
path: requested.clone(),
reason: "the node type index did not enumerate this definition".to_owned(),
})
}
Err(_) => SelectionRefusal::NotADefinition {
path: requested.clone(),
},
};
selection.refused.push(refusal);
}
Ok(())
}
fn refuse_unmodellable(
head_root: &NodeState<'_>,
info: &crate::index::inventory::IndexInfo,
) -> crate::Result<SelectionRefusal> {
let path = info.path.clone();
let unmodellable = |reason: String| SelectionRefusal::Unmodellable {
path: path.clone(),
reason,
};
let Some(node) = descend(head_root, &info.path)? else {
return Ok(unmodellable(
info.model_error
.clone()
.unwrap_or_else(|| "the definition node is absent".to_owned()),
));
};
match IndexDefinition::read(&node, &info.path) {
Ok(_) => Ok(unmodellable(
"the definition could not be read when the inventory was collected".to_owned(),
)),
Err(
error @ (crate::index::IndexError::RelativeFilterPath { .. }
| crate::index::IndexError::EmptyIncludeSet { .. }),
) => Ok(SelectionRefusal::PathFilterUnconstructable {
path,
reason: error.to_string(),
}),
Err(other) => Ok(unmodellable(other.to_string())),
}
}
fn refuse_lucene(
head_root: &NodeState<'_>,
definition: &IndexDefinition,
options: &SelectionOptions,
) -> crate::Result<Option<SelectionRefusal>> {
let path = definition.path.clone();
if !options.has_binary_text_policy {
return Ok(Some(SelectionRefusal::LuceneWithoutBinaryTextPolicy {
path,
}));
}
if definition.indexing_mode.synchronous {
return Ok(Some(SelectionRefusal::LuceneSynchronous { path }));
}
if definition.indexing_mode.synchronous_synonym {
return Ok(Some(SelectionRefusal::LuceneDefinitionUnsupported {
path,
reason: "it is hybrid — `async` lists `sync` beside its lane — and Oak keeps a \
synchronous :property-index for it that froe does not build"
.to_owned(),
}));
}
let Some(node) = descend(head_root, &definition.path)? else {
return Ok(Some(SelectionRefusal::Unmodellable {
path,
reason: "the definition node vanished between the listing and the selection".to_owned(),
}));
};
let mut warnings = Vec::new();
match crate::index::lucene::documents::rules::IndexingRules::read(
&node,
&definition.path,
head_root,
&mut warnings,
) {
Ok(_) => Ok(None),
Err(crate::index::IndexError::Record(error)) => Err(error),
Err(other) => Ok(Some(SelectionRefusal::LuceneDefinitionUnsupported {
path,
reason: other.to_string(),
})),
}
}
fn refuse_by_shape(definition: &IndexDefinition) -> Option<SelectionRefusal> {
let path = definition.path.clone();
if !path.starts_with("/oak:index/") || path.matches('/').count() != 2 {
return Some(SelectionRefusal::NestedDefinition { path });
}
if definition
.property
.value_pattern
.regular_expression()
.is_some()
{
return Some(SelectionRefusal::ValuePatternNotSupported { path });
}
if let Some(child_name) = definition.mount_children().first() {
return Some(SelectionRefusal::MountFragmentPresent {
path,
child_name: (*child_name).to_owned(),
});
}
match definition.index_type.as_ref() {
None => Some(SelectionRefusal::NotADefinition { path }),
Some(IndexType::Elasticsearch) => Some(SelectionRefusal::ExternalIndex {
path,
index_type: "elasticsearch".to_owned(),
}),
Some(IndexType::Disabled { .. }) => Some(SelectionRefusal::NoEditor {
path,
index_type: "disabled".to_owned(),
}),
Some(IndexType::Ordered) => Some(SelectionRefusal::NoEditor {
path,
index_type: "ordered".to_owned(),
}),
Some(IndexType::Unknown(name)) => Some(SelectionRefusal::NoEditor {
path,
index_type: name.clone(),
}),
Some(
IndexType::Property | IndexType::Reference | IndexType::Counter | IndexType::Lucene,
) => None,
}
}
fn resolve_state(
provider: &dyn SegmentProvider,
super_root: &NodeState<'_>,
head_root: &NodeState<'_>,
lanes: &AsyncLanes,
definition: &IndexDefinition,
options: &SelectionOptions,
) -> crate::Result<std::result::Result<(IndexingState, Option<RecordIdentifier>), SelectionRefusal>>
{
let _ = provider;
if definition.lane.as_deref() == Some(ASYNC_REINDEX_LANE) {
if lanes
.lane(ASYNC_REINDEX_LANE)
.and_then(|lane| lane.checkpoint.as_deref())
.is_some()
{
return Ok(Err(SelectionRefusal::ReindexLaneInProgress {
path: definition.path.clone(),
}));
}
if definition.index_type.as_ref() == Some(&IndexType::Lucene) {
return Ok(Err(SelectionRefusal::LuceneParkedOnTheReindexLane {
path: definition.path.clone(),
}));
}
return Ok(Ok((
IndexingState::Head,
Some(head_root.record_identifier()),
)));
}
let Some(lane_name) = definition.lane.as_deref() else {
return Ok(Ok((
IndexingState::Head,
Some(head_root.record_identifier()),
)));
};
if definition.indexing_mode.synchronous_synonym {
if definition.index_type.as_ref() == Some(&IndexType::Counter) {
return Ok(Err(SelectionRefusal::HybridCounter {
path: definition.path.clone(),
lane: lane_name.to_owned(),
}));
}
return Ok(Ok((
IndexingState::Head,
Some(head_root.record_identifier()),
)));
}
let lane = lanes.lane(lane_name);
let checkpoint = lane.and_then(|lane| lane.checkpoint.as_deref());
let resolved = match checkpoint {
None => None,
Some(name) => super_root
.child_node("checkpoints")?
.and_then(|checkpoints| checkpoints.child_node(name).transpose())
.transpose()?
.and_then(|node| node.child_node("root").transpose())
.transpose()?,
};
if let Some(root) = resolved {
return Ok(Ok((
IndexingState::LaneCheckpoint {
lane: lane_name.to_owned(),
checkpoint: checkpoint.unwrap_or_default().to_owned(),
},
Some(root.record_identifier()),
)));
}
if !options.from_head {
return Ok(Err(match checkpoint {
Some(name) => SelectionRefusal::DanglingLaneCheckpoint {
path: definition.path.clone(),
lane: lane_name.to_owned(),
checkpoint: name.to_owned(),
},
None => SelectionRefusal::LaneAbsent {
path: definition.path.clone(),
lane: lane_name.to_owned(),
},
}));
}
if definition.index_type.as_ref() == Some(&IndexType::Counter) {
return Ok(Ok((
IndexingState::ResetForReplay {
lane: lane_name.to_owned(),
},
None,
)));
}
if definition.index_type.as_ref() == Some(&IndexType::Lucene) {
return Ok(Ok((
IndexingState::ResetForReplay {
lane: lane_name.to_owned(),
},
None,
)));
}
Ok(Ok((
IndexingState::Head,
Some(head_root.record_identifier()),
)))
}
fn descend<'provider>(
root: &NodeState<'provider>,
path: &str,
) -> crate::Result<Option<NodeState<'provider>>> {
let mut node = *root;
for element in path.split('/').filter(|element| !element.is_empty()) {
match node.child_node(element)? {
Some(child) => node = child,
None => return Ok(None),
}
}
Ok(Some(node))
}
fn index_error_to_store_error(error: crate::index::IndexError) -> crate::Error {
match error {
crate::index::IndexError::Record(source) => source,
other => crate::Error::InvalidFormat {
details: other.to_string(),
},
}
}