Skip to content

Commit

Permalink
Hide order logic in backend: issue 55 (#61)
Browse files Browse the repository at this point in the history
* style: 💚 fix CI

* fix: 🐛 reject unknown field on config

* fix: 🐛 fix quick start docker-compose

* Follow semantic conventions for GRPC Spans

* bound check for add_to_request

* rewrite rpc.oj.backend.token/list with normal pagaintor

* optimize wasm size using alternative allocator and using different serialization library.

* add order to database

* change Dockerfile to enable bin feature

* add ReOrder trait

* change proto and the endpoint

issue: #55
  • Loading branch information
Eason0729 authored Aug 9, 2024
1 parent dce1ea0 commit 84485e6
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 6 deletions.
7 changes: 7 additions & 0 deletions backend/migration/src/m20231207_000001_create_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ enum Testcase {
Input,
Output,
Score,
Order,
}
#[derive(Iden)]
enum Token {
Expand Down Expand Up @@ -488,6 +489,12 @@ impl MigrationTrait for Migration {
.not_null()
.default(0),
)
.col(
ColumnDef::new(Testcase::Order)
.float()
.not_null()
.default(0.0),
)
.to_owned(),
)
.await?;
Expand Down
2 changes: 1 addition & 1 deletion backend/src/endpoint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use tonic::*;
use tracing::*;
use uuid::Uuid;

use crate::entity::util::filter::*;
use crate::entity::util::{filter::*, order::*};
use crate::util::with::*;
use crate::util::{auth::RoleLv, bound::BoundCheck, duplicate::*, error::Error, time::*};
use crate::{fill_active_model, fill_exist_active_model, server::ArcServer, TonicStream};
Expand Down
55 changes: 54 additions & 1 deletion backend/src/endpoint/problem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ impl Problem for ArcServer {
req.get_or_insert(|req| async move {
let (contest, model) = tokio::try_join!(
contest::Entity::write_by_id(req.contest_id, &auth)?
.into_partial_model()
.one(self.db.deref())
.instrument(debug_span!("find_parent").or_current()),
Entity::write_by_id(req.problem_id, &auth)?
Expand All @@ -231,13 +232,20 @@ impl Problem for ArcServer {
)
.map_err(Into::<Error>::into)?;

contest.ok_or(Error::NotInDB)?;
let contest: contest::IdModel = contest.ok_or(Error::NotInDB)?;

let mut model = model.ok_or(Error::NotInDB)?.into_active_model();
if let ActiveValue::Set(Some(v)) = model.contest_id {
return Err(Error::AlreadyExist("problem already linked"));
}

let order = contest
.with_db(self.db.deref())
.insert_last()
.await
.map_err(Into::<Error>::into)?;
model.order = ActiveValue::Set(order);

model.contest_id = ActiveValue::Set(Some(req.problem_id));
model
.save(self.db.deref())
Expand Down Expand Up @@ -292,6 +300,51 @@ impl Problem for ArcServer {
.with_grpc()
.into()
}
#[instrument(
skip_all,
level = "info",
name = "oj.backend.Problem/insert",
err(level = "debug", Display)
)]
async fn insert(&self, req: Request<InsertProblemRequest>) -> Result<Response<()>, Status> {
let (auth, req) = self.rate_limit(req).in_current_span().await?;
auth.perm().super_user()?;

req.get_or_insert(|req| async move {
let contest: contest::IdModel = contest::Entity::find_by_id(req.contest_id)
.with_auth(&auth)
.write()?
.into_partial_model()
.one(self.db.deref())
.instrument(info_span!("fetch").or_current())
.await
.map_err(Into::<Error>::into)?
.ok_or(Error::NotInDB)?;

let order = match req.pivot_id {
None => contest.with_db(self.db.deref()).insert_front().await,
Some(id) => contest.with_db(self.db.deref()).insert_after(id).await,
}
.map_err(Into::<Error>::into)?;

Entity::write_filter(
Entity::update(ActiveModel {
id: ActiveValue::Set(req.problem_id),
order: ActiveValue::Set(order),
..Default::default()
}),
&auth,
)?
.exec(self.db.deref())
.await
.map_err(Into::<Error>::into)?;

Ok(())
})
.await
.with_grpc()
.into()
}
#[instrument(
skip_all,
level = "info",
Expand Down
56 changes: 55 additions & 1 deletion backend/src/endpoint/testcase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ impl Testcase for ArcServer {
req.get_or_insert(|req| async move {
let (problem, model) = tokio::try_join!(
problem::Entity::write_by_id(req.problem_id, &auth)?
.into_partial_model()
.one(self.db.deref())
.instrument(debug_span!("find_parent").or_current()),
Entity::write_by_id(req.testcase_id, &auth)?
Expand All @@ -193,12 +194,20 @@ impl Testcase for ArcServer {
)
.map_err(Into::<Error>::into)?;

problem.ok_or(Error::NotInDB)?;
let problem: problem::IdModel = problem.ok_or(Error::NotInDB)?;

let mut model = model.ok_or(Error::NotInDB)?.into_active_model();
if let ActiveValue::Set(Some(v)) = model.problem_id {
return Err(Error::AlreadyExist("testcase already linked"));
}

let order = problem
.with_db(self.db.deref())
.insert_last()
.await
.map_err(Into::<Error>::into)?;
model.order = ActiveValue::Set(order);

model.problem_id = ActiveValue::Set(Some(req.problem_id));
model
.update(self.db.deref())
Expand Down Expand Up @@ -247,6 +256,51 @@ impl Testcase for ArcServer {
.with_grpc()
.into()
}
#[instrument(
skip_all,
level = "info",
name = "oj.backend.Testcase/insert",
err(level = "debug", Display)
)]
async fn insert(&self, req: Request<InsertTestcaseRequest>) -> Result<Response<()>, Status> {
let (auth, req) = self.rate_limit(req).in_current_span().await?;
auth.perm().super_user()?;

req.get_or_insert(|req| async move {
let problem: problem::IdModel = problem::Entity::find_by_id(req.problem_id)
.with_auth(&auth)
.write()?
.into_partial_model()
.one(self.db.deref())
.instrument(info_span!("fetch").or_current())
.await
.map_err(Into::<Error>::into)?
.ok_or(Error::NotInDB)?;

let order = match req.pivot_id {
None => problem.with_db(self.db.deref()).insert_front().await,
Some(id) => problem.with_db(self.db.deref()).insert_after(id).await,
}
.map_err(Into::<Error>::into)?;

Entity::write_filter(
Entity::update(ActiveModel {
id: ActiveValue::Set(req.testcase_id),
order: ActiveValue::Set(order),
..Default::default()
}),
&auth,
)?
.exec(self.db.deref())
.await
.map_err(Into::<Error>::into)?;

Ok(())
})
.await
.with_grpc()
.into()
}
#[instrument(
skip_all,
level = "info",
Expand Down
3 changes: 2 additions & 1 deletion backend/src/entity/testcase.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::*;

// FIXME: use partial model
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "testcase")]
pub struct Model {
#[sea_orm(primary_key)]
Expand All @@ -14,6 +14,7 @@ pub struct Model {
#[sea_orm(column_type = "Binary(BlobSize::Blob(None))")]
pub output: Vec<u8>,
pub score: u32,
pub order: f32,
}

#[derive(DerivePartialModel, FromQueryResult)]
Expand Down
4 changes: 3 additions & 1 deletion backend/src/entity/util/helper.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
//! a collection of helper function
//! a collection of helper function for low level sql query
//!
//! This module use extensively of [`sea_query`], which make it extreme unsafe to use
use std::ops::Deref;

Expand Down
1 change: 1 addition & 0 deletions backend/src/entity/util/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod filter;
pub mod helper;
pub mod order;
pub mod paginator;
111 changes: 111 additions & 0 deletions backend/src/entity/util/order.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use crate::util::with::WithDB;
use crate::util::with::WithDBTrait;
use sea_orm::*;
use tonic::async_trait;

#[async_trait]
pub trait ReOrder {
async fn insert_last(self) -> Result<f32, DbErr>;
async fn insert_after(self, pivot: i32) -> Result<f32, DbErr>;
async fn insert_front(self) -> Result<f32, DbErr>;
}

#[derive(Default, EnumIter, DeriveColumn, Clone, Copy, Debug)]
enum RetValue {
#[default]
RetValue,
}
pub mod testcase {
use super::*;
use crate::entity::problem;
use crate::entity::testcase::{Column, Entity};

impl WithDBTrait for problem::IdModel {}

#[async_trait]
impl ReOrder for WithDB<'_, problem::IdModel> {
async fn insert_last(self) -> Result<f32, DbErr> {
Entity::find()
.filter(Column::ProblemId.eq(self.1.id))
.select_only()
.column_as(Column::Order.max(), RetValue::default())
.into_values::<_, RetValue>()
.one(self.0)
.await
.map(|x: Option<f32>| x.unwrap_or_default() + 1.0)
}
async fn insert_after(self, pivot: i32) -> Result<f32, DbErr> {
let vals: Vec<f32> = Entity::find()
.filter(Column::ProblemId.eq(self.1.id))
.filter(Column::Order.gte(pivot))
.select_only()
.column_as(Column::Order.min(), RetValue::default())
.limit(2)
.into_values::<_, RetValue>()
.all(self.0)
.await?;
Ok(match vals.len() {
1 => vals[0] + 1.0,
2 => (vals[0] + vals[1]) * 0.5,
_ => 0.0,
})
}
async fn insert_front(self) -> Result<f32, DbErr> {
Entity::find()
.filter(Column::ProblemId.eq(self.1.id))
.select_only()
.column_as(Column::Order.min(), RetValue::default())
.into_values::<_, RetValue>()
.one(self.0)
.await
.map(|x: Option<f32>| x.unwrap_or_default() - 1.0)
}
}
}

pub mod contest {
use super::*;
use crate::entity::contest;
use crate::entity::problem::{Column, Entity};

impl WithDBTrait for contest::IdModel {}
#[async_trait]
impl ReOrder for WithDB<'_, contest::IdModel> {
async fn insert_last(self) -> Result<f32, DbErr> {
Entity::find()
.filter(Column::ContestId.eq(self.1.id))
.select_only()
.column_as(Column::Order.max(), RetValue::default())
.into_values::<_, RetValue>()
.one(self.0)
.await
.map(|x: Option<f32>| x.unwrap_or_default() + 1.0)
}
async fn insert_after(self, pivot: i32) -> Result<f32, DbErr> {
let vals: Vec<f32> = Entity::find()
.filter(Column::ContestId.eq(self.1.id))
.filter(Column::Order.gte(pivot))
.select_only()
.column_as(Column::Order.min(), RetValue::default())
.limit(2)
.into_values::<_, RetValue>()
.all(self.0)
.await?;
Ok(match vals.len() {
1 => vals[0] + 1.0,
2 => (vals[0] + vals[1]) * 0.5,
_ => 0.0,
})
}
async fn insert_front(self) -> Result<f32, DbErr> {
Entity::find()
.filter(Column::ContestId.eq(self.1.id))
.select_only()
.column_as(Column::Order.min(), RetValue::default())
.into_values::<_, RetValue>()
.one(self.0)
.await
.map(|x: Option<f32>| x.unwrap_or_default() - 1.0)
}
}
}
3 changes: 3 additions & 0 deletions backend/src/util/duplicate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,6 @@ create_cache!(AddEducationToProblemRequest, ());
create_cache!(UploadRequest, UploadResponse);
create_cache!(AddTestcaseToProblemRequest, ());
create_cache!(AddProblemToContestRequest, ());

create_cache!(InsertProblemRequest, ());
create_cache!(InsertTestcaseRequest, ());
11 changes: 11 additions & 0 deletions backend/src/util/rate_limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,14 @@ impl RateLimit for RefreshRequest {
50
}
}

impl RateLimit for InsertProblemRequest {
fn get_cost(&self) -> u32 {
3 + (self.pivot_id.is_some() as u32) * 2
}
}
impl RateLimit for InsertTestcaseRequest {
fn get_cost(&self) -> u32 {
3 + (self.pivot_id.is_some() as u32) * 2
}
}
2 changes: 1 addition & 1 deletion frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ RUN rustup target add ${ARCH}-unknown-linux-musl
ENV LEPTOS_OUTPUT_NAME="mdoj"
ENV LEPTOS_BIN_TARGET_TRIPLE=${ARCH}-unknown-linux-musl

RUN cargo leptos build -p frontend --release --precompress -vv
RUN cargo leptos build -p frontend --bin-features compress --release --precompress -vv

FROM scratch
WORKDIR /config
Expand Down
Loading

0 comments on commit 84485e6

Please sign in to comment.