Skip to content

Commit 3c3cff4

Browse files
committed
in-database embedded file-system for true serverless usage
1 parent 800b6a1 commit 3c3cff4

File tree

10 files changed

+399
-59
lines changed

10 files changed

+399
-59
lines changed

Cargo.lock

Lines changed: 152 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ repository = "https://github.com/lovasoa/SQLpage"
1010
documentation = "https://docs.rs/sqlpage"
1111

1212
[dependencies]
13-
sqlx = { version = "0.6.0", features = ["any", "runtime-actix-rustls", "sqlite", "postgres", "mysql"] }
13+
sqlx = { version = "0.6.0", features = ["any", "runtime-actix-rustls", "sqlite", "postgres", "mysql", "chrono"] }
14+
chrono = "0.4.23"
1415
actix-web = { version = "4", features = ["rustls"] }
1516
handlebars = "5.0.0-beta.0"
1617
log = "0.4.17"

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,23 @@ An easy way to do so is using the provided docker image:
128128
You can then use `deploy.zip` as the source for an AWS Lambda,
129129
selecting *Custom runtime on Amazon Linux 2* as a runtime.
130130

131+
### Hosting sql files directly inside the database
132+
133+
When running serverless, you can include the SQL files directly in the image that you are deploying.
134+
But if you want to be able to update your sql files on the fly without creating a new image,
135+
you can store the files directly inside the database, in a table that has the following structure:
136+
137+
```sql
138+
CREATE TABLE sqlpage_files(
139+
path VARCHAR(255) NOT NULL PRIMARY KEY,
140+
contents TEXT,
141+
last_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP
142+
);
143+
```
144+
145+
Make sure to update `last_modified` every time you update the contents of a file (or do it inside a TRIGGER).
146+
SQLPage will re-parse a file from the database only when it has been modified.
147+
131148
## Technologies and libraries used
132149

133150
- [actix web](https://actix.rs/) handles HTTP requests at an incredible speed,

src/file_cache.rs

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
use crate::AppState;
22
use anyhow::Context;
33
use async_trait::async_trait;
4+
use chrono::{DateTime, TimeZone, Utc};
45
use dashmap::DashMap;
56
use std::path::PathBuf;
67
use std::sync::atomic::{
78
AtomicU64,
89
Ordering::{Acquire, Release},
910
};
1011
use std::sync::Arc;
11-
use std::time::{Duration, SystemTime};
12+
use std::time::SystemTime;
1213

1314
const MAX_STALE_CACHE_MS: u64 = 100;
1415

@@ -33,25 +34,30 @@ impl<T> Cached<T> {
3334
this.last_checked_at.store(u64::MAX, Release);
3435
this
3536
}
36-
fn last_check_time(&self) -> SystemTime {
37-
let millis = self
38-
.last_checked_at
37+
fn last_check_time(&self) -> DateTime<Utc> {
38+
self.last_checked_at
3939
.load(Acquire)
40-
.saturating_mul(MAX_STALE_CACHE_MS);
41-
SystemTime::UNIX_EPOCH + Duration::from_millis(millis)
40+
.saturating_mul(MAX_STALE_CACHE_MS)
41+
.try_into()
42+
.ok()
43+
.and_then(|millis| Utc.timestamp_millis_opt(millis).single())
44+
.expect("file timestamp out of bound")
4245
}
4346
fn update_check_time(&self) {
44-
let elapsed = u64::try_from(Self::elapsed()).expect("too far in the future");
45-
self.last_checked_at.store(elapsed, Release);
47+
self.last_checked_at.store(Self::elapsed(), Release);
4648
}
47-
fn elapsed() -> u128 {
48-
(SystemTime::now().duration_since(SystemTime::UNIX_EPOCH))
49-
.unwrap()
50-
.as_millis()
51-
/ u128::from(MAX_STALE_CACHE_MS)
49+
fn elapsed() -> u64 {
50+
let timestamp_millis = (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH))
51+
.expect("invalid duration")
52+
.as_millis();
53+
let elapsed_intervals = timestamp_millis / u128::from(MAX_STALE_CACHE_MS);
54+
u64::try_from(elapsed_intervals).expect("invalid date")
5255
}
5356
fn needs_check(&self) -> bool {
54-
self.last_check_time() + Duration::from_millis(MAX_STALE_CACHE_MS) <= SystemTime::now()
57+
self.last_checked_at
58+
.load(Acquire)
59+
.saturating_add(MAX_STALE_CACHE_MS)
60+
< Self::elapsed()
5561
}
5662
}
5763

@@ -79,24 +85,25 @@ impl<T: AsyncFromStrWithState> FileCache<T> {
7985
log::trace!("Cache answer without filesystem lookup for {:?}", path);
8086
return Ok(Arc::clone(&cached.content));
8187
}
82-
let modified_res = tokio::fs::metadata(path).await.and_then(|m| m.modified());
83-
match modified_res {
84-
Ok(modified) => {
85-
if modified <= cached.last_check_time() {
86-
log::trace!("Cache answer with filesystem metadata read for {:?}", path);
87-
cached.update_check_time();
88-
return Ok(Arc::clone(&cached.content));
89-
}
88+
match app_state
89+
.file_system
90+
.modified_since(app_state, path, cached.last_check_time())
91+
.await
92+
{
93+
Ok(false) => {
94+
log::trace!("Cache answer with filesystem metadata read for {:?}", path);
95+
cached.update_check_time();
96+
return Ok(Arc::clone(&cached.content));
9097
}
91-
Err(e) => log::warn!(
92-
"Unable to check when '{}' was last modified. Re-reading the file: {e:#}",
93-
path.display()
94-
),
98+
Ok(true) => log::trace!("{path:?} was changed, updating cache..."),
99+
Err(e) => log::warn!("Cannot read metadata of {path:?}, re-loading it: {e:#}"),
95100
}
96101
}
97102
// Read lock is released
98103
log::trace!("Loading and parsing {:?}", path);
99-
let file_contents = tokio::fs::read_to_string(path)
104+
let file_contents = app_state
105+
.file_system
106+
.read_file(app_state, path)
100107
.await
101108
.with_context(|| format!("Reading {path:?} to load it in cache"));
102109
let parsed = match file_contents {

0 commit comments

Comments
 (0)