structure designed & storage is done
This commit is contained in:
parent
da14c2cae8
commit
a387dd48d1
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
config/
|
||||
venv/*
|
||||
**/target/
|
||||
.vscode/
|
||||
12
README.md
12
README.md
@ -1,2 +1,14 @@
|
||||
# bill-gates
|
||||
|
||||
运行本项目前,确保系统上已配置好以下软件:
|
||||
```
|
||||
bash
|
||||
python3
|
||||
xmake
|
||||
```
|
||||
|
||||
## 运行方法:
|
||||
直接执行start.sh
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
31
build.sh
Executable file
31
build.sh
Executable file
@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
BLUE='\033[0;34m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print timestamp
|
||||
timestamp(){
|
||||
date "+%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
|
||||
# Begin build process
|
||||
echo -e "${BLUE}$(timestamp)[BUILD]${NC}"
|
||||
|
||||
# if any of the commands fail, exit
|
||||
set -e
|
||||
# set up python venv
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Satisfy python dependencies
|
||||
python3 -m pip install -r gateway/requirements.txt
|
||||
|
||||
echo -e "${GREEN}$(timestamp)[BUILD FINISHED]${NC}"
|
||||
|
||||
|
||||
# Build storage crate
|
||||
pushd storage
|
||||
cargo build --release
|
||||
popd
|
||||
|
||||
# Run the project
|
||||
32
datasource/gateway.py
Normal file
32
datasource/gateway.py
Normal file
@ -0,0 +1,32 @@
|
||||
import tushare as ts
|
||||
import pandas as pd
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Read config from config.yaml
|
||||
def read_config():
|
||||
import yaml
|
||||
with open('config/config.yaml', 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
|
||||
# Ensure the config has the required keys
|
||||
if 'gateway' not in config or 'token' not in config['gateway']:
|
||||
raise KeyError("Missing required config key: gateway.token")
|
||||
return config
|
||||
def main():
|
||||
config = read_config()
|
||||
token = config['gateway']['token']
|
||||
|
||||
# Initialize Tushare with the token
|
||||
ts.set_token(token)
|
||||
pro = ts.pro_api()
|
||||
|
||||
# Get the last trading day
|
||||
last_trading_day = (datetime.now() - timedelta(days=1)).strftime('%Y%m%d')
|
||||
|
||||
# Fetch stock data for the last trading day
|
||||
df = pro.daily(trade_date=last_trading_day)
|
||||
|
||||
# Print the DataFrame
|
||||
print(df)
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
datasource/job.py
Normal file
0
datasource/job.py
Normal file
0
datasource/main.py
Normal file
0
datasource/main.py
Normal file
1
datasource/requirements.txt
Normal file
1
datasource/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
tushare
|
||||
0
datasource/rpc.py
Normal file
0
datasource/rpc.py
Normal file
0
datasource/storage.py
Normal file
0
datasource/storage.py
Normal file
0
gateway/gateway.py
Normal file
0
gateway/gateway.py
Normal file
1743
storage/Cargo.lock
generated
Normal file
1743
storage/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
storage/Cargo.toml
Normal file
16
storage/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "storage"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
saphyr = "0.0.6"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
axum = "0.8"
|
||||
serde_json = "1.0"
|
||||
home = "0.5"
|
||||
serde = {version = "1.0", features = ["derive"]}
|
||||
moka = {version = "0.12",features = ["future"]}
|
||||
rocksdb ={ version = "0.23"}
|
||||
152
storage/src/main.rs
Normal file
152
storage/src/main.rs
Normal file
@ -0,0 +1,152 @@
|
||||
use std::{env, fs, net::TcpListener, path::Path};
|
||||
use std::process::exit;
|
||||
use saphyr::{LoadableYamlNode, Yaml};
|
||||
use log::{info, error};
|
||||
use env_logger;
|
||||
use tokio::sync::mpsc;
|
||||
use home::home_dir;
|
||||
mod rpc;
|
||||
mod msg;
|
||||
mod storage;
|
||||
#[derive(Debug)]
|
||||
struct Config {
|
||||
rpc_url: String,
|
||||
lru_timeout: u64,
|
||||
lru_capacity: u64,
|
||||
lru_path: String,
|
||||
}
|
||||
|
||||
fn parse_capacity(s: &str) -> Result<u64, String> {
|
||||
let s = s.trim().to_uppercase();
|
||||
match s {
|
||||
s if s.ends_with("GB") => {
|
||||
match s.strip_suffix("GB") {
|
||||
Some(num) => num.trim().parse::<u64>().map_err(|e| e.to_string()).map(|n| n * 1024 * 1024 * 1024),
|
||||
None => Err("Invalid GB format".to_string()),
|
||||
}
|
||||
}
|
||||
s if s.ends_with("MB") => {
|
||||
match s.strip_suffix("MB") {
|
||||
Some(num) => num.trim().parse::<u64>().map_err(|e| e.to_string()).map(|n| n * 1024 * 1024),
|
||||
None => Err("Invalid MB format".to_string()),
|
||||
}
|
||||
}
|
||||
s if s.ends_with("KB") => {
|
||||
match s.strip_suffix("KB") {
|
||||
Some(num) => num.trim().parse::<u64>().map_err(|e| e.to_string()).map(|n| n * 1024),
|
||||
None => Err("Invalid KB format".to_string()),
|
||||
}
|
||||
}
|
||||
_ => s.parse::<u64>().map_err(|e| e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_config(config_path: &str) -> Result<Config, String> {
|
||||
let config_str = fs::read_to_string(config_path)
|
||||
.map_err(|e| format!("Failed to read {}: {}", config_path, e))?;
|
||||
let docs = Yaml::load_from_str(&config_str)
|
||||
.map_err(|e| format!("Failed to parse {}: {}", config_path, e))?;
|
||||
let doc = &docs[0];
|
||||
let storage = &doc["storage"];
|
||||
let rpc_url = storage["rpc"]["url"]
|
||||
.as_str()
|
||||
.ok_or("storage.rpc.url is missing or not a string")?
|
||||
.to_string();
|
||||
let lru_timeout = storage["lru"]["timeout"]
|
||||
.as_integer()
|
||||
.ok_or("storage.lru.timeout is missing or not an integer")? as u64;
|
||||
let lru_capacity_str = storage["lru"]["capacity"]
|
||||
.as_str()
|
||||
.ok_or("storage.lru.capacity is missing or not a string")?;
|
||||
let lru_capacity = parse_capacity(lru_capacity_str)
|
||||
.map_err(|e| format!("storage.lru.capacity parse error: {}", e))?;
|
||||
let lru_path = storage["lru"]["path"]
|
||||
.as_str()
|
||||
.ok_or("storage.lru.path is missing or not a string")?
|
||||
.to_string();
|
||||
let lru_path = expand_home(&lru_path);
|
||||
|
||||
if rpc_url.is_empty() {
|
||||
return Err("storage.rpc.url is empty".to_string());
|
||||
}
|
||||
if lru_timeout == 0 {
|
||||
return Err("storage.lru.timeout must be > 0".to_string());
|
||||
}
|
||||
if lru_capacity == 0 {
|
||||
return Err("storage.lru.capacity must be > 0".to_string());
|
||||
}
|
||||
if lru_path.is_empty() {
|
||||
return Err("storage.lru.path is empty".to_string());
|
||||
}
|
||||
if TcpListener::bind(&rpc_url).is_err() {
|
||||
return Err(format!("Cannot bind to address: {}", rpc_url));
|
||||
}
|
||||
let db_dir = Path::new(&lru_path);
|
||||
if !db_dir.exists() {
|
||||
if let Err(e) = fs::create_dir_all(db_dir) {
|
||||
return Err(format!("Cannot create rocksDB directory {}: {}", lru_path, e));
|
||||
}
|
||||
}
|
||||
let test_file = db_dir.join(".test_write");
|
||||
if fs::write(&test_file, b"test").is_err() {
|
||||
return Err(format!("Cannot write to rocksDB directory: {}", lru_path));
|
||||
}
|
||||
let _ = fs::remove_file(&test_file);
|
||||
|
||||
Ok(Config { rpc_url, lru_timeout, lru_capacity, lru_path })
|
||||
}
|
||||
|
||||
fn expand_home(path: &str) -> String {
|
||||
match path.strip_prefix("~/") {
|
||||
Some(stripped) => match home_dir() {
|
||||
Some(home) => home.join(stripped).to_string_lossy().to_string(),
|
||||
None => path.to_string(),
|
||||
},
|
||||
None => path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
use storage::storage_task;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
// 解析命令行参数
|
||||
let mut args = env::args().skip(1);
|
||||
let mut config_path = "config/config.yaml".to_string();
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == "-c" {
|
||||
if let Some(path) = args.next() {
|
||||
config_path = path;
|
||||
} else {
|
||||
error!("FATAL: -c option requires a path argument");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let config = match read_config(&config_path) {
|
||||
Ok(cfg) => {
|
||||
info!("Config loaded and validated: {:?}", cfg);
|
||||
cfg
|
||||
}
|
||||
Err(e) => {
|
||||
error!("FATAL: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建 channel
|
||||
let (tx, rx) = mpsc::channel(128);
|
||||
|
||||
let mem_bytes = config.lru_capacity;
|
||||
let ttl_secs = config.lru_timeout;
|
||||
let db_path = config.lru_path.clone();
|
||||
|
||||
// 启动 storage 任务
|
||||
tokio::spawn(storage_task(rx, mem_bytes, ttl_secs, db_path));
|
||||
|
||||
// 启动 RPC 服务
|
||||
rpc::start_rpc_server(&config.rpc_url, tx).await;
|
||||
}
|
||||
8
storage/src/msg.rs
Normal file
8
storage/src/msg.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum StorageMsg {
|
||||
Get { key: String },
|
||||
Set { key: String, value: Vec<u8> },
|
||||
}
|
||||
84
storage/src/rpc.rs
Normal file
84
storage/src/rpc.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use axum::{
|
||||
extract::{ConnectInfo, State}, http::StatusCode, routing::post, Json, Router
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use std::sync::Arc;
|
||||
use log::{info, error};
|
||||
use std::net::{SocketAddr, IpAddr};
|
||||
use crate::msg::StorageMsg;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RPCState {
|
||||
pub tx: Sender<StorageMsg>,
|
||||
pub allowed_host: String,
|
||||
}
|
||||
|
||||
pub async fn start_rpc_server(addr: &str, tx: Sender<StorageMsg>) {
|
||||
// 拆分host和port
|
||||
let (host_part, port_part) = match addr.rsplit_once(':') {
|
||||
Some((h, p)) => (h.to_string(), p.parse::<u16>().expect("Invalid port")),
|
||||
None => panic!("Invalid addr format, expected host:port"),
|
||||
};
|
||||
|
||||
// 判断host是否为IP
|
||||
let listen_ip = if host_part.parse::<IpAddr>().is_ok() {
|
||||
host_part.clone()
|
||||
} else {
|
||||
"0.0.0.0".to_string()
|
||||
};
|
||||
|
||||
let listen_addr = format!("{}:{}", listen_ip, port_part);
|
||||
|
||||
let state = Arc::new(RPCState {
|
||||
tx,
|
||||
allowed_host: host_part,
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", post(handle_rpc))
|
||||
.with_state(state)
|
||||
.into_make_service_with_connect_info::<SocketAddr>();
|
||||
|
||||
info!("HTTP JSON-RPC server listening on {}", listen_addr);
|
||||
let listener=tokio::net::TcpListener::bind(listen_addr).await.unwrap();
|
||||
axum::serve(listener,app).await.unwrap();
|
||||
}
|
||||
|
||||
async fn handle_rpc(
|
||||
State(state): State<Arc<RPCState>>,
|
||||
ConnectInfo(peer_addr): ConnectInfo<SocketAddr>,
|
||||
Json(payload): Json<Value>,
|
||||
) -> (StatusCode, Json<Value>) {
|
||||
let allowed = state.allowed_host == "0.0.0.0"
|
||||
|| peer_addr.ip().to_string() == state.allowed_host;
|
||||
if !allowed {
|
||||
error!("Connection from {} not allowed", peer_addr);
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(serde_json::json!({"error": "host/port not allowed"}))
|
||||
);
|
||||
}
|
||||
|
||||
let msg: Result<StorageMsg, _> = serde_json::from_value(payload);
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
// msg is StorageMsg
|
||||
if let Err(e) = state.tx.send(msg).await {
|
||||
error!("Failed to send request to storage: {}", e);
|
||||
return (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Json(serde_json::json!({"error": "internal error"}))
|
||||
);
|
||||
}
|
||||
(StatusCode::OK, Json(serde_json::json!({"status": "ok"})))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Invalid message: {}", e);
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({"error": "invalid message"}))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
103
storage/src/storage.rs
Normal file
103
storage/src/storage.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use crate::msg::StorageMsg;
|
||||
use log::{error, info};
|
||||
use moka::future::Cache;
|
||||
use rocksdb::{DB, Options};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
|
||||
pub struct Storage {
|
||||
cache: Cache<String, Vec<u8>>,
|
||||
db: Arc<DB>,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
pub fn new(mem_bytes: u64, ttl_secs: u64, db_path: &str) -> Self {
|
||||
let mut opts = Options::default();
|
||||
opts.create_if_missing(true);
|
||||
info!("Opening RocksDB at {}", db_path);
|
||||
let db = Arc::new(DB::open(&opts, db_path).expect("open rocksdb failed"));
|
||||
let _db = db.clone();
|
||||
info!("Initializing in-memory cache: max_capacity={} bytes, ttl={} secs", mem_bytes, ttl_secs);
|
||||
let cache = Cache::builder()
|
||||
.max_capacity(mem_bytes / 128)
|
||||
.time_to_live(Duration::from_secs(ttl_secs))
|
||||
.eviction_listener(move |key: Arc<String>, value, _cause| {
|
||||
let db = db.clone();
|
||||
let key = key.clone();
|
||||
info!("Evicting key from memory: {} (persisting to RocksDB)", key);
|
||||
if let Err(e) = db.put(key.as_bytes(), value) {
|
||||
error!("Failed to persist evicted key to rocksdb: {}", e);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
info!("Storage initialized");
|
||||
Storage { cache, db: _db }
|
||||
}
|
||||
|
||||
pub async fn get(&self, key: &String) -> Option<Vec<u8>> {
|
||||
if let Some(val) = self.cache.get(key).await {
|
||||
info!("Cache hit for key: {}", key);
|
||||
return Some(val);
|
||||
}
|
||||
info!("Cache miss for key: {}, loading from RocksDB", key);
|
||||
let db = self.db.clone();
|
||||
let key_owned = key.clone();
|
||||
let v = tokio::task::spawn_blocking(move || db.get(key_owned.as_bytes()).ok().flatten())
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(bytes) = v.clone() {
|
||||
info!("Loaded key from RocksDB: {} ({} bytes)", key, bytes.len());
|
||||
self.cache.insert(key.clone(), bytes.clone()).await;
|
||||
return Some(bytes);
|
||||
}
|
||||
info!("Key not found in RocksDB: {}", key);
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn set(&self, key: String, value: Vec<u8>) {
|
||||
info!("Set key: {} ({} bytes)", key, value.len());
|
||||
self.cache.insert(key.clone(), value.clone()).await;
|
||||
let db = self.db.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let res = db.put(key.as_bytes(), value);
|
||||
if let Err(e) = res {
|
||||
error!("Failed to persist key to rocksdb (set): {}", e);
|
||||
} else {
|
||||
info!("Persisted key to rocksdb (set)");
|
||||
}
|
||||
}).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn storage_task(
|
||||
mut rx: Receiver<StorageMsg>,
|
||||
mem_bytes: u64,
|
||||
ttl_secs: u64,
|
||||
db_path: String,
|
||||
) {
|
||||
let storage = Storage::new(mem_bytes, ttl_secs, &db_path);
|
||||
// storage.register_eviction();
|
||||
|
||||
while let Some(msg) = rx.recv().await {
|
||||
match msg {
|
||||
StorageMsg::Get { key } => {
|
||||
let result = storage.get(&key).await;
|
||||
info!(
|
||||
"GET key={} result={:?}",
|
||||
key,
|
||||
result.as_ref().map(|v| v.len())
|
||||
);
|
||||
// TODO: 返回结果给请求方(可用oneshot channel等)
|
||||
}
|
||||
StorageMsg::Set { key, value } => {
|
||||
storage.set(key, value).await;
|
||||
info!("SET key");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
0
strategy/xmake.lua
Normal file
0
strategy/xmake.lua
Normal file
Loading…
Reference in New Issue
Block a user