Skip to content

Commit 5536080

Browse files
tegiozcynthia-sg
andauthored
Add jobs views tracker (#258)
Related to #250 Signed-off-by: Sergio Castaño Arteaga <[email protected]> Signed-off-by: Cintia Sánchez García <[email protected]> Co-authored-by: Cintia Sánchez García <[email protected]>
1 parent 72faa62 commit 5536080

File tree

18 files changed

+456
-43
lines changed

18 files changed

+456
-43
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ lettre = { version = "0.11.15", default-features = false, features = ["builder",
3333
markdown = "1.0.0-alpha.24"
3434
mime_guess = "2.0.5"
3535
minify-html = "0.16.4"
36+
mockall = "0.13.1"
3637
num-format = "0.4.4"
3738
oauth2 = "5.0.0"
3839
openidconnect = { version = "4.0.0", features = ["accept-rfc3339-timestamps"] }

database/migrations/functions/001_load_functions.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{{ template "dashboard/search_applications.sql" }}
33
{{ template "img/get_image_version.sql" }}
44
{{ template "jobboard/search_jobs.sql" }}
5+
{{ template "jobboard/update_jobs_views.sql" }}
56
{{ template "misc/search_locations.sql" }}
67

78
---- create above / drop below ----
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- update_jobs_views updates the views of the jobs provided.
2+
create or replace function update_jobs_views(p_lock_key bigint, p_data jsonb)
3+
returns void as $$
4+
-- Make sure only one batch of updates is processed at a time
5+
select pg_advisory_xact_lock(p_lock_key);
6+
7+
-- Insert or update the corresponding views counters as needed
8+
insert into job_views (job_id, day, total)
9+
select
10+
(value->>0)::uuid as job_id,
11+
(value->>1)::date as day,
12+
(value->>2)::integer as total
13+
from jsonb_array_elements(p_data)
14+
on conflict (job_id, day) do
15+
update set total = job_views.total + excluded.total;
16+
$$ language sql;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
create table if not exists job_views (
2+
job_id uuid references job on delete set null,
3+
day date not null,
4+
total integer not null,
5+
unique (job_id, day)
6+
);
7+
8+
---- create above / drop below ----
9+
10+
drop table if exists job_views;

gitjobs-server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ unicode-segmentation = { workspace = true }
5858
uuid = { workspace = true }
5959

6060
[dev-dependencies]
61+
futures = { workspace = true }
62+
mockall = { workspace = true }
6163

6264
[build-dependencies]
6365
anyhow = { workspace = true }

gitjobs-server/src/db/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use tokio::{select, time::sleep};
1717
use tokio_util::sync::CancellationToken;
1818
use tracing::instrument;
1919
use uuid::Uuid;
20+
use views::DBViews;
2021
use workers::DBWorkers;
2122

2223
pub(crate) mod auth;
@@ -25,6 +26,7 @@ pub(crate) mod img;
2526
pub(crate) mod jobboard;
2627
pub(crate) mod misc;
2728
pub(crate) mod notifications;
29+
pub(crate) mod views;
2830
pub(crate) mod workers;
2931

3032
/// Error message when a transaction client is not found.
@@ -40,7 +42,7 @@ const TXS_CLIENT_TIMEOUT: TimeDelta = TimeDelta::seconds(10);
4042
/// DB implementation must support.
4143
#[async_trait]
4244
pub(crate) trait DB:
43-
DBJobBoard + DBDashBoard + DBAuth + DBImage + DBNotifications + DBWorkers + DBMisc
45+
DBJobBoard + DBDashBoard + DBAuth + DBImage + DBNotifications + DBWorkers + DBViews + DBMisc
4446
{
4547
/// Begin transaction.
4648
async fn tx_begin(&self) -> Result<Uuid>;

gitjobs-server/src/db/views.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//! This module defines some database functionality used in the views tracker.
2+
3+
use std::sync::Arc;
4+
5+
use anyhow::Result;
6+
use async_trait::async_trait;
7+
#[cfg(test)]
8+
use mockall::automock;
9+
use tokio_postgres::types::Json;
10+
use tracing::{instrument, trace};
11+
12+
use crate::{
13+
db::PgDB,
14+
views::{Day, JobId, Total},
15+
};
16+
17+
// Lock key used when updating the jobs views in the database.
18+
const LOCK_KEY_UPDATE_JOBS_VIEWS: i64 = 1;
19+
20+
/// Trait that defines some database operations used in the views tracker.
21+
#[async_trait]
22+
#[cfg_attr(test, automock)]
23+
pub(crate) trait DBViews {
24+
/// Update the number of views of the jobs provided.
25+
async fn update_jobs_views(&self, data: Vec<(JobId, Day, Total)>) -> Result<()>;
26+
}
27+
28+
/// Type alias to represent a `DBViews` trait object.
29+
pub(crate) type DynDBViews = Arc<dyn DBViews + Send + Sync>;
30+
31+
#[async_trait]
32+
impl DBViews for PgDB {
33+
#[instrument(skip(self), err)]
34+
async fn update_jobs_views(&self, data: Vec<(JobId, Day, Total)>) -> Result<()> {
35+
trace!("db: update jobs views");
36+
37+
let db = self.pool.get().await?;
38+
db.execute(
39+
"select update_jobs_views($1::bigint, $2::jsonb)",
40+
&[&LOCK_KEY_UPDATE_JOBS_VIEWS, &Json(&data)],
41+
)
42+
.await?;
43+
44+
Ok(())
45+
}
46+
}

gitjobs-server/src/handlers/jobboard/jobs.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use crate::{
2424
jobboard::jobs::{ExploreSection, Filters, JobSection, JobsPage, ResultsSection},
2525
pagination::{NavigationLinks, build_url},
2626
},
27+
views::DynViewsTracker,
2728
};
2829

2930
// Pages and sections handlers.
@@ -132,3 +133,14 @@ pub(crate) async fn apply(
132133

133134
Ok(StatusCode::NO_CONTENT)
134135
}
136+
137+
/// Handler used to track a job view.
138+
#[instrument(skip_all, err)]
139+
pub(crate) async fn track_view(
140+
State(views_tracker): State<DynViewsTracker>,
141+
Path(job_id): Path<Uuid>,
142+
) -> Result<impl IntoResponse, HandlerError> {
143+
views_tracker.track_view(job_id).await?;
144+
145+
Ok(StatusCode::NO_CONTENT)
146+
}

gitjobs-server/src/main.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use tokio_util::sync::CancellationToken;
1515
use tokio_util::task::TaskTracker;
1616
use tracing::{error, info};
1717
use tracing_subscriber::EnvFilter;
18+
use views::ViewsTrackerDB;
1819

1920
use crate::{
2021
config::{Config, LogFormat},
@@ -29,6 +30,7 @@ mod img;
2930
mod notifications;
3031
mod router;
3132
mod templates;
33+
mod views;
3234
mod workers;
3335

3436
#[derive(Debug, Parser)]
@@ -82,19 +84,29 @@ async fn main() -> Result<()> {
8284
// Setup image store
8385
let image_store = Arc::new(DbImageStore::new(db.clone()));
8486

85-
// Run some workers
86-
workers::run(db.clone(), &tracker, cancellation_token.clone());
87-
88-
// Setup and launch notifications manager
87+
// Setup notifications manager
8988
let notifications_manager = Arc::new(PgNotificationsManager::new(
9089
db.clone(),
91-
cfg.email,
92-
tracker.clone(),
93-
cancellation_token.clone(),
90+
&cfg.email,
91+
&tracker,
92+
&cancellation_token,
9493
)?);
9594

95+
// Setup views tracker
96+
let views_tracker = Arc::new(ViewsTrackerDB::new(db.clone(), &tracker, &cancellation_token));
97+
98+
// Run some other workers
99+
workers::run(db.clone(), &tracker, cancellation_token.clone());
100+
96101
// Setup and launch HTTP server
97-
let router = router::setup(cfg.server.clone(), db, image_store, notifications_manager).await?;
102+
let router = router::setup(
103+
cfg.server.clone(),
104+
db,
105+
image_store,
106+
notifications_manager,
107+
views_tracker,
108+
)
109+
.await?;
98110
let listener = TcpListener::bind(&cfg.server.addr).await?;
99111
info!("server started");
100112
info!(%cfg.server.addr, "listening");

gitjobs-server/src/notifications.rs

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -48,54 +48,38 @@ pub(crate) type DynNotificationsManager = Arc<dyn NotificationsManager + Send +
4848
/// Notifications manager backed by `PostgreSQL`.
4949
pub(crate) struct PgNotificationsManager {
5050
db: DynDB,
51-
cfg: EmailConfig,
52-
tracker: TaskTracker,
53-
cancellation_token: CancellationToken,
5451
}
5552

5653
impl PgNotificationsManager {
5754
/// Create a new `PgNotificationsManager` instance.
5855
pub(crate) fn new(
5956
db: DynDB,
60-
cfg: EmailConfig,
61-
tracker: TaskTracker,
62-
cancellation_token: CancellationToken,
57+
cfg: &EmailConfig,
58+
tracker: &TaskTracker,
59+
cancellation_token: &CancellationToken,
6360
) -> Result<Self> {
64-
let notifications_manager = Self {
65-
db,
66-
cfg,
67-
tracker,
68-
cancellation_token,
69-
};
70-
notifications_manager.run()?;
71-
72-
Ok(notifications_manager)
73-
}
74-
75-
/// Run notifications manager.
76-
fn run(&self) -> Result<()> {
7761
// Setup smtp client
78-
let smtp_client = AsyncSmtpTransport::<Tokio1Executor>::relay(&self.cfg.smtp.host)?
62+
let smtp_client = AsyncSmtpTransport::<Tokio1Executor>::relay(&cfg.smtp.host)?
7963
.credentials(Credentials::new(
80-
self.cfg.smtp.username.clone(),
81-
self.cfg.smtp.password.clone(),
64+
cfg.smtp.username.clone(),
65+
cfg.smtp.password.clone(),
8266
))
8367
.build();
8468

8569
// Setup and run some workers to deliver notifications
8670
for _ in 1..=NUM_WORKERS {
8771
let mut worker = Worker {
88-
db: self.db.clone(),
89-
cfg: self.cfg.clone(),
72+
db: db.clone(),
73+
cfg: cfg.clone(),
9074
smtp_client: smtp_client.clone(),
91-
cancellation_token: self.cancellation_token.clone(),
75+
cancellation_token: cancellation_token.clone(),
9276
};
93-
self.tracker.spawn(async move {
77+
tracker.spawn(async move {
9478
worker.run().await;
9579
});
9680
}
9781

98-
Ok(())
82+
Ok(Self { db })
9983
}
10084
}
10185

0 commit comments

Comments
 (0)