-
Notifications
You must be signed in to change notification settings - Fork 23
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
feat: Send push notifications for expired and expiring positions #1297
feat: Send push notifications for expired and expiring positions #1297
Conversation
klochowicz
commented
Sep 14, 2023
- Send notification to the user if their position is about to expire.
- Add unit tests for the logic determining which notification to send.
- periodically load all relevant positions with their fcm_tokens from the DB and
- attempt to deliver notifications
) -> Result<()> { | ||
let users = user::all(conn)?; | ||
|
||
let positions_with_fcm_tokens = positions::Position::get_all_positions_with_expiry_after( |
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 this could be nicely done with an inner join!
At least we should only load users which have an expired position.
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 mentioned that in the comment, but I don't think the optimisation is worthwhile at this stage?
🤔 especially if we're doing it very infrequently? (potentially, if we rework according to your other suggestions, we would only do this during the rollover weekend)
how strongly do you feel about it? I'm happy to rework it if you feel it's worth the time investment to optimise it.
This comment has been minimized.
This comment has been minimized.
the premise is not true. I am delivering one notification of each.
Fair enough I miss read the condition, but wouldn't it still send 2 notifications as you are checking for 1 hour after but run it ever 30 minutes? Anyways it's surely not as critical as sending a notification non-stop.
I hadn't something specific in mind it was just a proposal after looking over your PR. However we should align it with the rest of the code. We don't need another task that regularly checks if a position has expired we already have that. See
I am not blocking that PR as the the amount of push notifications are less than assumed when asking for changes, however I don't see a rush in bringing this out in anyway possible, we should take the time to get this to a point where we are all happy with it. |
if let Some(fcm_token) = maybe_fcm_token { | ||
Some((p, fcm_token)) | ||
} else { | ||
tracing::warn!(?p, "No FCM token for position"); | ||
None | ||
} |
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.
🔧 This can just be a map
(or a find_map
altogether).
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.
the reason I unfolded is that I wanted to print out the warning instead of just ignoring the ones that didn't have a FCM token.
if you have a solution that does it even neater with that requirement, please share, as I always am keen on improving my iterator-fu 🙏
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.
Fair enough. I think you could have done that within the find_map
, but this code has diverged so I can't really give it a go myself anymore!
if position.expiry_timestamp <= now | ||
&& now < position.expiry_timestamp + time::Duration::hours(1) |
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.
❓ Can we use a Range
to define this predicate? It should be more readable if possible.
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.
cool idea! I'll look into it!
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.
there's no ranges support for OffsetDateTime
unfortunately :(
Yes, you're correct - it would do it twice. I wanted to avoid the situation when e.g. people get it 1 min before the expiry, and can't react.
This one only checks within particular time window, and I prefer that for separating concerns and concentrating logic of when to send notifications in one place. An infrequent read from a DB is not that costly, readability and testability is preferred IMHO.
isn't this the default situation with every review we have that doesn't solve a critical bug in production? 🤓 FYI: The only drawback is that #1211, which I'm currently working on, is depending on it. |
21662ee
to
a07531e
Compare
a07531e
to
33a7016
Compare
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.
Looks good.
if position.expiry_timestamp <= now | ||
&& now < position.expiry_timestamp + time::Duration::hours(1) |
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.
Is my understanding correct that the user will get a notification that his position has expired up to 1h after the position has expired?
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.
yes, I tried to limit the amount of notifications to not overwhelm the user (cutoff time arbitrarily chosen, it's now with the constant).
the notification itself does not expire on the phone, unless user willingly dismisses it.
} else if position.expiry_timestamp > now | ||
&& position.expiry_timestamp <= now + time::Duration::hours(1) |
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.
Is my understanding correct that the user receives this notification and has 1h to come online before his position is expires?
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.
yes, that's the limitation currently (perhaps a 1h window of delivery e.g. ~12h before it expires would be better!), ie. at the start at the "rollover window".
772c827
to
a771df2
Compare
@holzeis I've rebased on your recent work, trying to improve the naming somewhat, so I'd like to hear your opinion on this. I think my naming doesn't cut it perhaps either - so I was thinking maybe Also, I've moved the window to [13h,12h] before expiry, and adjusted polling interval to be just under 1h (to minimise the doubling of the notifications). |
a771df2
to
83786a0
Compare
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.
@holzeis I've rebased on your recent work, trying to improve the naming somewhat, so I'd like to hear your opinion on this. I think my naming doesn't cut it perhaps either - so I was thinking maybe
feed
oruser_feed
would be best? these are messages that people get when they're connected, in contrast to the notifications that arrive at any time, even if they've have their position closed. thoughts?Also, I've moved the window to [13h,12h] before expiry, and adjusted polling interval to be just under 1h (to minimise the doubling of the notifications).
I think it already looks good. I am not a the best in naming things either, but I guess we can improve that over time.
What I would have liked to see is a more general purpose notification service, that is managing the user data in a set as we are currently doing it with the authenticated_users
. Upon startup, we do anyways always send the fcm token information, so instead of having a REST API we could send it over with the websocket authentication message, store on signin and keep it in memory with our authenticated users. That way the notification service could as well cache that information (However, I do not think that is a huge bottleneck anyways - It's just a thought)
I've left some additional remarks, but they are not blocking! We can also address them in a follow up PR.
coordinator/src/bin/coordinator.rs
Outdated
@@ -214,7 +219,7 @@ async fn main() -> Result<()> { | |||
notifier.clone(), | |||
network, | |||
); | |||
let _handle = rollover::monitor(pool.clone(), tx_user_feed.clone(), notifier, network); | |||
let _handle = rollover::monitor(pool.clone(), tx_user_feed.clone(), notifier); |
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 guess that commit will not compile without the network passed as argument here.
coordinator/src/message.rs
Outdated
pub enum Notification { | ||
Message { | ||
/// Message sent to users via the websocket. | ||
pub enum CoordinatorMessage { |
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.
🔧 CoordinatorMessage
is not correct here as this also includes messages from the orderbook. How about WebsocketNotification
? Inititially, I though that we could as well include the push notification types into this Notifications as well, and include something like a Type
e.g. (OnlyIfOnline
, Always
, OnlyPush
) and then the notification service could first try on the websocket and if failed leave a push notification.
I feel like the websocket messages and the push notifcations have somewhat of a relationship, as I might want to inform the user about a missed websocket message.
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'll change it to WebsocketMessage
, thank you fro the suggestion.
I'm not sure whether it would make sense to couple them together (in my mind push notifications are completely separate thing, that user can opt-in into; and also needs FCM key to work locally).
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.
edit: there was already WebsocketMessage
somewhere else, so I ended up with OrderbookMessage
(these are after all messages delivered via the orderbook websocket stream).
Push notifications are sent in the following time windows: 1. Expiring positions - [EXPIRING_THRESHOLD, now] 2. Expired positions: - [now, EXPIRED_THRESHOLD] Here both of the thresholds are set to 1h. - Add unit tests for the logic determining which notification to send. - periodically load all relevant positions with their fcm_tokens from the DB and attempt to deliver notifications Notification task runs every 30 mins, so that the user would get 2 of each. This way they'll likely get it at least 30 mins before the expiry.
When the other party is not online, send a notification that a position has expired.
…xpiry Create a window [START_OF_EXPIRING_POSITION, END_OF_EXPIRING_POSITION] to better configure the notification about imminent position expiry. Configure the window to [13h,12h] before expiry.
Improve documentation and adjust how often we run the check to minimise the changes of user receiving 2 notifications (it can still happen, but it is around 3% now (overlapping the time a bit to ensure we don't out miss sending out the notification).
83786a0
to
8f887fc
Compare