Initial commit: Rust YpDaemon

This commit is contained in:
Torsten Schulz (local)
2025-11-21 23:05:34 +01:00
commit d0ec363f09
21 changed files with 8067 additions and 0 deletions

View File

@@ -0,0 +1,509 @@
use crate::db::{ConnectionPool, DbError, Row};
use crate::message_broker::MessageBroker;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};
use super::base::{BaseWorker, Worker, WorkerState};
pub struct ValueRecalculationWorker {
base: BaseWorker,
}
// Produktwissen / Produktions-Logs
const QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER: &str = r#"
UPDATE falukant_data.knowledge k
SET knowledge = LEAST(100, k.knowledge + 1)
FROM falukant_data.character c
JOIN falukant_log.production p
ON DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day'
WHERE c.id = k.character_id
AND c.user_id = 18
AND k.product_id = 10;
"#;
const QUERY_DELETE_OLD_PRODUCTIONS: &str = r#"
DELETE FROM falukant_log.production flp
WHERE DATE(flp.production_timestamp) < CURRENT_DATE;
"#;
const QUERY_GET_PRODUCERS_LAST_DAY: &str = r#"
SELECT p.producer_id
FROM falukant_log.production p
WHERE DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day'
GROUP BY producer_id;
"#;
// Regionale Verkaufspreise
const QUERY_UPDATE_REGION_SELL_PRICE: &str = r#"
UPDATE falukant_data.town_product_worth tpw
SET worth_percent =
GREATEST(
0,
LEAST(
CASE
WHEN s.quantity > avg_sells THEN tpw.worth_percent - 1
WHEN s.quantity < avg_sells THEN tpw.worth_percent + 1
ELSE tpw.worth_percent
END,
100
)
)
FROM (
SELECT region_id,
product_id,
quantity,
(SELECT AVG(quantity)
FROM falukant_log.sell avs
WHERE avs.product_id = s.product_id) AS avg_sells
FROM falukant_log.sell s
WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day'
) s
WHERE tpw.region_id = s.region_id
AND tpw.product_id = s.product_id;
"#;
const QUERY_DELETE_REGION_SELL_PRICE: &str = r#"
DELETE FROM falukant_log.sell s
WHERE DATE(s.sell_timestamp) < CURRENT_DATE;
"#;
const QUERY_GET_SELL_REGIONS: &str = r#"
SELECT s.region_id
FROM falukant_log.sell s
WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day'
GROUP BY region_id;
"#;
// Ehen / Beziehungen
const QUERY_SET_MARRIAGES_BY_PARTY: &str = r#"
WITH updated_relations AS (
UPDATE falukant_data.relationship AS rel
SET relationship_type_id = (
SELECT id
FROM falukant_type.relationship AS rt
WHERE rt.tr = 'married'
)
WHERE rel.id IN (
SELECT rel2.id
FROM falukant_data.party AS p
JOIN falukant_type.party AS pt
ON pt.id = p.party_type_id
AND pt.tr = 'wedding'
JOIN falukant_data.falukant_user AS fu
ON fu.id = p.falukant_user_id
JOIN falukant_data.character AS c
ON c.user_id = fu.id
JOIN falukant_data.relationship AS rel2
ON rel2.character1_id = c.id
OR rel2.character2_id = c.id
JOIN falukant_type.relationship AS rt2
ON rt2.id = rel2.relationship_type_id
AND rt2.tr = 'engaged'
WHERE p.created_at <= NOW() - INTERVAL '1 day'
)
RETURNING character1_id, character2_id
)
SELECT
c1.user_id AS character1_user,
c2.user_id AS character2_user
FROM updated_relations AS ur
JOIN falukant_data.character AS c1
ON c1.id = ur.character1_id
JOIN falukant_data.character AS c2
ON c2.id = ur.character2_id;
"#;
// Lernen / Studium
const QUERY_GET_STUDYINGS_TO_EXECUTE: &str = r#"
SELECT
l.id,
l.associated_falukant_user_id,
l.associated_learning_character_id,
l.learn_all_products,
l.learning_recipient_id,
l.product_id,
lr.tr
FROM falukant_data.learning l
JOIN falukant_type.learn_recipient lr
ON lr.id = l.learning_recipient_id
WHERE l.learning_is_executed = FALSE
AND l.created_at + INTERVAL '1 day' < NOW();
"#;
const QUERY_GET_OWN_CHARACTER_ID: &str = r#"
SELECT id
FROM falukant_data.character c
WHERE c.user_id = $1;
"#;
const QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE: &str = r#"
UPDATE falukant_data.knowledge k
SET knowledge = LEAST(100, k.knowledge + $1)
WHERE k.character_id = $2
AND k.product_id = $3;
"#;
const QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE: &str = r#"
UPDATE falukant_data.knowledge k
SET knowledge = LEAST(100, k.knowledge + $1)
WHERE k.character_id = $2;
"#;
const QUERY_SET_LEARNING_DONE: &str = r#"
UPDATE falukant_data.learning
SET learning_is_executed = TRUE
WHERE id = $1;
"#;
impl ValueRecalculationWorker {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
Self {
base: BaseWorker::new("ValueRecalculationWorker", pool, broker),
}
}
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
// Wir nutzen hier einfach Intervall-Logik (täglich / halbtäglich),
// statt exakte Uhrzeiten nachzubilden Verhalten ist funktional ähnlich.
let mut last_product = None;
let mut last_sell_price = None;
loop {
if !state.running_worker.load(Ordering::Relaxed) {
break;
}
let now = Instant::now();
// Produktwissen einmal täglich
if should_run_interval(last_product, now, Duration::from_secs(24 * 3600)) {
if let Err(err) = Self::calculate_product_knowledge_inner(&pool, &broker) {
eprintln!("[ValueRecalculationWorker] Fehler in calculateProductKnowledge: {err}");
}
last_product = Some(now);
}
// Regionale Verkaufspreise einmal täglich (gegen Mittag)
if should_run_interval(last_sell_price, now, Duration::from_secs(24 * 3600)) {
if let Err(err) = Self::calculate_regional_sell_price_inner(&pool, &broker) {
eprintln!("[ValueRecalculationWorker] Fehler in calculateRegionalSellPrice: {err}");
}
last_sell_price = Some(now);
}
// Ehen & Studium bei jedem Durchlauf
if let Err(err) = Self::calculate_marriages_inner(&pool, &broker) {
eprintln!("[ValueRecalculationWorker] Fehler in calculateMarriages: {err}");
}
if let Err(err) = Self::calculate_studying_inner(&pool, &broker) {
eprintln!("[ValueRecalculationWorker] Fehler in calculateStudying: {err}");
}
// 60-Sekunden-Wartezeit in kurze Scheiben aufteilen, damit ein Shutdown
// (running_worker = false) schnell greift.
const SLICE_MS: u64 = 500;
let total_ms = 60_000;
let mut slept = 0;
while slept < total_ms {
if !state.running_worker.load(Ordering::Relaxed) {
break;
}
let remaining = total_ms - slept;
let slice = SLICE_MS.min(remaining);
std::thread::sleep(Duration::from_millis(slice));
slept += slice;
}
}
}
fn calculate_product_knowledge_inner(
pool: &ConnectionPool,
broker: &MessageBroker,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(
"update_product_knowledge_user",
QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER,
)?;
conn.execute("update_product_knowledge_user", &[])?;
conn.prepare("get_producers_last_day", QUERY_GET_PRODUCERS_LAST_DAY)?;
let users = conn.execute("get_producers_last_day", &[])?;
for row in users {
if let Some(user_id) = row.get("producer_id").and_then(|v| v.parse::<i32>().ok()) {
let message = format!(r#"{{"event":"price_update","user_id":{}}}"#, user_id);
broker.publish(message);
}
}
conn.prepare("delete_old_productions", QUERY_DELETE_OLD_PRODUCTIONS)?;
conn.execute("delete_old_productions", &[])?;
Ok(())
}
fn calculate_regional_sell_price_inner(
pool: &ConnectionPool,
broker: &MessageBroker,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("update_region_sell_price", QUERY_UPDATE_REGION_SELL_PRICE)?;
conn.execute("update_region_sell_price", &[])?;
conn.prepare("get_sell_regions", QUERY_GET_SELL_REGIONS)?;
let regions = conn.execute("get_sell_regions", &[])?;
for row in regions {
if let Some(region_id) = row.get("region_id").and_then(|v| v.parse::<i32>().ok()) {
let message =
format!(r#"{{"event":"price_update","region_id":{}}}"#, region_id);
broker.publish(message);
}
}
conn.prepare("delete_region_sell_price", QUERY_DELETE_REGION_SELL_PRICE)?;
conn.execute("delete_region_sell_price", &[])?;
Ok(())
}
fn calculate_marriages_inner(
pool: &ConnectionPool,
broker: &MessageBroker,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("set_marriages_by_party", QUERY_SET_MARRIAGES_BY_PARTY)?;
let rows = conn.execute("set_marriages_by_party", &[])?;
for row in rows {
if let Some(uid) =
row.get("character1_user").and_then(|v| v.parse::<i32>().ok())
{
let msg =
format!(r#"{{"event":"relationship_changed","user_id":{}}}"#, uid);
broker.publish(msg);
}
if let Some(uid) =
row.get("character2_user").and_then(|v| v.parse::<i32>().ok())
{
let msg =
format!(r#"{{"event":"relationship_changed","user_id":{}}}"#, uid);
broker.publish(msg);
}
}
Ok(())
}
fn calculate_studying_inner(
pool: &ConnectionPool,
broker: &MessageBroker,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(
"get_studyings_to_execute",
QUERY_GET_STUDYINGS_TO_EXECUTE,
)?;
conn.prepare("set_learning_done", QUERY_SET_LEARNING_DONE)?;
let studies = conn.execute("get_studyings_to_execute", &[])?;
for study in studies {
let tr = study.get("tr").cloned().unwrap_or_default();
match tr.as_str() {
"self" => Self::calculate_studying_self(pool, broker, &study)?,
"children" | "director" => {
Self::calculate_studying_for_associated_character(
pool, broker, &study,
)?
}
_ => {}
}
if let Some(id) = study.get("id").and_then(|v| v.parse::<i32>().ok()) {
conn.execute("set_learning_done", &[&id])?;
}
}
Ok(())
}
fn calculate_studying_self(
pool: &ConnectionPool,
broker: &MessageBroker,
entry: &Row,
) -> Result<(), DbError> {
let falukant_user_id = match entry
.get("associated_falukant_user_id")
.and_then(|v| v.parse::<i32>().ok())
{
Some(id) => id,
None => return Ok(()),
};
let (learn_all, product_id) = study_scope(entry);
let character_id = Self::get_own_character_id(pool, falukant_user_id)?;
if let Some(cid) = character_id {
Self::calculate_studying_character(
pool,
broker,
cid,
learn_all,
product_id,
parse_i32(entry, "learning_recipient_id", -1),
)?;
}
Ok(())
}
fn calculate_studying_for_associated_character(
pool: &ConnectionPool,
broker: &MessageBroker,
entry: &Row,
) -> Result<(), DbError> {
let character_id = parse_i32(entry, "associated_learning_character_id", -1);
if character_id < 0 {
return Ok(());
}
let (learn_all, product_id) = study_scope(entry);
let recipient_id = parse_i32(entry, "learning_recipient_id", -1);
Self::calculate_studying_character(
pool,
broker,
character_id,
learn_all,
product_id,
recipient_id,
)
}
fn get_own_character_id(
pool: &ConnectionPool,
falukant_user_id: i32,
) -> Result<Option<i32>, DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("get_own_character_id", QUERY_GET_OWN_CHARACTER_ID)?;
let rows = conn.execute("get_own_character_id", &[&falukant_user_id])?;
Ok(rows
.get(0)
.and_then(|r| r.get("id"))
.and_then(|v| v.parse::<i32>().ok()))
}
fn calculate_studying_character(
pool: &ConnectionPool,
broker: &MessageBroker,
character_id: i32,
learn_all: bool,
product_id: Option<i32>,
falukant_user_id: i32,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
if learn_all {
conn.prepare(
"increase_all_products_knowledge",
QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE,
)?;
conn.execute(
"increase_all_products_knowledge",
&[&1_i32, &character_id],
)?;
} else if let Some(pid) = product_id {
conn.prepare(
"increase_one_product_knowledge",
QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE,
)?;
conn.execute(
"increase_one_product_knowledge",
&[&5_i32, &character_id, &pid],
)?;
}
let message =
format!(r#"{{"event":"knowledge_updated","user_id":{}}}"#, falukant_user_id);
broker.publish(message);
Ok(())
}
}
impl Worker for ValueRecalculationWorker {
fn start_worker_thread(&mut self) {
let pool = self.base.pool.clone();
let broker = self.base.broker.clone();
self.base
.start_worker_with_loop(move |state: Arc<WorkerState>| {
ValueRecalculationWorker::run_loop(pool.clone(), broker.clone(), state);
});
}
fn stop_worker_thread(&mut self) {
self.base.stop_worker();
}
fn enable_watchdog(&mut self) {
self.base.start_watchdog();
}
}
fn should_run_interval(
last_run: Option<Instant>,
now: Instant,
interval: Duration,
) -> bool {
match last_run {
None => true,
Some(prev) => now.saturating_duration_since(prev) >= interval,
}
}
fn parse_i32(row: &Row, key: &str, default: i32) -> i32 {
row.get(key)
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(default)
}
fn study_scope(entry: &Row) -> (bool, Option<i32>) {
let learn_all_flag =
entry.get("learn_all_products").map(|v| v == "t").unwrap_or(false);
let product_id_str = entry.get("product_id").cloned().unwrap_or_default();
if learn_all_flag || product_id_str.is_empty() {
(true, None)
} else {
let pid = product_id_str.parse::<i32>().ok();
match pid {
Some(id) => (false, Some(id)),
None => (true, None),
}
}
}