Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Higher availability #8

Merged
merged 6 commits into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,092 changes: 1,033 additions & 59 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dotenv = "0.15.0"
futures-util = "0.3.28"
humantime = "2.1.0"
lazy_static = "1.4.0"
libsql-client = "0.33.2"
reqwest = { version = "0.11.18", features = ["json"] }
serde = { version = "1.0.183", features = ["derive"] }
serde_json = "1.0.104"
Expand Down
2 changes: 1 addition & 1 deletion fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ primary_region = "ord"

[env]
HOST = "troyonthetrails.com"
RUST_LOG = "info"
RUST_LOG = "troyonthetrails=info"

[checks]
[checks.health]
Expand Down
191 changes: 191 additions & 0 deletions src/db_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
use std::{
fmt::{Display, Formatter},
time::{Duration, SystemTime},
};

use anyhow::Context;
use lazy_static::lazy_static;
use libsql_client::{args, Statement, SyncClient};
use tokio::sync::Mutex;
use tracing::{debug, error};

use crate::strava_api_service::TokenData;

#[derive(Debug)]
pub struct TroyStatus {
pub is_on_trail: bool,
pub trail_status_updated: Option<SystemTime>,
}

enum DBTable {
TroyStatus,
StravaAuth,
}

impl Display for DBTable {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
DBTable::TroyStatus => write!(f, "troy_status"),
DBTable::StravaAuth => write!(f, "strava_auth"),
}
}
}

lazy_static! {
pub static ref DB_SERVICE: Mutex<DbService> = Mutex::new(DbService::default());
}

pub struct DbService {
client: SyncClient,
}

impl Default for DbService {
fn default() -> Self {
let service = Self::new();
service.init_tables();
service
}
}

impl DbService {
pub fn new() -> Self {
let client = SyncClient::from_env()
.context("Failed to connect to libsql db")
.unwrap();

DbService { client }
}

pub fn init_tables(&self) {
if let Err(err) = self.client.execute("CREATE TABLE IF NOT EXISTS troy_status (id INTEGER PRIMARY KEY CHECK (id = 1), is_on_trail INTEGER, trail_status_updated INTEGER)") {
error!("Failed to create table troy_status: {}", err);
}

if let Err(err) = self.client.execute("CREATE TABLE IF NOT EXISTS strava_auth (id INTEGER PRIMARY KEY CHECK (id = 1), access_token TEXT, refresh_token TEXT, expires_at INTEGER)") {
error!("Failed to create table strava_auth: {}", err);
}
}

fn upsert(&self, statement: Statement, table: DBTable) {
let result = self.client.execute(statement);

if result.is_err() {
error!("{}", result.unwrap_err());
return;
}

let result = result.unwrap();
if result.rows_affected != 1 {
error!(
"Failed to to db, expected 1 row affected but got {}",
result.rows_affected
);
return;
}

debug!("{} upserted to db", table);
}

pub fn get_troy_status(&self) -> TroyStatus {
let result = self.client.execute("SELECT * FROM troy_status");

if result.is_err() {
error!("Failed to get troy status from db");
return TroyStatus {
is_on_trail: false,
trail_status_updated: None,
};
}

let result = result.unwrap();

if result.rows.len() != 1 {
error!(
"Failed to get troy status from db, expected 1 row but got {}",
result.rows.len()
);
return TroyStatus {
is_on_trail: false,
trail_status_updated: None,
};
}

let mut result = result.rows;
let result = result.pop().unwrap();

TroyStatus {
is_on_trail: result
.try_column("is_on_trail")
.context("Failed to parse is_on_trail to int")
.unwrap_or(0)
== 1,
trail_status_updated: Some(
SystemTime::UNIX_EPOCH
+ Duration::from_secs(result.try_column("trail_status_updated").unwrap_or(0)),
),
}
}

pub fn set_troy_status(&self, is_on_trail: bool) {
let is_on_trail = match is_on_trail {
true => 1,
false => 0,
};

// get current unix milis timestamp
let current_timestamp: i64 = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
{
Ok(duration) => duration.as_secs() as i64,
Err(_) => 0,
};

self
.upsert(Statement::with_args(
"INSERT INTO troy_status (id, is_on_trail, trail_status_updated) \
VALUES (1, ?, ?) \
ON CONFLICT (id) \
DO UPDATE SET is_on_trail = excluded.is_on_trail, trail_status_updated = excluded.trail_status_updated",
args!(is_on_trail, current_timestamp),
), DBTable::TroyStatus);
}

pub fn get_strava_auth(&self) -> Option<TokenData> {
let result = self.client.execute("SELECT * FROM strava_auth");
if result.is_err() {
error!("Failed to get strava auth from db");
return None;
}

let result = result.unwrap();
if result.rows.len() != 1 {
error!(
"Failed to get strava auth from db, expected 1 row but got {}",
result.rows.len()
);
return None;
}

let mut result = result.rows;
let result = result.pop().unwrap();

Some(TokenData {
access_token: result.try_column("access_token").unwrap_or("").to_string(),
refresh_token: result.try_column("refresh_token").unwrap_or("").to_string(),
expires_at: result.try_column("expires_at").unwrap_or(0),
})
}

pub fn set_strava_auth(&self, token_data: TokenData) {
let access_token = token_data.access_token;
let refresh_token = token_data.refresh_token;
let expires_at = token_data.expires_at as i64;

self.upsert(Statement::with_args(
"INSERT INTO strava_auth (id, access_token, refresh_token, expires_at) \
VALUES (1, ?, ?, ?) \
ON CONFLICT (id) \
DO UPDATE SET access_token = excluded.access_token, refresh_token = excluded.refresh_token, expires_at = excluded.expires_at",
args!(access_token, refresh_token, expires_at),
), DBTable::StravaAuth);
}
}
11 changes: 8 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ use tracing_subscriber::{
filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter,
};

use crate::strava_api_service::API_SERVICE;

mod db_service;
mod env_utils;
mod route_handlers;
mod strava_api_service;
mod utils;

#[derive(Default)]
pub struct AppState {
// troy data
is_troy_on_the_trails: bool,
troy_status_last_updated: Option<Instant>,
// trail data
trail_data_last_updated: Option<Instant>,
trail_data: Vec<route_handlers::trail_check::TrailSystem>,
Expand All @@ -44,6 +44,11 @@ async fn main() -> anyhow::Result<()> {

debug!("initializing app state ...");

{
let mut strava_api_service = API_SERVICE.lock().await;
strava_api_service.read_strava_auth_from_db().await;
}

let port = crate::env_utils::get_port();
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], port));
let host_uri = crate::env_utils::get_host_uri();
Expand Down
31 changes: 14 additions & 17 deletions src/route_handlers/home.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
use std::sync::Arc;
use crate::{db_service::DB_SERVICE, strava_api_service::API_SERVICE};

use axum::extract::State;
use tokio::{sync::Mutex, time::Instant};
pub async fn handler() -> impl axum::response::IntoResponse {
let db_service = DB_SERVICE.lock().await;

use crate::{strava_api_service::API_SERVICE, AppState};

pub async fn handler(app_state: State<Arc<Mutex<AppState>>>) -> impl axum::response::IntoResponse {
let mut state = app_state.lock().await;

let last_updated = match state.troy_status_last_updated {
let last_updated = match db_service.get_troy_status().trail_status_updated {
None => "never".to_string(),
Some(updated) => {
let elapsed = updated.elapsed();

// if its been more than 4 hours then troy problably isn't on the trails
Some(last_updated) => {
let elapsed = last_updated.elapsed().unwrap();
if elapsed.as_secs() > 14400 {
state.is_troy_on_the_trails = false;
state.troy_status_last_updated = Some(Instant::now());
db_service.set_troy_status(false);
}

let elapsed = humantime::format_duration(elapsed).to_string();
let elapsed: Vec<&str> = elapsed.split_whitespace().collect();
let elapsed = elapsed[..elapsed.len() - 3].join(" ");
let elapsed = elapsed
.split_whitespace()
.filter(|group| {
!group.contains("ms") && !group.contains("us") && !group.contains("ns")
})
.collect::<Vec<&str>>()
.join(" ");
format!("{} ago", elapsed)
}
};
Expand Down
16 changes: 4 additions & 12 deletions src/route_handlers/troy_check.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
use std::sync::Arc;

use axum::extract::State;
use tokio::sync::Mutex;

use crate::AppState;

pub async fn handler(
State(state): State<Arc<Mutex<AppState>>>,
) -> impl axum::response::IntoResponse {
let state = state.lock().await;
use crate::db_service::DB_SERVICE;

pub async fn handler() -> impl axum::response::IntoResponse {
let db_service = DB_SERVICE.lock().await;
let template = TrailCheckTemplate {
is_troy_on_the_trails: state.is_troy_on_the_trails,
is_troy_on_the_trails: db_service.get_troy_status().is_on_trail,
};
super::html_template::HtmlTemplate(template)
}
Expand Down
20 changes: 7 additions & 13 deletions src/route_handlers/webhooks.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
use std::sync::Arc;

use axum::{extract::State, Json};
use axum::Json;
use serde::{Deserialize, Serialize};
use tokio::{sync::Mutex, time::Instant};
use tracing::log::{debug, error, info};
use webhook::{client::WebhookClient, models::Message};

use crate::{
db_service::DB_SERVICE,
strava_api_service::{Activity, API_SERVICE},
utils::{meters_to_feet, meters_to_miles, mps_to_miph},
AppState,
};

#[derive(Debug, Serialize)]
Expand All @@ -29,15 +26,13 @@ struct WebhookData {
max_speed: f64,
}

pub async fn handler(
State(state): State<Arc<Mutex<AppState>>>,
Json(payload): Json<WebhookRequest>,
) -> impl axum::response::IntoResponse {
pub async fn handler(Json(payload): Json<WebhookRequest>) -> impl axum::response::IntoResponse {
debug!("Webhook request: {:?}", payload);

let mut state = state.lock().await;
let db_service = DB_SERVICE.lock().await;
let troy_status = db_service.get_troy_status();

let current_status = state.is_troy_on_the_trails;
let current_status = troy_status.is_on_trail;
let new_status = payload.on_the_trail;
if current_status != new_status {
info!(
Expand All @@ -50,8 +45,7 @@ pub async fn handler(
});
}

state.is_troy_on_the_trails = payload.on_the_trail;
state.troy_status_last_updated = Some(Instant::now());
db_service.set_troy_status(new_status);

axum::http::status::StatusCode::OK
}
Expand Down
Loading
Loading