-
Notifications
You must be signed in to change notification settings - Fork 262
feat(argus): internal interfaces and actor model #2659
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
…b/argus/internal-interfaces
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the design. I recommend adding module docs to each of your actors now that you’re establishing the structure and design of the service.
#[allow(dead_code)] | ||
pub struct ChainPriceListenerState { | ||
chain_id: String, | ||
contract: Arc<dyn GetChainPrices + Send + Sync>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we make a type alias for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(and for ChainId also)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've got one actually, let me use that here
|
||
#[derive(Debug)] | ||
pub enum PythPriceListenerMessage { | ||
GetLatestPrice(PriceId, RpcReplyPort<Option<Price>>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would advice against using reply between actors because on the sender side it results in an extra async blocker (other than the main message queue) and it defeats the concurrency benefits of the actors (not getting into livelocks and deadlocks). This is mostly useful if an external observer outside the actors wants to get a reply from the system.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kind of expected to have a good construct for a reply-based mechanism using cast but couldn't find it. probably you need to pass a reference to the requesting actor (and track the request/...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we had a bunch of issues with two-way communication between actors in pyth-agent. I don't know if we had a good solution to that though. maybe you simply use shared memory with a lock for these kinds of read requests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah i think we should either do full actor model or full shared memory model (like hermes/agent/..)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah interesting. Do either of you remember why 2-way communication didn't work so well in pyth-agent?
Re @ali-bahjati about the reply-based mechanisms in ractor using cast:
cast
supports fully async replies -- you can pass an ActorRef in the message, the receiver can use it to send a reply message to the sender at a later time.call
supports awaitable replies -- the receiver gets an RpcReplyPort that it can use to reply while handling the incoming message. the sender can await this reply. the receiver gets messages serially, so it doesn't need to track requests.
I find call
semantics more ergonomic for certain things like fetching the latest price in the Controller, not sure how we would get it to work with cast
, since we'd need the latest price in the middle of the Controller's update loop. Lmk if you guys have ideas here.
So the tradeoff of using call
with GetLatestPrice operations would be that we're slightly compromising the fully async nature of the actors (but prices would still get polled in an unblocked background thread), but we still get the isolation benefits. I'm inclined to stick with call
here for simplicity, but I hear you guys saying this might have been a regret in the past :) Let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did some thinking, and yeah we are indeed in between paradigms here. By introducing synchronous dependencies between actors (using call
,) we trade away the high concurrency of the actor model for simplicity. From what I gather, this became an issue in pyth-agent as the code evolved and added implicit dependencies between actors resulting in dead/livelocks that were hard to diagnose and fix. In Argus, our actor design is a DAG, so it felt okay to introduce these sync dependencies, but ultimately I settled on going the full shared memory model route here: #2682
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems generally reasonable to me. I do think that revisiting the pyth-agent experience with actors is worthwhile, as that system got pretty janky over time. Probably some lessons there about how (not) to use Rust actors.
chain_id: String, | ||
contract: Arc<dyn GetChainPrices + Send + Sync>, | ||
feed_ids: HashSet<PriceId>, | ||
latest_prices: Arc<RwLock<HashMap<PriceId, Price>>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider using DashMap as a thread-safe hashmap (instead of RwLock<HashMap<...>>)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oo very cool, thanks for sharing
backoff_policy: ExponentialBackoff, | ||
} | ||
|
||
pub struct PricePusher; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're going to get in trouble with this Actor for two reasons: (1) the controller is running on a separate frequency, and so its checks for when to push an update are not synchronized with the actions of PricePusher. That is, the PricePusher can be trying to push an update (possibly stuck in backoff) and meanwhile the Controller is repeatedly detecting that the update needs to occur and is sending more PushPriceUpdate messages. (2) this approach serializes the updates for all subscriptions on a chain, so if we can't update subscription 1 for some reason, that blocks subscription 2 as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah good point. We can definitely handle this better in PricePusher. I'm thinking we can spawn
a task to attempt the update with backoff and track the handles. If a newer push for a subscription comes in while the last one is stuck in backoff, we can cancel the outstanding task and spawn a new one. This also means the actor's handle function doesn't block, and we can multiple push attempts for different subscriptions active at a time.
|
||
#[derive(Debug)] | ||
pub enum PythPriceListenerMessage { | ||
GetLatestPrice(PriceId, RpcReplyPort<Option<Price>>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we had a bunch of issues with two-way communication between actors in pyth-agent. I don't know if we had a good solution to that though. maybe you simply use shared memory with a lock for these kinds of read requests?
Summary
Skeleton for Argus. Defines internal interfaces and sets up the actor model using ractor framework.
Note to reviewer: the interesting stuff is in
actors
andadapters
, see thetypes.rs
in each for the big picture.Actors
PythPriceListener
actor is responsible for tracking off-chain prices. It can handle aGetLatestPrice(feed_ids)
message, which it serves from its internal store of prices that are being polled/streamed from Hermes in the background. It can also handle aUpdateFeedIdSet(feed_ids)
message, which can be used to adjust the feeds being tracked.actors/types.rs
for the message definitions, these enums describe the actors' interfaces.keeper.rs
, but this will eventually be done using ractor's Factory concept, which provides durability and lifecycle mgmt for actors.Adapters
adapters/types.rs
. These traits are used by the actors, and are implemented byhermes.rs
,contract.rs
, etc.SubscriptionListener
actor expects an implementor ofReadChainSubscriptions
. This trait specifiesget_active_subscriptions()
andsubscribe_to_subscription_events()
, which are impl'd byPythPulse<M>
incontract.rs
.How has this been tested?
Not tested yet. Next up is writing the Controller's main update loop, wiring it up to mock implementations, and ensuring the actor communication works. Then, we will implement individual actor logic, and eventually wire it up to the blockchain instead of mock adapters.