3647: Improve the health route by ensuring lmdb is not down r=irevoire a=irevoire

Fixes #3644

In this PR, I try to make a small read on the `AuthController` and `IndexScheduler` databases.
The idea is not to validate that everything works but just to avoid the bug we had last time when lmdb was stuck forever.

In order to get access to the `AuthController` without going through the extractor, I need to wrap it in the `Data` type from `actix-web`.
And to do that, I had to patch our extractor so it works with the `Data` type as well.

Co-authored-by: Tamo <tamo@meilisearch.com>
This commit is contained in:
bors[bot] 2023-04-06 18:23:52 +00:00 committed by GitHub
commit bc25f378e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 64 additions and 26 deletions

View File

@ -429,6 +429,13 @@ impl IndexScheduler {
Ok(this) Ok(this)
} }
/// Return `Ok(())` if the index scheduler is able to access one of its database.
pub fn health(&self) -> Result<()> {
let rtxn = self.env.read_txn()?;
self.all_tasks.first(&rtxn)?;
Ok(())
}
fn index_budget( fn index_budget(
tasks_path: &Path, tasks_path: &Path,
base_map_size: usize, base_map_size: usize,

View File

@ -34,6 +34,12 @@ impl AuthController {
Ok(Self { store: Arc::new(store), master_key: master_key.clone() }) Ok(Self { store: Arc::new(store), master_key: master_key.clone() })
} }
/// Return `Ok(())` if the auth controller is able to access one of its database.
pub fn health(&self) -> Result<()> {
self.store.health()?;
Ok(())
}
/// Return the size of the `AuthController` database in bytes. /// Return the size of the `AuthController` database in bytes.
pub fn size(&self) -> Result<u64> { pub fn size(&self) -> Result<u64> {
self.store.size() self.store.size()

View File

@ -61,6 +61,13 @@ impl HeedAuthStore {
Ok(Self { env, keys, action_keyid_index_expiration, should_close_on_drop: true }) Ok(Self { env, keys, action_keyid_index_expiration, should_close_on_drop: true })
} }
/// Return `Ok(())` if the auth store is able to access one of its database.
pub fn health(&self) -> Result<()> {
let rtxn = self.env.read_txn()?;
self.keys.first(&rtxn)?;
Ok(())
}
/// Return the size in bytes of database /// Return the size in bytes of database
pub fn size(&self) -> Result<u64> { pub fn size(&self) -> Result<u64> {
Ok(self.env.real_disk_size()?) Ok(self.env.real_disk_size()?)

View File

@ -86,7 +86,7 @@ impl SegmentAnalytics {
pub async fn new( pub async fn new(
opt: &Opt, opt: &Opt,
index_scheduler: Arc<IndexScheduler>, index_scheduler: Arc<IndexScheduler>,
auth_controller: AuthController, auth_controller: Arc<AuthController>,
) -> Arc<dyn Analytics> { ) -> Arc<dyn Analytics> {
let instance_uid = super::find_user_id(&opt.db_path); let instance_uid = super::find_user_id(&opt.db_path);
let first_time_run = instance_uid.is_none(); let first_time_run = instance_uid.is_none();
@ -376,7 +376,11 @@ impl Segment {
}) })
} }
async fn run(mut self, index_scheduler: Arc<IndexScheduler>, auth_controller: AuthController) { async fn run(
mut self,
index_scheduler: Arc<IndexScheduler>,
auth_controller: Arc<AuthController>,
) {
const INTERVAL: Duration = Duration::from_secs(60 * 60); // one hour const INTERVAL: Duration = Duration::from_secs(60 * 60); // one hour
// The first batch must be sent after one hour. // The first batch must be sent after one hour.
let mut interval = let mut interval =
@ -408,10 +412,10 @@ impl Segment {
async fn tick( async fn tick(
&mut self, &mut self,
index_scheduler: Arc<IndexScheduler>, index_scheduler: Arc<IndexScheduler>,
auth_controller: AuthController, auth_controller: Arc<AuthController>,
) { ) {
if let Ok(stats) = if let Ok(stats) =
create_all_stats(index_scheduler.into(), auth_controller, &AuthFilter::default()) create_all_stats(index_scheduler.into(), auth_controller.into(), &AuthFilter::default())
{ {
// Replace the version number with the prototype name if any. // Replace the version number with the prototype name if any.
let version = if let Some(prototype) = crate::prototype_name() { let version = if let Some(prototype) = crate::prototype_name() {

View File

@ -4,6 +4,7 @@ use std::marker::PhantomData;
use std::ops::Deref; use std::ops::Deref;
use std::pin::Pin; use std::pin::Pin;
use actix_web::web::Data;
use actix_web::FromRequest; use actix_web::FromRequest;
pub use error::AuthenticationError; pub use error::AuthenticationError;
use futures::future::err; use futures::future::err;
@ -23,7 +24,7 @@ impl<P, D> GuardedData<P, D> {
} }
async fn auth_bearer( async fn auth_bearer(
auth: AuthController, auth: Data<AuthController>,
token: String, token: String,
index: Option<String>, index: Option<String>,
data: Option<D>, data: Option<D>,
@ -43,7 +44,7 @@ impl<P, D> GuardedData<P, D> {
} }
} }
async fn auth_token(auth: AuthController, data: Option<D>) -> Result<Self, ResponseError> async fn auth_token(auth: Data<AuthController>, data: Option<D>) -> Result<Self, ResponseError>
where where
P: Policy + 'static, P: Policy + 'static,
{ {
@ -60,7 +61,7 @@ impl<P, D> GuardedData<P, D> {
} }
async fn authenticate( async fn authenticate(
auth: AuthController, auth: Data<AuthController>,
token: String, token: String,
index: Option<String>, index: Option<String>,
) -> Result<Option<AuthFilter>, ResponseError> ) -> Result<Option<AuthFilter>, ResponseError>
@ -90,7 +91,7 @@ impl<P: Policy + 'static, D: 'static + Clone> FromRequest for GuardedData<P, D>
req: &actix_web::HttpRequest, req: &actix_web::HttpRequest,
_payload: &mut actix_web::dev::Payload, _payload: &mut actix_web::dev::Payload,
) -> Self::Future { ) -> Self::Future {
match req.app_data::<AuthController>().cloned() { match req.app_data::<Data<AuthController>>().cloned() {
Some(auth) => match req Some(auth) => match req
.headers() .headers()
.get("Authorization") .get("Authorization")
@ -122,10 +123,15 @@ impl<P: Policy + 'static, D: 'static + Clone> FromRequest for GuardedData<P, D>
} }
pub trait Policy { pub trait Policy {
fn authenticate(auth: AuthController, token: &str, index: Option<&str>) -> Option<AuthFilter>; fn authenticate(
auth: Data<AuthController>,
token: &str,
index: Option<&str>,
) -> Option<AuthFilter>;
} }
pub mod policies { pub mod policies {
use actix_web::web::Data;
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use meilisearch_auth::{AuthController, AuthFilter, SearchRules}; use meilisearch_auth::{AuthController, AuthFilter, SearchRules};
// reexport actions in policies in order to be used in routes configuration. // reexport actions in policies in order to be used in routes configuration.
@ -178,7 +184,7 @@ pub mod policies {
/// Otherwise, returns an object containing the generated permissions: the search filters to add to a search, and the list of allowed indexes /// Otherwise, returns an object containing the generated permissions: the search filters to add to a search, and the list of allowed indexes
/// (that may contain more indexes than requested). /// (that may contain more indexes than requested).
fn authenticate( fn authenticate(
auth: AuthController, auth: Data<AuthController>,
token: &str, token: &str,
index: Option<&str>, index: Option<&str>,
) -> Option<AuthFilter> { ) -> Option<AuthFilter> {

View File

@ -88,7 +88,7 @@ fn is_empty_db(db_path: impl AsRef<Path>) -> bool {
pub fn create_app( pub fn create_app(
index_scheduler: Data<IndexScheduler>, index_scheduler: Data<IndexScheduler>,
auth_controller: AuthController, auth_controller: Data<AuthController>,
opt: Opt, opt: Opt,
analytics: Arc<dyn Analytics>, analytics: Arc<dyn Analytics>,
enable_dashboard: bool, enable_dashboard: bool,
@ -136,7 +136,7 @@ enum OnFailure {
KeepDb, KeepDb,
} }
pub fn setup_meilisearch(opt: &Opt) -> anyhow::Result<(Arc<IndexScheduler>, AuthController)> { pub fn setup_meilisearch(opt: &Opt) -> anyhow::Result<(Arc<IndexScheduler>, Arc<AuthController>)> {
let empty_db = is_empty_db(&opt.db_path); let empty_db = is_empty_db(&opt.db_path);
let (index_scheduler, auth_controller) = if let Some(ref snapshot_path) = opt.import_snapshot { let (index_scheduler, auth_controller) = if let Some(ref snapshot_path) = opt.import_snapshot {
let snapshot_path_exists = snapshot_path.exists(); let snapshot_path_exists = snapshot_path.exists();
@ -195,6 +195,7 @@ pub fn setup_meilisearch(opt: &Opt) -> anyhow::Result<(Arc<IndexScheduler>, Auth
// We create a loop in a thread that registers snapshotCreation tasks // We create a loop in a thread that registers snapshotCreation tasks
let index_scheduler = Arc::new(index_scheduler); let index_scheduler = Arc::new(index_scheduler);
let auth_controller = Arc::new(auth_controller);
if let ScheduleSnapshot::Enabled(snapshot_delay) = opt.schedule_snapshot { if let ScheduleSnapshot::Enabled(snapshot_delay) = opt.schedule_snapshot {
let snapshot_delay = Duration::from_secs(snapshot_delay); let snapshot_delay = Duration::from_secs(snapshot_delay);
let index_scheduler = index_scheduler.clone(); let index_scheduler = index_scheduler.clone();
@ -380,7 +381,7 @@ fn import_dump(
pub fn configure_data( pub fn configure_data(
config: &mut web::ServiceConfig, config: &mut web::ServiceConfig,
index_scheduler: Data<IndexScheduler>, index_scheduler: Data<IndexScheduler>,
auth: AuthController, auth: Data<AuthController>,
opt: &Opt, opt: &Opt,
analytics: Arc<dyn Analytics>, analytics: Arc<dyn Analytics>,
) { ) {

View File

@ -74,13 +74,14 @@ async fn main() -> anyhow::Result<()> {
async fn run_http( async fn run_http(
index_scheduler: Arc<IndexScheduler>, index_scheduler: Arc<IndexScheduler>,
auth_controller: AuthController, auth_controller: Arc<AuthController>,
opt: Opt, opt: Opt,
analytics: Arc<dyn Analytics>, analytics: Arc<dyn Analytics>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let enable_dashboard = &opt.env == "development"; let enable_dashboard = &opt.env == "development";
let opt_clone = opt.clone(); let opt_clone = opt.clone();
let index_scheduler = Data::from(index_scheduler); let index_scheduler = Data::from(index_scheduler);
let auth_controller = Data::from(auth_controller);
let http_server = HttpServer::new(move || { let http_server = HttpServer::new(move || {
create_app( create_app(

View File

@ -1,5 +1,6 @@
use std::str; use std::str;
use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use deserr::actix_web::{AwebJson, AwebQueryParameter}; use deserr::actix_web::{AwebJson, AwebQueryParameter};
use deserr::Deserr; use deserr::Deserr;
@ -35,7 +36,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
} }
pub async fn create_api_key( pub async fn create_api_key(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, Data<AuthController>>,
body: AwebJson<CreateApiKey, DeserrJsonError>, body: AwebJson<CreateApiKey, DeserrJsonError>,
_req: HttpRequest, _req: HttpRequest,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -66,7 +67,7 @@ impl ListApiKeys {
} }
pub async fn list_api_keys( pub async fn list_api_keys(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
list_api_keys: AwebQueryParameter<ListApiKeys, DeserrQueryParamError>, list_api_keys: AwebQueryParameter<ListApiKeys, DeserrQueryParamError>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let paginate = list_api_keys.into_inner().as_pagination(); let paginate = list_api_keys.into_inner().as_pagination();
@ -84,7 +85,7 @@ pub async fn list_api_keys(
} }
pub async fn get_api_key( pub async fn get_api_key(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
path: web::Path<AuthParam>, path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let key = path.into_inner().key; let key = path.into_inner().key;
@ -103,7 +104,7 @@ pub async fn get_api_key(
} }
pub async fn patch_api_key( pub async fn patch_api_key(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_UPDATE }>, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_UPDATE }>, Data<AuthController>>,
body: AwebJson<PatchApiKey, DeserrJsonError>, body: AwebJson<PatchApiKey, DeserrJsonError>,
path: web::Path<AuthParam>, path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -123,7 +124,7 @@ pub async fn patch_api_key(
} }
pub async fn delete_api_key( pub async fn delete_api_key(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_DELETE }>, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_DELETE }>, Data<AuthController>>,
path: web::Path<AuthParam>, path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let key = path.into_inner().key; let key = path.into_inner().key;

View File

@ -19,7 +19,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
pub async fn create_dump( pub async fn create_dump(
index_scheduler: GuardedData<ActionPolicy<{ actions::DUMPS_CREATE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DUMPS_CREATE }>, Data<IndexScheduler>>,
auth_controller: GuardedData<ActionPolicy<{ actions::DUMPS_CREATE }>, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::DUMPS_CREATE }>, Data<AuthController>>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {

View File

@ -17,7 +17,7 @@ pub fn configure(config: &mut web::ServiceConfig) {
pub async fn get_metrics( pub async fn get_metrics(
index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>,
auth_controller: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<AuthController>>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let auth_filters = index_scheduler.filters(); let auth_filters = index_scheduler.filters();
if !auth_filters.all_indexes_authorized() { if !auth_filters.all_indexes_authorized() {

View File

@ -238,7 +238,7 @@ pub struct Stats {
async fn get_stats( async fn get_stats(
index_scheduler: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<IndexScheduler>>,
auth_controller: GuardedData<ActionPolicy<{ actions::STATS_GET }>, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<AuthController>>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -253,7 +253,7 @@ async fn get_stats(
pub fn create_all_stats( pub fn create_all_stats(
index_scheduler: Data<IndexScheduler>, index_scheduler: Data<IndexScheduler>,
auth_controller: AuthController, auth_controller: Data<AuthController>,
filters: &meilisearch_auth::AuthFilter, filters: &meilisearch_auth::AuthFilter,
) -> Result<Stats, ResponseError> { ) -> Result<Stats, ResponseError> {
let mut last_task: Option<OffsetDateTime> = None; let mut last_task: Option<OffsetDateTime> = None;
@ -318,9 +318,14 @@ struct KeysResponse {
pub async fn get_health( pub async fn get_health(
req: HttpRequest, req: HttpRequest,
index_scheduler: Data<IndexScheduler>,
auth_controller: Data<AuthController>,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
analytics.health_seen(&req); analytics.health_seen(&req);
index_scheduler.health().unwrap();
auth_controller.health().unwrap();
Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "available" }))) Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "available" })))
} }

View File

@ -82,7 +82,7 @@ impl Server {
> { > {
actix_web::test::init_service(create_app( actix_web::test::init_service(create_app(
self.service.index_scheduler.clone().into(), self.service.index_scheduler.clone().into(),
self.service.auth.clone(), self.service.auth.clone().into(),
self.service.options.clone(), self.service.options.clone(),
analytics::MockAnalytics::new(&self.service.options), analytics::MockAnalytics::new(&self.service.options),
true, true,

View File

@ -13,7 +13,7 @@ use crate::common::encoder::Encoder;
pub struct Service { pub struct Service {
pub index_scheduler: Arc<IndexScheduler>, pub index_scheduler: Arc<IndexScheduler>,
pub auth: AuthController, pub auth: Arc<AuthController>,
pub options: Opt, pub options: Opt,
pub api_key: Option<String>, pub api_key: Option<String>,
} }
@ -107,7 +107,7 @@ impl Service {
pub async fn request(&self, mut req: test::TestRequest) -> (Value, StatusCode) { pub async fn request(&self, mut req: test::TestRequest) -> (Value, StatusCode) {
let app = test::init_service(create_app( let app = test::init_service(create_app(
self.index_scheduler.clone().into(), self.index_scheduler.clone().into(),
self.auth.clone(), self.auth.clone().into(),
self.options.clone(), self.options.clone(),
analytics::MockAnalytics::new(&self.options), analytics::MockAnalytics::new(&self.options),
true, true,