Skip to content
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

Add notification for third party services #7

Merged
merged 8 commits into from
Apr 18, 2024

Conversation

joshpme
Copy link
Contributor

@joshpme joshpme commented Dec 8, 2023

This will allow downstream systems to be notified when revisions are created in indico specifically for JACoW

It is fire and forget to prevent any outage affecting indico uptime.

The hope is that will enable a downstream system "e.g." catscan to perform a scan on a change, rather than polling.

Also first python PR ever, so I welcome any feedback.

If you are happy to merge and deploy the change, we can discuss the URL / Token on slack

openreferee_server/notify.py Outdated Show resolved Hide resolved
@@ -26,6 +26,7 @@ def create_app():
register_error_handlers(app)
db.init_app(app)
register_db_cli(app)
NotifyExtension(app, os.environ.get('NOTIFY_URL'), os.environ.get('NOTIFY_TOKEN'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use os.environ[...] if you expect this to always be present

Copy link
Contributor Author

@joshpme joshpme Dec 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will throw an exception if the keys are not present, where as get will soft return None

I explicitly handle none, plus as these variables can be configured with config instead of environmental variables if that is needed later.

Is there a reason why you prefer to use a dict lookup over get?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally prefer to fail early: If required config keys are missing, I want it to fail at startup time. That's something I see immediately because the app is clearly broken.

If the missing value leaks into the config and it only fails later when trying to send a request, there's a good chance I won't notice it until a real request from a user's action arrives.

That said, I think it makes sense to simply disable notifications if these settings are missing. I'd do that by not initializing the extension and silently doing nothing later when the extension is not on the app context (based on your current architecture). Without the extension structure I'd simply return early in the function to send a notification if the config values are None,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, fair. I'll refactor this now then.

openreferee_server/notify.py Outdated Show resolved Hide resolved
openreferee_server/server.py Outdated Show resolved Hide resolved
openreferee_server/notify.py Outdated Show resolved Hide resolved
if self.session is None:
self.session = setup_requests_session(self.token)

threading.Thread(target=self.send, args=(payload,)).start()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder... is all this extra complexity really needed? To me it sounds like the whole code in here could be done with a simple

def notify(payload):
    sess = ...
    try:
        sess.post(..., timeout=5)
    except ...:
        pass  # log it

no threads, no flask extension, no context etc needed ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I thought I might be over-complicating this, but I thought I'd be adding the notify to each of the endpoints, or maybe extending the logic out if required.

The reason why I use an extension, I needed to load in the environmental config for the notify URL. So I figured I'd need an extension to load this in? Should I just load from process.env from anywhere?

The threading is there as I wanted this to me fire and forget so there would be no slow down to this service if our downstream system was offline or slow to response.

The two reasons I'm using a session is that I am able to config the authentication at a parent level using the method you already have, and depending on the traffic in this service, having a common session between requests reduces http overhead. I'm not sure how it cleans itself up though, so I'm happy to simply this if you do not believe there is much traffic?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason why I use an extension, I needed to load in the environmental config for the notify URL. So I figured I'd need an extension to load this in? Should I just load from process.env from anywhere?

Yes, you can read env vars anywhere. Or even put it in the app.config dict and then access it from there later.

The threading is there as I wanted this to me fire and forget so there would be no slow down to this service if our downstream system was offline or slow to response.

Fair enough, I don't know how reliable your downstream is. Personally I'd probably just use a low-enough timeout and then only log request-related errors so they don't break the flow.

The two reasons I'm using a session is that I am able to config the authentication at a parent level using the method you already have, and depending on the traffic in this service, having a common session between requests reduces http overhead. I'm not sure how it cleans itself up though, so I'm happy to simply this if you do not believe there is much traffic?

Actually, I'm not sure if the session actually keeps connections alive etc. or not. Using it for the token is a good idea though!

Copy link
Contributor Author

@joshpme joshpme Dec 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this call to the microservice blocking, as in, will buttons take longer to load if we put in a downstream call?

I expect that the (yet-to-be-built) downstream service may be hosted on the other side of the planet, also I really didn't want to be responsible for an extra 2-5 second load times on some buttons, or prematurely timeout due to distance.

That being said, I'm also no python guru, so maybe threads are not the best tool for the job here (due to leaky threads or contention issues)? So if you feel strongly about this I will just change it.

Also regarding the use of an extension, I was just following a pattern I'm familiar with from other (non-python) frameworks, so if you want me to simplify it, no problem, I'm just not sure if you comment was more of an observation or a suggestion for change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the buttons won't take longer to load since you're not doing anything to the get_custom_revision_actions endpoint, but still it's probably better to do it this way if you're unsure of the reliability of the service

Log error on exception
Check logger extension is present before attempting to log
…esence of NOTIFY_URL, route the flask debugger into the service to allow logging to actually work
… class remove the extension class which really isn't used beyond initialisation notify_init is now the path for initialising the service
@joshpme joshpme requested a review from ThiefMaster December 12, 2023 04:55
@joshpme
Copy link
Contributor Author

joshpme commented Feb 27, 2024

@ThiefMaster is there anything you still need me to do on this PR before it can be merged?

openreferee_server/app.py Outdated Show resolved Hide resolved
openreferee_server/notify.py Outdated Show resolved Hide resolved
openreferee_server/notify.py Outdated Show resolved Hide resolved
openreferee_server/notify.py Outdated Show resolved Hide resolved
openreferee_server/notify.py Outdated Show resolved Hide resolved
Copy link
Member

@duartegalvao duartegalvao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yo, hows it going? overall the PR lgtm, and from the few tests I did it didn't seem to break anything. I just have a couple of code style suggestions but thats it

cheers

openreferee_server/notify.py Outdated Show resolved Hide resolved
@@ -285,6 +291,17 @@ def review_editable(
"A new revision %r was submitted for contribution %r", revision_id, contrib_id
)
resp = {}

notify(current_app, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might want to consider calling this in some of the other methods, such as create_editable, otherwise you won't get notified of the first submission... even if you don't need it right now it might be useful in the future

isort ran
remove non-pythonic empty check
remove incorrect return type hinting
reduce indents by returning early on notify_init
Copy link
Member

@duartegalvao duartegalvao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm! might wanna get rid of that merge commit tho, we usually prefer rebasing
(not sure if this matters for this repo, I'll let @ThiefMaster decide)

@ThiefMaster
Copy link
Member

I'll squash+merge anyway, so doesn't really matter.

@ThiefMaster ThiefMaster merged commit 930364e into indico:master Apr 18, 2024
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants