use crate::{AbsolutePath, ChangeSeq, CheckpointId, InodeId, NamespaceId, RevisionNo};
use serde::{Deserialize, Serialize};
use xxhash_rust::xxh64::xxh64;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GrepRequest {
pub pattern: String,
#[serde(default)]
pub case_insensitive: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path_prefix: Option<AbsolutePath>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(default)]
pub allow_stale: bool,
#[serde(default)]
pub allow_scan: bool,
}
impl GrepRequest {
pub fn fingerprint(&self) -> u64 {
let mut seed = xxh64(self.pattern.as_bytes(), 0);
seed = xxh64(
self.path_prefix
.as_ref()
.map(AbsolutePath::as_str)
.unwrap_or("")
.as_bytes(),
seed,
);
let flags = [
u8::from(self.case_insensitive),
u8::from(self.allow_stale),
u8::from(self.allow_scan),
];
xxh64(&flags, seed)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GrepMatch {
pub absolute_path: AbsolutePath,
pub inode_id: InodeId,
pub revision_no: RevisionNo,
pub line_number: u64,
pub byte_offset: u64,
pub line: String,
#[serde(default)]
pub line_truncated: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GrepResponse {
pub namespace_id: NamespaceId,
pub head_seq: ChangeSeq,
pub built_through_seq: ChangeSeq,
pub tail_scanned: bool,
pub matches: Vec<GrepMatch>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(tag = "phase", rename_all = "snake_case")]
pub enum GrepIndexLifecycle {
Disabled,
Backfilling {
target_seq: ChangeSeq,
#[serde(default, skip_serializing_if = "Option::is_none")]
cursor_inode_id: Option<InodeId>,
checkpoint_id: CheckpointId,
},
Steady {
built_through_seq: ChangeSeq,
#[serde(default, skip_serializing_if = "is_zero")]
next_event_index: u32,
},
}
impl GrepIndexLifecycle {
pub fn is_built_through(&self, target_seq: ChangeSeq) -> bool {
match self {
Self::Disabled | Self::Backfilling { .. } => false,
Self::Steady {
built_through_seq,
next_event_index,
} => {
*built_through_seq > target_seq
|| (*built_through_seq == target_seq && *next_event_index == 0)
}
}
}
}
fn is_zero(value: &u32) -> bool {
*value == 0
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct EnableGrepIndexResponse {
pub namespace_id: NamespaceId,
pub already_enabled: bool,
pub state: GrepIndexLifecycle,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GrepIndexStatusResponse {
pub namespace_id: NamespaceId,
pub state: GrepIndexLifecycle,
pub next_run_ordinal: u64,
pub reorganize_pending: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct DisableGrepIndexResponse {
pub namespace_id: NamespaceId,
pub was_enabled: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GrepGcRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_objects: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GrepGcResponse {
pub namespace_id: NamespaceId,
pub deleted_segments: u64,
pub deleted_other_objects: u64,
pub namespace_reaped: bool,
pub retained_candidates: u64,
pub namespace_degraded: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grep_paths_keep_the_plain_string_wire_shape() {
let request = GrepRequest {
pattern: "needle".to_owned(),
case_insensitive: false,
path_prefix: Some(AbsolutePath::parse("/docs").expect("path prefix")),
cursor: None,
limit: None,
allow_stale: false,
allow_scan: false,
};
assert_eq!(
serde_json::to_value(request).expect("serialize grep request"),
serde_json::json!({
"pattern": "needle",
"case_insensitive": false,
"path_prefix": "/docs",
"allow_stale": false,
"allow_scan": false
})
);
let found = GrepMatch {
absolute_path: AbsolutePath::parse("/docs/a.txt").expect("match path"),
inode_id: InodeId(2),
revision_no: RevisionNo(3),
line_number: 4,
byte_offset: 5,
line: "needle".to_owned(),
line_truncated: false,
};
assert_eq!(
serde_json::to_value(found).expect("serialize grep match"),
serde_json::json!({
"absolute_path": "/docs/a.txt",
"inode_id": 2,
"revision_no": 3,
"line_number": 4,
"byte_offset": 5,
"line": "needle",
"line_truncated": false
})
);
}
#[test]
fn the_lifecycle_phases_never_share_a_sequence_field() {
let backfilling = GrepIndexLifecycle::Backfilling {
target_seq: ChangeSeq(9),
cursor_inode_id: Some(InodeId(4)),
checkpoint_id: CheckpointId::parse("chk_00000000000000000000000000000009")
.expect("checkpoint id"),
};
assert_eq!(
serde_json::to_value(&backfilling).expect("serialize backfilling"),
serde_json::json!({
"phase": "backfilling",
"target_seq": 9,
"cursor_inode_id": 4,
"checkpoint_id": "chk_00000000000000000000000000000009"
}),
"a backfill reports its target and its walk, never a watermark"
);
assert_eq!(
serde_json::to_value(GrepIndexLifecycle::Steady {
built_through_seq: ChangeSeq(9),
next_event_index: 0,
})
.expect("serialize steady"),
serde_json::json!({"phase": "steady", "built_through_seq": 9}),
"a steady index reports its watermark and no target"
);
assert_eq!(
serde_json::to_value(GrepIndexLifecycle::Disabled).expect("serialize disabled"),
serde_json::json!({"phase": "disabled"})
);
}
#[test]
fn only_a_steady_index_has_built_through_a_sequence() {
let backfilling = GrepIndexLifecycle::Backfilling {
target_seq: ChangeSeq(9),
cursor_inode_id: None,
checkpoint_id: CheckpointId::parse("chk_00000000000000000000000000000009")
.expect("checkpoint id"),
};
assert!(
!backfilling.is_built_through(ChangeSeq(0)),
"a backfill has indexed nothing until it turns steady"
);
assert!(!GrepIndexLifecycle::Disabled.is_built_through(ChangeSeq(0)));
let steady = |built_through_seq, next_event_index| GrepIndexLifecycle::Steady {
built_through_seq,
next_event_index,
};
assert!(steady(ChangeSeq(9), 0).is_built_through(ChangeSeq(9)));
assert!(steady(ChangeSeq(9), 0).is_built_through(ChangeSeq(8)));
assert!(!steady(ChangeSeq(9), 0).is_built_through(ChangeSeq(10)));
assert!(!steady(ChangeSeq(9), 3).is_built_through(ChangeSeq(9)));
assert!(steady(ChangeSeq(9), 3).is_built_through(ChangeSeq(8)));
}
#[test]
fn grep_path_prefix_validates_during_deserialization() {
let encoded = serde_json::json!({
"pattern": "needle",
"path_prefix": "relative/path"
});
assert!(serde_json::from_value::<GrepRequest>(encoded).is_err());
}
}