Compare commits

...

20 Commits

Author SHA1 Message Date
323dd43c5b
release: 🔖 v0.4.1 2025-03-29 07:10:58 -03:00
4629a02398
feat: add active status to ticker model and related services 2025-03-29 07:10:25 -03:00
13ea0619cd
release: 🔖 v0.4.0 2025-03-28 12:49:47 -03:00
e9427b769e
chore: 🧹 rename RUSTLER_DATABASE to DATABASE_URL in connection handling 2025-03-28 12:47:58 -03:00
ecb8d874c2
chore: 🧹 update registry name in release.toml 2025-03-28 09:06:43 -03:00
287f095294
release: 🔖 v0.3.11 2025-03-28 09:06:04 -03:00
9570293588
chore: 🧹 update lool dependency version to ^0.9.0 2025-03-28 09:00:46 -03:00
4582987aba
chore: 🧹 update Cargo registry registry URLs 2025-03-28 08:53:59 -03:00
8384b56f1f
release: 🔖 v0.3.10 2025-03-28 03:00:00 -03:00
c8d254e6c4
chore: 🧹 update dependencies in Cargo.toml 2025-03-28 02:58:18 -03:00
9d0a066a80
release: 🔖 v0.3.9 2024-06-21 16:36:58 -03:00
d79237b103
feat: don't connect if already connected or connecting 2024-06-21 16:36:42 -03:00
9d6a0476aa
release: 🔖 v0.3.8 2024-06-21 15:36:01 -03:00
ad3a3bd1c5
feat: add is_* functions tu Rustler for status
adds:
  - is_connecting
  - is_connected
  - is_disconnecting
  - is_disconnected
2024-06-21 15:35:20 -03:00
b35cbe95ee
refactor: 🔨 remove comments about callbacks 2024-06-21 12:56:39 -03:00
7849458963
docs: 📝 improve docs for functions in the Rustler trait 2024-06-07 12:56:28 -03:00
d1305e6a63
release: 🔖 v0.3.7 2024-06-07 11:26:31 -03:00
ecfaf6f1a1
fix: 🚑 crash when not rustler available for a market 2024-06-07 11:26:02 -03:00
321ddd6bcb
release: 🔖 v0.3.6 2024-06-07 11:19:23 -03:00
8a8f47a859
fix: 🚑 run rustler immediately when no rules configured 2024-06-07 11:17:35 -03:00
22 changed files with 1410 additions and 885 deletions

View File

@ -1,8 +1,6 @@
[registries.lugit] [registries.lugit]
index = "sparse+http://lugit.local/api/packages/lucodear/cargo/" index = "sparse+https://git.lucode.dev/api/packages/lucas/cargo/"
[registries.lugit-sa]
index = "sparse+http://lugit.local/api/packages/singleaction/cargo/"
[registry] [registry]
global-credential-providers = ["cargo:token"] global-credential-providers = ["cargo:token"]

View File

@ -1,11 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 116 54"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 118 54">
<style> <style>
.a { fill: #000000; } .a { fill: #000000; }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.a { fill: #ffffff; } .a { fill: #ffffff; }
} }
</style> </style>
<path class="a" d="M70,26H68a4,4,0,0,1,4,4H68a3.99443,3.99443,0,0,1-3.30475-1.74884A11.95966,11.95966,0,0,1,56,32H50.5a.5.5,0,0,1-.5-.5v-11a.5.5,0,0,1,.5-.5H67.48285l.0144.00018a.50528.50528,0,0,1,.49176.51831,12.03909,12.03909,0,0,1-.312,2.236A3.97,3.97,0,0,1,70,22h4A4,4,0,0,1,70,26Zm6-10H42V12h8V4.5a.5.5,0,0,1,.5-.5H68v8h8Z" />
<path class="a" d="M10,46H22v2H10Zm84,0h12v2H94Z" /> <path class="a" d="M70,26H68a4,4,0,0,1,4,4H68a3.99443,3.99443,0,0,1-3.30475-1.74884A11.95966,11.95966,0,0,1,56,32H50.5a.5.5,0,0,1-.5-.5v-11a.5.5,0,0,1,.5-.5H67.48285l.0144.00018a.50528.50528,0,0,1,.49176.51831,12.03993,12.03993,0,0,1-.312,2.236A3.97,3.97,0,0,1,70,22h4A4,4,0,0,1,70,26Zm6-10H42V12h8V4.5a.5.5,0,0,1,.5-.5H68v8h8Z" />
<path class="a" d="M30.84546,48.46484h1.42773v-3.4082H30.84546V43.66406h3.20361v1.752h.084a3.55107,3.55107,0,0,1,.23437-.65332,2.13106,2.13106,0,0,1,.38379-.56446,1.69883,1.69883,0,0,1,.56982-.38965,2.00651,2.00651,0,0,1,.792-.14453h.58838v1.63282H35.3811a1.24421,1.24421,0,0,0-.9956.38769,1.40477,1.40477,0,0,0-.33643.95117v1.8291h2.04v1.39161H30.84546Zm10.91016.26368h-.07227a2.20387,2.20387,0,0,1-.6416.90039A1.77169,1.77169,0,0,1,39.85962,50a2.16822,2.16822,0,0,1-.81592-.14941,1.65809,1.65809,0,0,1-.63623-.4502,2.18,2.18,0,0,1-.41406-.73242,3.02043,3.02043,0,0,1-.1499-.99512V43.66406h1.77587v3.76856q0,1.1997,1.044,1.20019a1.5192,1.5192,0,0,0,.40234-.05469,1.00553,1.00553,0,0,0,.34766-.168.92264.92264,0,0,0,.24609-.28125.78686.78686,0,0,0,.0962-.39649V43.66406h1.77587v6.19239H41.75562ZM47.67358,50a4.71586,4.71586,0,0,1-1.79394-.31152,2.54291,2.54291,0,0,1-1.146-.85157l.9961-.9248a2.566,2.566,0,0,0,.84619.61816,2.6358,2.6358,0,0,0,1.10986.22266,1.76649,1.76649,0,0,0,.82813-.16211.53185.53185,0,0,0,.2998-.498.37347.37347,0,0,0-.2041-.36621,2.03377,2.03377,0,0,0-.564-.16211l-.99609-.15625a4.12407,4.12407,0,0,1-.77978-.18554,2.042,2.042,0,0,1-.624-.3418,1.59818,1.59818,0,0,1-.41992-.52832,1.67581,1.67581,0,0,1-.15625-.75586,1.79276,1.79276,0,0,1,.74414-1.52441,3.446,3.446,0,0,1,2.08789-.55176,4.35406,4.35406,0,0,1,1.5542.24609,2.45377,2.45377,0,0,1,1.02588.70215l-.8877,1.00781a2.26014,2.26014,0,0,0-.708-.5039,2.43517,2.43517,0,0,0-1.04444-.20411q-1.032,0-1.03174.624a.38372.38372,0,0,0,.20411.37793,2.02864,2.02864,0,0,0,.564.16211l.98389.15625a4.12407,4.12407,0,0,1,.77978.18554,2.13007,2.13007,0,0,1,.63037.3418,1.57252,1.57252,0,0,1,.42579.52832,1.67581,1.67581,0,0,1,.15625.75586,1.82005,1.82005,0,0,1-.75,1.53613A3.4907,3.4907,0,0,1,47.67358,50Zm7.56006-.14355a1.85876,1.85876,0,0,1-1.42822-.50293,1.804,1.804,0,0,1-.46777-1.28028v-3.0166H51.65747V43.66406h1.09229a.82345.82345,0,0,0,.564-.15527.77537.77537,0,0,0,.168-.57617V41.48047h1.63184v2.18359h2.4121v1.39258h-2.4121v3.4082h2.4121v1.39161Zm3.60547-1.39161H60.7356V42.36816H58.83911v-1.3916H62.512v7.48828h1.89551v1.39161H58.83911ZM68.64282,50a3.06922,3.06922,0,0,1-2.36328-.86328,3.3174,3.3174,0,0,1-.80469-2.35254,4.14575,4.14575,0,0,1,.21-1.36133,2.93815,2.93815,0,0,1,.60059-1.03223,2.53993,2.53993,0,0,1,.94824-.64843,3.37487,3.37487,0,0,1,1.25391-.22168,3.3327,3.3327,0,0,1,1.24805.22168,2.61169,2.61169,0,0,1,.93554.62988,2.82413,2.82413,0,0,1,.59375.99023,3.80393,3.80393,0,0,1,.21,1.30176v.52832H67.2395v.1084a1.295,1.295,0,0,0,.37207.96582,1.4917,1.4917,0,0,0,1.0918.36621,2.00488,2.00488,0,0,0,.94824-.21,2.2576,2.2576,0,0,0,.6836-.55859l.96,1.04394a3.01449,3.01449,0,0,1-1.00782.76856A3.67242,3.67242,0,0,1,68.64282,50Zm-.14355-5.208a1.216,1.216,0,0,0-.918.35449,1.31281,1.31281,0,0,0-.3418.95411v.0957h2.4961v-.0957a1.34032,1.34032,0,0,0-.33008-.96A1.18957,1.18957,0,0,0,68.49927,44.792Zm4.127,3.67285h1.42871v-3.4082H72.62622V43.66406h3.2041v1.752h.084a3.48128,3.48128,0,0,1,.23437-.65332,2.11952,2.11952,0,0,1,.38379-.56446,1.69117,1.69117,0,0,1,.57031-.38965,2.00144,2.00144,0,0,1,.792-.14453h.58789v1.63282H77.16235a1.24731,1.24731,0,0,0-.99609.38769,1.40683,1.40683,0,0,0-.33594.95117v1.8291h2.04v1.39161H72.62622ZM82.27466,50a4.71416,4.71416,0,0,1-1.79395-.31152,2.53909,2.53909,0,0,1-1.14648-.85157l.99609-.9248a2.57423,2.57423,0,0,0,.84571.61816,2.63962,2.63962,0,0,0,1.11035.22266,1.76779,1.76779,0,0,0,.82812-.16211.531.531,0,0,0,.29981-.498.37423.37423,0,0,0-.2041-.36621,2.035,2.035,0,0,0-.56348-.16211l-.9961-.15625a4.12948,4.12948,0,0,1-.78027-.18554,2.04206,2.04206,0,0,1-.624-.3418,1.59818,1.59818,0,0,1-.41992-.52832,1.66918,1.66918,0,0,1-.15625-.75586,1.79273,1.79273,0,0,1,.74414-1.52441,3.447,3.447,0,0,1,2.08789-.55176,4.34753,4.34753,0,0,1,1.55371.24609,2.44925,2.44925,0,0,1,1.02637.70215l-.8877,1.00781a2.25441,2.25441,0,0,0-.708-.5039,2.43589,2.43589,0,0,0-1.04394-.20411q-1.03272,0-1.03223.624a.38294.38294,0,0,0,.2041.37793,2.03488,2.03488,0,0,0,.56348.16211l.98437.15625a4.1294,4.1294,0,0,1,.78028.18554,2.13369,2.13369,0,0,1,.62988.3418,1.57832,1.57832,0,0,1,.42578.52832,1.68222,1.68222,0,0,1,.15625.75586,1.81881,1.81881,0,0,1-.75,1.53613A3.4881,3.4881,0,0,1,82.27466,50Z" /> <path class="a" d="M10,46H22v2H10Zm86,0h12v2H96Z" />
<path class="a" d="M32,48.46484h1.41957v-3.4082H32V43.66406h3.18531v1.752h.08352a3.56464,3.56464,0,0,1,.233-.65332,2.13156,2.13156,0,0,1,.3816-.56446,1.68872,1.68872,0,0,1,.56657-.38965,1.98485,1.98485,0,0,1,.78747-.14453h.585v1.63282H36.50973a1.23331,1.23331,0,0,0-.98991.38769,1.40971,1.40971,0,0,0-.33451.95117v1.8291h2.02835v1.39161H32Zm10.84784.26368H42.776a2.20362,2.20362,0,0,1-.63793.90039A1.75476,1.75476,0,0,1,40.96267,50a2.14489,2.14489,0,0,1-.81126-.14941,1.64867,1.64867,0,0,1-.6326-.4502,2.18327,2.18327,0,0,1-.41169-.73242,3.03642,3.03642,0,0,1-.14905-.99512V43.66406H40.7238v3.76856q0,1.1997,1.038,1.20019a1.50266,1.50266,0,0,0,.4-.05469.99754.99754,0,0,0,.34567-.168.921.921,0,0,0,.24469-.28125.79052.79052,0,0,0,.09565-.39649V43.66406h1.76572v6.19239H42.84784ZM48.732,50a4.665,4.665,0,0,1-1.78369-.31152,2.52956,2.52956,0,0,1-1.13945-.85157l.99041-.9248a2.552,2.552,0,0,0,.84136.61816,2.60788,2.60788,0,0,0,1.10352.22266,1.74774,1.74774,0,0,0,.82339-.16211.53224.53224,0,0,0,.29809-.498.3739.3739,0,0,0-.20293-.36621,2.01314,2.01314,0,0,0-.56078-.16211l-.9904-.15625a4.08093,4.08093,0,0,1-.77533-.18554,2.02692,2.02692,0,0,1-.62043-.3418,1.59678,1.59678,0,0,1-.41753-.52832,1.684,1.684,0,0,1-.15536-.75586,1.79638,1.79638,0,0,1,.73989-1.52441,3.41145,3.41145,0,0,1,2.076-.55176,4.30652,4.30652,0,0,1,1.54532.24609,2.43934,2.43934,0,0,1,1.02.70215l-.88263,1.0078a2.24724,2.24724,0,0,0-.704-.50389A2.40916,2.40916,0,0,0,48.899,44.7686q-1.02611,0-1.02585.624a.38424.38424,0,0,0,.20295.37793,2.00861,2.00861,0,0,0,.56078.16211l.97827.15625a4.07989,4.07989,0,0,1,.77532.18554,2.11393,2.11393,0,0,1,.62677.3418,1.57077,1.57077,0,0,1,.42336.52832,1.684,1.684,0,0,1,.15536.75586,1.82374,1.82374,0,0,1-.74572,1.53612A3.4557,3.4557,0,0,1,48.732,50Zm7.51688-.14355a1.84182,1.84182,0,0,1-1.42006-.50293,1.81011,1.81011,0,0,1-.4651-1.28028v-3.0166H52.69313V43.66406h1.08605a.81537.81537,0,0,0,.56078-.15527.77849.77849,0,0,0,.167-.57617V41.48047h1.62252v2.18359h2.39833v1.39258H56.12951v3.4082h2.39833v1.39161Zm3.58487-1.39161H61.7194V42.36816H59.83375v-1.3916h3.65191v7.48828h1.88468v1.39161H59.83375ZM69.58146,50a3.04163,3.04163,0,0,1-2.34979-.86328,3.32947,3.32947,0,0,1-.80009-2.35254,4.16775,4.16775,0,0,1,.2088-1.36133,2.94213,2.94213,0,0,1,.59717-1.03223,2.525,2.525,0,0,1,.94282-.64843,3.33853,3.33853,0,0,1,1.24675-.22168,3.29672,3.29672,0,0,1,1.24091.22168,2.59612,2.59612,0,0,1,.9302.62988,2.8274,2.8274,0,0,1,.59036.99023,3.82372,3.82372,0,0,1,.2088,1.30176v.52832H68.18615v.1084a1.29914,1.29914,0,0,0,.37.96582,1.47781,1.47781,0,0,0,1.08556.36621,1.98382,1.98382,0,0,0,.94282-.21,2.24727,2.24727,0,0,0,.6797-.55859l.95452,1.04394a2.9991,2.9991,0,0,1-1.00207.76856A3.63329,3.63329,0,0,1,69.58146,50Zm-.14273-5.208a1.2053,1.2053,0,0,0-.91276.35449,1.31733,1.31733,0,0,0-.33984.95411v.0957H70.668v-.0957a1.34511,1.34511,0,0,0-.32819-.96A1.179,1.179,0,0,0,69.43873,44.792Zm4.10342,3.67285H74.9627v-3.4082H73.5421V43.66406h3.1858v1.752h.08352a3.49416,3.49416,0,0,1,.233-.65332,2.11981,2.11981,0,0,1,.3816-.56446,1.68088,1.68088,0,0,1,.567-.38965,1.9798,1.9798,0,0,1,.78748-.14453h.58453v1.63282H78.05232a1.23641,1.23641,0,0,0-.9904.38769,1.4118,1.4118,0,0,0-.334.95117v1.8291h2.02835v1.39161H73.5421ZM83.13543,50a4.66331,4.66331,0,0,1-1.78371-.31152,2.52578,2.52578,0,0,1-1.13993-.85157l.99041-.9248a2.56024,2.56024,0,0,0,.84088.61816,2.61172,2.61172,0,0,0,1.104.22266,1.74908,1.74908,0,0,0,.82339-.16211.53139.53139,0,0,0,.2981-.498.37467.37467,0,0,0-.20293-.36621,2.01473,2.01473,0,0,0-.56026-.16211l-.99041-.15625a4.086,4.086,0,0,1-.77582-.18554,2.02686,2.02686,0,0,1-.62044-.3418,1.59662,1.59662,0,0,1-.41752-.52832,1.67738,1.67738,0,0,1-.15536-.75586,1.79636,1.79636,0,0,1,.73989-1.52441,3.41246,3.41246,0,0,1,2.076-.55176,4.30006,4.30006,0,0,1,1.54483.24609,2.43474,2.43474,0,0,1,1.02051.70215l-.88263,1.0078a2.24174,2.24174,0,0,0-.704-.50389,2.40988,2.40988,0,0,0-1.038-.20411q-1.02683,0-1.02633.624a.38346.38346,0,0,0,.20294.37793,2.01463,2.01463,0,0,0,.56026.16211l.97874.15625a4.08607,4.08607,0,0,1,.77583.18554,2.11727,2.11727,0,0,1,.62628.3418,1.57663,1.57663,0,0,1,.42335.52832,1.69053,1.69053,0,0,1,.15536.75586,1.82246,1.82246,0,0,1-.74572,1.53612A3.4531,3.4531,0,0,1,83.13543,50Z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -19,7 +19,7 @@
"target": true, "target": true,
// 📝 readmes // 📝 readmes
"**/**/README.md": true, // "**/**/README.md": true,
// 🗑 // 🗑
"check_size.py": true, "check_size.py": true,

1870
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "rustler-core" name = "rustler-core"
version = "0.3.5" version = "0.4.1"
edition = "2021" edition = "2021"
description = "🐎 » rustler-core market data extractor core functionality" description = "🐎 » rustler-core market data extractor core functionality"
authors = ["Lucas Colombo <lucas@lucode.ar>"] authors = ["Lucas Colombo <lucas@lucode.ar>"]
@ -15,44 +15,44 @@ path = "lib/lib.rs"
# utils # utils
eyre = { version = "0.6.12", default-features = false } eyre = { version = "0.6.12", default-features = false }
dotenvy = "0.15.7" dotenvy = "0.15.7"
chrono = "0.4.38" chrono = "0.4.40"
getset = "0.1.2" getset = "0.1.5"
# async # async
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] }
async-trait = "0.1.80" async-trait = "0.1.88"
# grpc & websocket # grpc & websocket
tokio-tungstenite = { version = "0.22.0" } tokio-tungstenite = { version = "0.26.2" }
tonic = "0.11.0" tonic = "0.13.0"
prost = "0.12.6" # protocol buffers prost = "0.13.5" # protocol buffers
futures = "0.3.30" futures = "0.3.31"
tokio-util = "0.7.11" tokio-util = "0.7.14"
# database # database
sea-orm = { version = "0.12.15", features = [ sea-orm = { version = "1.1.7", features = [
"runtime-tokio-native-tls", "runtime-tokio-native-tls",
"sqlx-sqlite", "sqlx-sqlite",
"macros", "macros",
] } ] }
sea-orm-migration = { version = "0.12.15", features = [ sea-orm-migration = { version = "1.1.7", features = [
"runtime-tokio-native-tls", "runtime-tokio-native-tls",
"sqlx-sqlite", "sqlx-sqlite",
] } ] }
redis = { version = "0.25.4", features = ["tokio-comp"] } redis = { version = "0.29.2", features = ["tokio-comp"] }
# other # other
serde = { version = "1.0.203", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.117" serde_json = "1.0.140"
uuid = { version = "1.8.0", features = ["v4", "fast-rng"] } uuid = { version = "1.16.0", features = ["v4", "fast-rng"] }
lool = { version = "^0.3.2", registry = "lugit", features = [ lool = { version = "^0.9.0", registry = "lugit", features = [
"cli.stylize", "cli.stylize",
"logger", "logger",
"sched.tokio", "sched.tokio",
"sched.rule-recurrence", "sched.rule-recurrence",
"macros", "macros",
] } ] }
async-stream = "0.3.5" async-stream = "0.3.6"
[build-dependencies] [build-dependencies]
tonic-build = "0.11.0" tonic-build = "0.13.0"

View File

@ -20,26 +20,15 @@ Also, this library is built using the `Rust` programming language... so, **_rust
## What this library includes ## What this library includes
This library defines the core functionality for a `rustler`. It includes the following: See [here](./lib/rustlers/README.md).
- A [`Rustler`](./lib/rustlers/rustler.rs) trait that defines the core functionality for a `rustler`.
- A [`RustlersSvc`](./lib/rustlers/svc.rs) which orchestrates the `rustlers` at runtime, scheduling them to scrape stock pricing data between market hours.
More info [here](./lib/rustlers/README.md).
Apart from the above, this library also defines:
- a [database schema](./lib/entities/orm/) for storing market hours, which is used by the `RustlersSvc` to schedule the `rustlers`.
- initial [database migrations](./lib/entities/migration) to create the schema.
- a [grpc server](./lib/grpc/) to interact with the rustlers database.
- <img alt="unimplemented" src="https://raw.githubusercontent.com/lucas-labs/rustler-core/master/.github/img/todo.svg" height="12"> a [websocket gateway server](./lib/socket/) to stream stock pricing data to subscribed clients
> [!NOTE] > [!NOTE]
> >
> This library defines a _rustler_ as a service that scrapes stock pricing data for a > This library defines a _rustler_ as a service that scrapes stock pricing data for a particular
> particular market. > market.
> >
> Although this library contains the core and abstract functionality for the rustlers, it doesn't include any concrete implementation for them. > Although this library contains the core and abstract functionality for the rustlers, it doesn't
> include any concrete implementation for them.
> >
> Actual concrete implementations for each market cannot be published for many reasons. > Actual concrete implementations for each market cannot be published for many reasons.

38
lib/README.md Normal file
View File

@ -0,0 +1,38 @@
<p align="center"><img src="https://raw.githubusercontent.com/lucas-labs/rustler-core/master/.github/img/rustler-core-logo.svg" height="264"></p>
<br>
<br>
<p align="center">
𝐫𝐮𝐬𝐭𝐥𝐞𝐫𝐜𝐨𝐫𝐞 is a library that contains the core functionality for `rustler`, a web scraping service
that scrapes several stock market providers for stock pricing data. It is built using the
<code>Rust</code> programming language.
</p>
<br>
<br>
## Why "rustler"
A `rustler` is a person who steals live**_stock_**. Well, this library is a service that collects
_stock_ market data from the internet. So, it's a "_rustler_" for stock market data.
Also, this library is built using the `Rust` programming language... so, __rust__-ler 😊
## What this library includes
This library defines the core functionality for a `rustler`. It includes the following:
- A [`rustlers::Rustler`] trait that defines the core functionality for a `rustler`.
- A [`rustlers::svc::RustlersSvc`] which orchestrates the `rustlers` at runtime, scheduling them to scrape stock pricing data between market hours.
More info [here](rustlers).
Apart from the above, this library also defines:
- a [database schema](entities) for storing market hours, which is used by the `RustlersSvc` to schedule the `rustlers`.
- initial [database migrations](entities/migration) to create the schema.
- a [grpc server](grpc) to interact with the rustlers database.
- a [websocket gateway server](socket) to stream stock pricing data to subscribed clients

View File

@ -14,7 +14,7 @@ fn main() {
fn compile_proto(proto_file: &str) { fn compile_proto(proto_file: &str) {
tonic_build::configure() tonic_build::configure()
.build_server(true) .build_server(true)
.compile(&[proto_file], &["."]) .compile_protos(&[proto_file], &["."])
.unwrap_or_else(|e| panic!("protobuf compile error: {}", e)); .unwrap_or_else(|e| panic!("protobuf compile error: {}", e));
println!("cargo:rerun-if-changed={}", proto_file); println!("cargo:rerun-if-changed={}", proto_file);

View File

@ -50,10 +50,10 @@ impl<RM: BusMessage> PublisherTrait<RM> for RedisPublisher<RM> {
async fn publish(&mut self, value: RM) -> Result<()> { async fn publish(&mut self, value: RM) -> Result<()> {
let obj_key = key(self.get_prefix(), value.to_bus_key()); let obj_key = key(self.get_prefix(), value.to_bus_key());
// set hash key // set hash key
self.conn.hset_multiple(&obj_key, value.to_bus_val().as_slice()).await?; () = self.conn.hset_multiple(&obj_key, value.to_bus_val().as_slice()).await?;
// publish to the appropriate channel // publish to the appropriate channel
self.conn.publish(&obj_key, value.as_message()).await?; () = self.conn.publish(&obj_key, value.as_message()).await?;
Ok(()) Ok(())
} }
} }

View File

@ -16,6 +16,7 @@ impl MigrationTrait for Migration {
.col(ColumnDef::new(Ticker::Symbol).string().not_null()) .col(ColumnDef::new(Ticker::Symbol).string().not_null())
.col(ColumnDef::new(Ticker::QuoteSymbol).string().null()) .col(ColumnDef::new(Ticker::QuoteSymbol).string().null())
.col(ColumnDef::new(Ticker::MarketId).string().not_null()) .col(ColumnDef::new(Ticker::MarketId).string().not_null())
.col(ColumnDef::new(Ticker::Active).boolean().not_null().default(true))
.foreign_key( .foreign_key(
ForeignKey::create() ForeignKey::create()
.name("fk_ticker_market_id") .name("fk_ticker_market_id")
@ -51,4 +52,6 @@ enum Ticker {
QuoteSymbol, QuoteSymbol,
/// Market ID /// Market ID
MarketId, MarketId,
/// Active status of the ticker. This defines if quotes are generated for this ticker or not.
Active,
} }

View File

@ -35,21 +35,20 @@ pub mod db {
}, },
}; };
const RUSTLER_DATABASE: &str = "RUSTLER_DATABASE"; const DATABASE_URL: &str = "DATABASE_URL";
fn get_default_conn_str() -> String { fn get_default_conn_str() -> String {
let conn_str = s!("sqlite://rustler.db?mode=rwc"); let conn_str = s!("sqlite://rustler.db?mode=rwc");
info!( info!(
"No `{}` env var found, using default: {}", "No `{}` env var found, using default: {}",
RUSTLER_DATABASE.italic(), DATABASE_URL.italic(),
conn_str.green() conn_str.green()
); );
conn_str conn_str
} }
pub async fn get_connection() -> Result<DatabaseConnection> { pub async fn get_connection() -> Result<DatabaseConnection> {
let db_conn_str = let db_conn_str = std::env::var(DATABASE_URL).unwrap_or_else(|_| get_default_conn_str());
std::env::var(RUSTLER_DATABASE).unwrap_or_else(|_| get_default_conn_str());
let mut conn_opts = ConnectOptions::new(db_conn_str.to_owned()); let mut conn_opts = ConnectOptions::new(db_conn_str.to_owned());
conn_opts.sqlx_logging(false); conn_opts.sqlx_logging(false);

View File

@ -11,6 +11,7 @@ pub struct Model {
pub symbol: String, pub symbol: String,
pub quote_symbol: Option<String>, pub quote_symbol: Option<String>,
pub market_id: String, pub market_id: String,
pub active: bool,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@ -24,6 +24,13 @@ impl Service {
Ok(tickers) Ok(tickers)
} }
/// 🐎 » get all active
pub async fn get_all_active(&self) -> Result<Vec<TickerModel>, DbErr> {
let tickers =
Ticker::find().filter(ticker::Column::Active.eq(true)).all(&self.conn).await?;
Ok(tickers)
}
/// 🐎 » retrieves a ticker from the database, given its id /// 🐎 » retrieves a ticker from the database, given its id
pub async fn get(&self, id: String) -> Result<Option<TickerModel>, DbErr> { pub async fn get(&self, id: String) -> Result<Option<TickerModel>, DbErr> {
let ticker = Ticker::find_by_id(id).one(&self.conn).await?; let ticker = Ticker::find_by_id(id).one(&self.conn).await?;

View File

@ -23,6 +23,7 @@ message Ticker {
string symbol = 2; string symbol = 2;
optional string quote_symbol = 3; optional string quote_symbol = 3;
string market_id = 4; string market_id = 4;
bool active = 5;
} }
message Tickers { message Tickers {

View File

@ -22,6 +22,7 @@ impl Ticker {
symbol: self.symbol, symbol: self.symbol,
quote_symbol: self.quote_symbol, quote_symbol: self.quote_symbol,
market_id: self.market_id, market_id: self.market_id,
active: self.active,
} }
} }
@ -32,6 +33,7 @@ impl Ticker {
symbol: model.symbol, symbol: model.symbol,
quote_symbol: model.quote_symbol, quote_symbol: model.quote_symbol,
market_id: model.market_id, market_id: model.market_id,
active: model.active,
} }
} }
} }

View File

@ -1,3 +1,5 @@
#![doc = include_str!("README.md")]
pub mod bus; pub mod bus;
pub mod entities; pub mod entities;
pub mod grpc; pub mod grpc;

View File

@ -1,44 +1,56 @@
<p align="center"><img src="../../.github/img/doc-title-rustler.svg" height="264"></p> <p align="center"><img src="https://raw.githubusercontent.com/lucas-labs/rustler-core/master/.github/img/doc-title-rustler.svg" height="264"></p>
<br> <br>
<br> <br>
## `rustler.rs` ## `rustler.rs`
Contains the `rustler!` macro, which is used to define a `Rustler`. Contains the `rustler!` macro, which is used to define a [`Rustler`].
`Rustler` is a trait that extends the `RustlerAccessor` trait. [`Rustler`] is a trait that extends the [`RustlerAccessor`] trait.
Together, they define the interface and the common functionalities for all Rustlers. Together, they define the interface and the common functionalities for all Rustlers.
### The `RustlerAccessor` trait ### The [`RustlerAccessor`] trait
The `RustlerAccessor` trait defines the interface for accessing the Rustler's data (`getters` and `setters` that all Rustlers must implement). The [`RustlerAccessor`] trait defines the interface for accessing the Rustler's data (`getters` and
`setters` that all Rustlers must implement).
Some of the expected accessors are: Some of the expected accessors are:
- `status` and `set_status` - `status` and `set_status`
- `tickers` and `set_tickers` - `tickers` and `set_tickers`
- `msg_sender` and `set_msg_sender` - `msg_sender` and `set_msg_sender`
### The `Rustler` trait ### The [`Rustler`] trait
The `Rustler` trait extends the `RustlerAccessor` trait and defines the interface for common Rustler's functionalities: The [`Rustler`] trait extends the [`RustlerAccessor`] trait and defines the interface for common
Rustler's functionalities:
- `start` the Rustler, calling abstract `connect` if the Rustler is set to connect on start - [`Rustler::start`] the Rustler, calling abstract [`Rustler::connect`] if the Rustler is set to
connect on start
- status change handling - status change handling
- `add` new tickers to the Rustler (calling `on_add` at the end if tickers were added). Also calls `connect` if the Rustler is set to connect on add and the Rustler is not already connected - [`Rustler::add`] new tickers to the Rustler (calling [`Rustler::on_add`] at the end if tickers
- `delete` tickers from the Rustler (calling `on_delete` at the end if tickers were deleted). Also calls `disconnect` if there are no more tickers in the Rustler. were added). Also calls [`Rustler::connect`] if the Rustler is set to connect on add and the
Rustler is not already connected
- [`Rustler::delete`] tickers from the Rustler (calling [`Rustler::on_delete`] at the end if tickers
were deleted). Also calls [`Rustler::disconnect`] if there are no more tickers in the Rustler.
The `Rustler` trait also defines the following abstract methods that must be implemented by each `Rustler` implementation. The [`Rustler`] trait also defines the following abstract methods that must be implemented by each
[`Rustler`] implementation.
- `connect` method that connects the Rustler to the data source - [`Rustler::connect`] method that connects the Rustler to the data source
- `disconnect` method that disconnects the Rustler from the data source - [`Rustler::disconnect`] method that disconnects the Rustler from the data source
- `on_add` method that is called when new tickers are added to the Rustler. This method is called when new tickers are added to the Rustler and must implement the logic to start tracking and rustling the new tickers. - [`Rustler::on_add`] method that is called when new tickers are added to the Rustler. This method
- `on_delete` method that is called when tickers are deleted from the Rustler. This method is called when tickers are deleted from the Rustler and must implement the logic to stop tracking and rustling the deleted tickers. is called when new tickers are added to the Rustler and must implement the logic to start tracking
and rustling the new tickers.
- `on_delete` method that is called when tickers are deleted from the Rustler. This method is called
when tickers are deleted from the Rustler and must implement the logic to stop tracking and
rustling the deleted tickers.
### The `rustler!` macro ### The `rustler!` macro
The `rustler!` macro is used to define a `Rustler` and to automatically implement the `RustlerAccessor` trait. This adds the necessary fields and accessors to the struct. The `rustler!` macro is used to define a [`Rustler`] and to automatically implement the
[`RustlerAccessor`] trait. This adds the necessary fields and accessors to the struct.
**Example:** **Example:**
@ -48,20 +60,21 @@ rustler! {
} }
``` ```
Now we have a `MyRustler` struct that implements the `RustlerAccessor` Now we have a `MyRustler` struct that implements the [`RustlerAccessor`]
trait and has all the necessary fields and accessors :) trait and has all the necessary fields and accessors :)
## `rustlerjar.rs` ## `rustlerjar.rs`
This files defines the `RustlerJar` struct. This files defines the [`rustlerjar::RustlerJar`] struct.
A `RustlerJar` is a collection of `Rustler`s and their corresponding mappings to markets. Such mapping indicates which Rustler should be used for a given market. A [`rustlerjar::RustlerJar`] is a collection of [`Rustler`]s and their corresponding mappings to markets. Such
mapping indicates which Rustler should be used for a given market.
It provides methods to retrieve Rustlers by `Market`. It provides methods to retrieve Rustlers by [`crate::entities::market`].
### The `rustlerjar!` macro ### The `rustlerjar!` macro
The `rustlerjar!` macro is used to create an instance of a `RustlerJar` on an easy way. The `rustlerjar!` macro is used to create an instance of a [`rustlerjar::RustlerJar`] on an easy way.
**Example:** **Example:**
@ -74,22 +87,23 @@ let rustler_jar = rustlerjar! {
let rustler = rustler_jar.get(&market); let rustler = rustler_jar.get(&market);
``` ```
the `rustlerjar!` expects a mapping of **market names** pointing to **`Rustler` creation functions (constructors)** and will return a `RustlerJar` instance. the `rustlerjar!` expects a mapping of **market names** pointing to **[`Rustler`] creation functions (constructors)** and will return a [`rustlerjar::RustlerJar`] instance.
Note: the `rustlerjar!` macro executes the `create` function for each `Rustler`, so in the example above, we assume that `BinaceRustler::create(url)` returns a function that creates a new instance of `BinanceRustler` and not an instance of `BinanceRustler`. Note: the `rustlerjar!` macro executes the `create` function for each [`Rustler`], so in the example above, we assume that `BinaceRustler::create(url)` returns a function that creates a new instance of `BinanceRustler` and not an instance of `BinanceRustler`.
## `svc.rs` ## `svc.rs`
Contains the `RustlersSvc` struct. Contains the [`svc::RustlersSvc`] struct.
The `RustlersSvc` struct is a service that manages the execution of several `Rustler`s from a `RustlerJar`. The [`svc::RustlersSvc`] struct is a service that manages the execution of several [`Rustler`]s from a `RustlerJar`.
It is responsible for starting and stopping the Rustlers on the right schedule. It is responsible for starting and stopping the Rustlers on the right schedule.
It contains a `MarketService`, which connects to the database and is used to retrieve the markets (including their schedules) and their tickets. Then, for each market, it retrieves the corresponding Rustler from the `RustlerJar`, adds the tickers to the it, and starts it. It contains a `MarketService`, which connects to the database and is used to retrieve the markets (including their schedules) and their tickets. Then, for each market, it retrieves the corresponding Rustler from the `RustlerJar`, adds the tickers to the it, and starts it.
> [!NOTE] > **NOTE**
> >
> <img alt="unimplemented" src="./../../.github/img/todo.svg" height="12"> > <img alt="unimplemented" src="https://raw.githubusercontent.com/lucas-labs/rustler-core/master/.github/img/todo.svg" height="12">
> >
> Although it's not yet implemented, the `RustlersSvc` will also be responsible for adding and deleting tickers and rustlers at runtime. > Although it's not yet implemented, the [`svc::RustlersSvc`] will also be responsible for adding and
> deleting tickers and rustlers at runtime.

View File

@ -1,3 +1,7 @@
//! The rustlers module defines [`Rustler`], a trait that allows rustling data from a source.
#![doc = include_str!("README.md")]
mod rustler; mod rustler;
pub mod rustlerjar; pub mod rustlerjar;

View File

@ -221,39 +221,88 @@ impl Ticker {
pub trait RustlerAccessor { pub trait RustlerAccessor {
// #region fields g&s // #region fields g&s
/// 🐎 » returns the name of the rustler
fn name(&self) -> String; fn name(&self) -> String;
fn static_name() -> String /// 🐎 » returns the [`RustlerStatus`] of the rustler
where
Self: Sized;
fn status(&self) -> &RustlerStatus; fn status(&self) -> &RustlerStatus;
/// 🐎 » sets the [`RustlerStatus`] of the rustler
fn set_status(&mut self, status: RustlerStatus) -> Result<()>; fn set_status(&mut self, status: RustlerStatus) -> Result<()>;
/// 🐎 » returns `true` if the rustler's [`RustlerStatus`] is [RustlerStatus::Connecting]
fn is_connecting(&self) -> bool {
self.status() == &RustlerStatus::Connecting
}
/// 🐎 » returns `true` if the rustler's [`RustlerStatus`] is [RustlerStatus::Connected]
fn is_connected(&self) -> bool {
self.status() == &RustlerStatus::Connected
}
/// 🐎 » returns `true` if the rustler's [`RustlerStatus`] is [RustlerStatus::Disconnecting]
fn is_disconnecting(&self) -> bool {
self.status() == &RustlerStatus::Disconnecting
}
/// 🐎 » returns `true` if the rustler's [`RustlerStatus`] is [RustlerStatus::Disconnected]
fn is_disconnected(&self) -> bool {
self.status() == &RustlerStatus::Disconnected
}
/// 🐎 » returns `true` if the rustler's [`RustlerStatus`] is [RustlerStatus::Connected] or
/// [RustlerStatus::Connecting]
fn is_connected_or_connecting(&self) -> bool {
self.is_connected() || self.is_connecting()
}
/// 🐎 » returns `true` if the rustler's [`RustlerStatus`] is [RustlerStatus::Disconnected] or
/// [RustlerStatus::Disconnecting]
fn is_disconnected_or_disconnecting(&self) -> bool {
self.is_disconnected() || self.is_disconnecting()
}
/// 🐎 » returns the next run time of the rustler
fn next_run(&self) -> &DateTime<Local>; fn next_run(&self) -> &DateTime<Local>;
/// 🐎 » sets the next run time of the rustler
fn set_next_run(&mut self, next_run: DateTime<Local>); fn set_next_run(&mut self, next_run: DateTime<Local>);
/// 🐎 » returns the next stop time of the rustler
fn next_stop(&self) -> &Option<DateTime<Local>>; fn next_stop(&self) -> &Option<DateTime<Local>>;
//// 🐎 » sets the next stop time of the rustler
fn set_next_stop(&mut self, next_stop: Option<DateTime<Local>>); fn set_next_stop(&mut self, next_stop: Option<DateTime<Local>>);
/// 🐎 » returns the last run time of the rustler
fn last_run(&self) -> &Option<DateTime<Local>>; fn last_run(&self) -> &Option<DateTime<Local>>;
/// 🐎 » sets the last run time of the rustler
fn set_last_run(&mut self, last_run: Option<DateTime<Local>>); fn set_last_run(&mut self, last_run: Option<DateTime<Local>>);
/// 🐎 » returns the last stop time of the rustler
fn last_stop(&self) -> &Option<DateTime<Local>>; fn last_stop(&self) -> &Option<DateTime<Local>>;
/// 🐎 » sets the last stop time of the rustler
fn set_last_stop(&mut self, last_stop: Option<DateTime<Local>>); fn set_last_stop(&mut self, last_stop: Option<DateTime<Local>>);
/// 🐎 » returns the last update time of the rustler
fn last_update(&self) -> &Option<DateTime<Local>>; fn last_update(&self) -> &Option<DateTime<Local>>;
/// 🐎 » sets the last update time of the rustler
fn set_last_update(&mut self, last_update: Option<DateTime<Local>>); fn set_last_update(&mut self, last_update: Option<DateTime<Local>>);
/// 🐎 » returns the options of the rustler
fn opts(&self) -> &RustlerOpts; fn opts(&self) -> &RustlerOpts;
/// 🐎 » sets the options (see [`RustlerOpts`]) of the rustler
fn set_opts(&mut self, opts: RustlerOpts); fn set_opts(&mut self, opts: RustlerOpts);
/// 🐎 » returns the [`Ticker`]s of the rustler
fn tickers(&self) -> &HashMap<String, Ticker>; fn tickers(&self) -> &HashMap<String, Ticker>;
/// 🐎 » returns the [`Ticker`]s of the rustler as mutable
fn tickers_mut(&mut self) -> &mut HashMap<String, Ticker>; fn tickers_mut(&mut self) -> &mut HashMap<String, Ticker>;
/// 🐎 » sets the [`Ticker`]s of the rustler
fn set_tickers(&mut self, tickers: HashMap<String, Ticker>); fn set_tickers(&mut self, tickers: HashMap<String, Ticker>);
/// 🐎 » returns the message sender of the rustler
///
/// the message sender is used to send messages back to the rustler service; if the message is
/// a [`RustlerMsg::QuoteMsg`] then the rustler will publish the quote to the bus (redis
/// probably)
fn msg_sender(&self) -> &Option<Sender<RustlerMsg>>; fn msg_sender(&self) -> &Option<Sender<RustlerMsg>>;
/// 🐎 » returns the message sender of the rustler as mutable
fn msg_sender_mut(&mut self) -> &mut Option<Sender<RustlerMsg>>; fn msg_sender_mut(&mut self) -> &mut Option<Sender<RustlerMsg>>;
/// 🐎 » sets the message sender of the rustler
fn set_msg_sender(&mut self, sender: Option<Sender<RustlerMsg>>); fn set_msg_sender(&mut self, sender: Option<Sender<RustlerMsg>>);
// #endregion // #endregion
} }
@ -262,19 +311,53 @@ pub trait RustlerAccessor {
pub trait Rustler: RustlerAccessor + Send + Sync { pub trait Rustler: RustlerAccessor + Send + Sync {
// #region Unimplemented trait functions // #region Unimplemented trait functions
/// 🐎 » fn called after tickers are added to the rustler /// 🐎 » fn called after tickers are added to the rustler
///
/// After calling this function the rustler should start broadcasting quotes for the added
/// tickers.
async fn on_add(&mut self, tickers: &[Ticker]) -> Result<()>; async fn on_add(&mut self, tickers: &[Ticker]) -> Result<()>;
/// 🐎 » fn called after tickers are deleted from the rustler /// 🐎 » fn called after tickers are deleted from the rustler
///
/// After calling this function the tickers should be removed from the rustler and it should
/// stop broadcasting quotes for the deleted tickers.
async fn on_delete(&mut self, tickers: &[Ticker]) -> Result<()>; async fn on_delete(&mut self, tickers: &[Ticker]) -> Result<()>;
/// 🐎 » connects the rustler to the data source /// 🐎 » connects the rustler to the data source
///
/// The implementation should take care of setting up any resources, open connections, etc.
/// after calling this function the rustler should be in a connected state, and the `status`
/// should be `RustlerStatus::Connected`.
///
/// Being in a connected state not necessarily means that the rustler has started rustling, it
/// just means that it is connected to the data source and ready to start rustling. Although
/// the implementation can start rustling after connecting if needed. In most cases, the
/// rustler should only start rustling after `on_add` is called and the rustler has something to
/// rustle.
async fn connect(&mut self) -> Result<()>; async fn connect(&mut self) -> Result<()>;
/// 🐎 » disconnects the rustler from the data source /// 🐎 » disconnects the rustler from the data source
///
/// The implementation should take care of cleaning up any resources, close
/// connections, etc.
///
/// After calling this function the rustler should be in a
/// disconnected state, and the `status` should be `RustlerStatus::Disconnected`.
///
/// Being in a disconnected state means that the rustler is not connected to the data source and
/// is not rustling or broadcasting any quotes.
///
/// After calling this function it is assumed that the rustler:
/// - is not rustling
/// - is not connected to the data source
/// - has freed up any resources and is ready to be dropped if necessary
/// - can connect to the data source again if needed, by calling the `connect` function
///
/// This function will be called atomatically when the rustler does not have any tickers
/// anymore (after calling `on_delete` and the tickers map is empty)
async fn disconnect(&mut self) -> Result<()>; async fn disconnect(&mut self) -> Result<()>;
// #endregion // #endregion
/// 🐎 » starts the rustler /// 🐎 » starts the rustler
async fn start(&mut self) -> Result<()> { async fn start(&mut self) -> Result<()> {
let opts = self.opts(); let opts = self.opts();
if opts.connect_on_start { if opts.connect_on_start && !self.is_connected_or_connecting() {
self.connect().await?; self.connect().await?;
} }
Ok(()) Ok(())
@ -285,31 +368,22 @@ pub trait Rustler: RustlerAccessor + Send + Sync {
/// should be called after the status of the rustler changes /// should be called after the status of the rustler changes
fn handle_status_change(&mut self) -> Result<()> { fn handle_status_change(&mut self) -> Result<()> {
match self.status() { match self.status() {
RustlerStatus::Disconnected => { RustlerStatus::Disconnected => self.set_last_stop(Some(Local::now())),
self.set_last_stop(Some(Local::now())); RustlerStatus::Connected => self.set_last_run(Some(Local::now())),
// if let Some(callbacks) = self.callbacks() {
// if let Some(on_disconnected) = callbacks.on_disconnected {
// on_disconnected()?;
// }
// }
}
RustlerStatus::Connected => {
self.set_last_run(Some(Local::now()));
// if let Some(callbacks) = self.callbacks() {
// if let Some(on_connected) = callbacks.on_connected {
// on_connected()?;
// }
// }
}
_ => {} _ => {}
}; };
Ok(()) Ok(())
} }
/// adds new tickers to the rustler /// 🐎 » adds tickers to the rustler
///
/// Will call the [`Rustler::on_add`] function, so that the implementation can decide what to do after
/// adding the tickers (e.g. sending a message to a websocket to start listening for quotes,
/// send an http request, etc.).
///
/// Depending on the `connect_on_add` option in the rustler's options, the rustler will
/// call [`Rustler::connect`] if it is disconnected before calling [`Rustler::on_add`].
async fn add(&mut self, new_tickers: &Vec<Ticker>) -> Result<()> { async fn add(&mut self, new_tickers: &Vec<Ticker>) -> Result<()> {
let tickers = self.tickers_mut(); let tickers = self.tickers_mut();
let mut added_tickers = vec![]; let mut added_tickers = vec![];
@ -326,7 +400,7 @@ pub trait Rustler: RustlerAccessor + Send + Sync {
if self.opts().connect_on_add { if self.opts().connect_on_add {
// if disconnected, then connect the rustler // if disconnected, then connect the rustler
if self.status() == &RustlerStatus::Disconnected { if !self.is_connected_or_connecting() {
self.connect().await?; self.connect().await?;
} }
} }
@ -338,7 +412,14 @@ pub trait Rustler: RustlerAccessor + Send + Sync {
Ok(()) Ok(())
} }
/// deletes tickers from the rustler /// 🐎 » deletes tickers from the rustler
///
/// Will call the [`Rustler::on_delete`] function, so that the implementation can decide what to
/// do after deleting the tickers (e.g. sending a message to a websocket to stop listening for
/// quotes, etc.).
///
/// If after deleting the tickers the tickers map is empty, the rustler will call
/// [`Rustler::disconnect`] to disconnect the rustler from the data source.
async fn delete(&mut self, new_tickers: &Vec<Ticker>) -> Result<()> { async fn delete(&mut self, new_tickers: &Vec<Ticker>) -> Result<()> {
let tickers = self.tickers_mut(); let tickers = self.tickers_mut();
let mut removed_tickers = vec![]; let mut removed_tickers = vec![];
@ -352,7 +433,7 @@ pub trait Rustler: RustlerAccessor + Send + Sync {
// if after deleting the tickers the tickers map is // if after deleting the tickers the tickers map is
// empty, disconnect the rustler // empty, disconnect the rustler
if tickers.is_empty() { if tickers.is_empty() && !self.is_disconnected_or_disconnecting() {
self.disconnect().await?; self.disconnect().await?;
} }
@ -366,7 +447,7 @@ pub trait Rustler: RustlerAccessor + Send + Sync {
/// macro that expands to the accessor functions for a `Rustler` struct /// macro that expands to the accessor functions for a `Rustler` struct
/// ///
/// for internal use /// __intended for internal use only__
#[macro_export] #[macro_export]
macro_rules! rustler_accessors { macro_rules! rustler_accessors {
( (
@ -375,9 +456,6 @@ macro_rules! rustler_accessors {
fn name(&self) -> String { fn name(&self) -> String {
stringify!($name).to_string() stringify!($name).to_string()
} }
fn static_name() -> String {
stringify!($name).to_string()
}
fn status(&self) -> &$crate::rustlers::RustlerStatus { fn status(&self) -> &$crate::rustlers::RustlerStatus {
&self.status &self.status
} }
@ -399,6 +477,11 @@ macro_rules! rustler_accessors {
fn next_run(&self) -> &$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local> { fn next_run(&self) -> &$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local> {
&self.next_run &self.next_run
} }
// TODO: Instead of next_run and next_stop, store the scheduling rules
// we can calculate the next run and next stop times from the rules, and will also be
// useful to decide if we should recover from a disconnection or not (we should only
// recover if the rules say we should be connected at the current time, otherwise we
// should stay disconnected, even if it was an abnormal disconnection)
fn set_next_run( fn set_next_run(
&mut self, &mut self,
next_run: $crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>, next_run: $crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>,

View File

@ -76,18 +76,18 @@ impl RustlerJar {
/// get the Rustler for the given market /// get the Rustler for the given market
pub fn get(&self, market: &market::Model) -> Option<&Arc<Mutex<Box<dyn Rustler>>>> { pub fn get(&self, market: &market::Model) -> Option<&Arc<Mutex<Box<dyn Rustler>>>> {
let key = self.get_key(market); let key = self.get_key(market)?;
self.rustlers.get(key) self.rustlers.get(key)
} }
/// get the mutable Rustler for the given market as a mutable reference /// get the mutable Rustler for the given market as a mutable reference
pub fn get_mut(&mut self, market: &market::Model) -> Option<&mut Arc<Mutex<Box<dyn Rustler>>>> { pub fn get_mut(&mut self, market: &market::Model) -> Option<&mut Arc<Mutex<Box<dyn Rustler>>>> {
let key = self.get_key(market).to_owned(); let key = self.get_key(market)?.to_owned();
self.rustlers.get_mut(&key) self.rustlers.get_mut(&key)
} }
/// get the key from the mappings for the given market /// get the key from the mappings for the given market
fn get_key(&self, market: &market::Model) -> &str { fn get_key(&self, market: &market::Model) -> Option<&String> {
self.mappings.get(&market.short_name).unwrap() self.mappings.get(&market.short_name)
} }
} }

View File

@ -112,6 +112,11 @@ where
let mut publisher = self.publisher.clone(); let mut publisher = self.publisher.clone();
while let Some(msg) = receiver.recv().await { while let Some(msg) = receiver.recv().await {
match msg { match msg {
// TODO: if we want to restart a dead rustler, we should listen for a restart
// signal here and restart the rustler. The restart signal does not exist
// yet, so we will need to implement it and send it from rustlers when
// it makes sense to restart them (when we are sure we are not going to
// keep listening for quotes from the source feed, for example)
RustlerMsg::QuoteMsg(quote) => publisher.publish(quote).await?, RustlerMsg::QuoteMsg(quote) => publisher.publish(quote).await?,
} }
} }
@ -159,10 +164,10 @@ where
rustler.set_msg_sender(Some(sender)) rustler.set_msg_sender(Some(sender))
} }
if let Some((start, stop)) = rules { let start_name = format!("start-rustler-{}", market.short_name);
let start_name = format!("start-rustler-{}", market.short_name); let end_name = format!("end-rustler-{}", market.short_name);
let end_name = format!("end-rustler-{}", market.short_name);
if let Some((start, stop)) = &rules {
// TODO: we will need to store the job handlers in the `RustlersSvc` struct // TODO: we will need to store the job handlers in the `RustlersSvc` struct
// so that we can stop them when we need to restart the rustlers // so that we can stop them when we need to restart the rustlers
@ -194,17 +199,16 @@ where
market.short_name, market.short_name,
end_job.get_next_run() end_job.get_next_run()
); );
if should_be_running_now(start, stop) {
info!("Starting '{start_name}' right away");
Self::start_rustler_for(rustler.clone(), tickers).await;
}
Ok(())
} else { } else {
warn!("No schedule rules found for market '{}'", market.short_name); info!("No schedule rules found for market '{}'", market.short_name);
Ok(())
} }
if should_be_running_now(rules) {
info!("Starting '{start_name}' right away");
Self::start_rustler_for(rustler.clone(), tickers).await;
}
Ok(())
} else { } else {
warn!("No rustler found for market '{}'", market.short_name); warn!("No rustler found for market '{}'", market.short_name);
Ok(()) Ok(())
@ -314,20 +318,25 @@ enum Op {
} }
/// checks if the rustler should be running now /// checks if the rustler should be running now
fn should_be_running_now(start: SchedulingRule, stop: SchedulingRule) -> bool { fn should_be_running_now(rules: Option<(SchedulingRule, SchedulingRule)>) -> bool {
let now = chrono::Local::now(); if let Some((start, stop)) = rules {
let now = chrono::Local::now();
let start_date = start.next_from(now); let start_date = start.next_from(now);
let stop_date = stop.next_from(now); let stop_date = stop.next_from(now);
// if start date is Some in the past and stop_date is None, we should be running // if start date is Some in the past and stop_date is None, we should be running
if start_date.is_some() && stop_date.is_none() { if start_date.is_some() && stop_date.is_none() {
return true; return true;
} }
match (start_date, stop_date) { match (start_date, stop_date) {
(Some(start), Some(stop)) => stop < start && now < stop, (Some(start), Some(stop)) => stop < start && now < stop,
_ => true, _ => true,
}
} else {
// if there are no rules, it means the rustler should be running all the time
true
} }
} }

View File

@ -1,7 +1,7 @@
allow-branch = ["master"] allow-branch = ["master"]
sign-commit = true sign-commit = true
sign-tag = true sign-tag = true
registry = "lugit-sa" registry = "lugit"
pre-release-commit-message = "release: 🔖 v{{version}}" pre-release-commit-message = "release: 🔖 v{{version}}"
tag-message = "release: 🔖 v{{version}}" tag-message = "release: 🔖 v{{version}}"
tag-prefix = "" tag-prefix = ""