Skip to content

Rewrite branding manager#1463

Merged
jb3 merged 53 commits into
python-discord:mainfrom
kwzrd:kwzrd/branding
Mar 31, 2021
Merged

Rewrite branding manager#1463
jb3 merged 53 commits into
python-discord:mainfrom
kwzrd:kwzrd/branding

Conversation

@kwzrd
Copy link
Copy Markdown
Contributor

@kwzrd kwzrd commented Mar 13, 2021

Resolves #1431
Resolves #1377


This PR implements the changes proposed in #1431. The branding management extension is rewritten to comply with the new event structures in the branding repository.

What has changed?

Everything. I've completely rewritten the extension for two reasons:

  1. I was unhappy with the previous (my own) implementation, as it suffered from being unable to persist state across restarts, which resulted in redundant re-uploads of all assets and icon rotation resets.
  2. The requirements have changed ~ events are now defined directly in the branding repository, and we have to discover them, instead of having hard-coded paths, durations and descriptions in this repository.

So while the extension still aims to solve the same problem, it now works in a completely different way.

Implementation

I've decided to split the implementation into two classes.

1. BrandingRepository

Forms an abstraction of the remote repository that we pull events from. It allows the user to get all events represented as Python objects, validated to ensure that they are correctly defined. It also abstracts away the year-agnostic dates, and is able to select the currently active event without having to worry about dates at all.

2. Branding

This is a typical cog containing the logic needed to keep the Discord guild synchronised. It is mostly designed to be autonomous: an internal daemon will pull events every UTC midnight, transition between events when appropriate, rotate icons at the configured frequency, and even react to asset changes in on-going events. If the banner changes for an already active event, the cog will detect this by comparing its hash against the cache, and re-apply the new version. The same applies for server icons.

In order to achieve this, the cog uses 3 Redis caches. In fact, all state is stored in Redis, so there is no "warm-up" cost on restarts ~ everything should be completely smooth.

Some of the internal state is exposed via user commands: you can get information about the current event, and also see a schedule of all events.

image

None of this requires GitHub requests! We store everything in Redis & 1 automatic refresh a day keeps a stale cache at bay, or something.

Moderators+ can then enable & disable the daemon and force resynchronisation if necessary.

image

image

image

However, a manual sync is seen as more of a recovery scenario. Unless something catastrophic happens, the daemon should simply keep everything updated in the background, and require no user supervision.

A few thoughts on the testing suite

It ain't there. I've committed to reaching 100% coverage on a previous contribution, and although it allowed me to learn a lot about writing tests, in retrospect it ended up being a source of frustration, as I had to completely rewrite or remove portions of the testing suite when reacting to reviewer feedback, massively ramping up the required effort to move forward. The parts of the code that would be easy to test don't really need extensive testing, and the other parts are really difficult to test thoroughly, considering that the solution depends on two external APIs and is asynchronous. For these reasons, I decided to omit the tests this time.

How to review

When reading the code, I'd suggest starting with the repository abstraction, reading it top to bottom, and then the same for the cog. I would also recommend that reviewers read the branding-side PR! It may be good to get that fully approved before we start thoroughly reviewing this contribution, as changes branding-side may need to be reflected here.

The repository abstraction is currently targeting the source branch of the branding PR! We will update that here once the branding PR is merged.

Testing

Testing may be a little intricate due to the reliance on another repository, and also due to the fact that the cog is mostly controlled by the daemon, rather than commands. When testing the daemon, it's possible to manually change the calculated sleep seconds in the before hook. Changing the loop frequency to e.g. 30 seconds will let you observe the nightly wake-ups. Similarly, you can overwrite the calculated time difference in maybe_rotate_icons or change the configured frequency to 0 in order to observe rotations.

Since testing the detection of asset changes is a little tricky, I recorded my approach. Maybe someone will find it useful. Amongst other things, it will show you how you can easily manipulate BrandingRepository to give you an event for a specific date. If you'd like to make a new branch to target in my branding fork, feel free to reach out, I will add you as a contrib.

To test certain scenarios, it may also be useful to manipulate the caches using internal eval.

If you find the debug and trace logging from BrandingRepository noisy, try bumping the local logger's level to INFO.

I strongly recommend setting a GitHub API key when testing, as you will otherwise run into rate limits quickly!

The GitHub key can be generated in your account settings. It requires no extra permissions, as the target repo is public. It merely functions to authorize your requests and bump rate limits.

Set it in the .env file as such:

image

Then, the cog will automatically use it.

Otherwise, if you're unsure on how to test something, or how something works, feel free to ask me!

Pre-merge checklist

  • Merge branding-side changes
  • Target upstream/main when making GitHub requests (currently aimed at my fork)

In terms of merge strategy I propose the following:

  • 1. Disable current branding manager daemon
  • 2. Merge branding PR
  • 3. Merge this PR
  • 4. Enable new daemon

kwzrd added 18 commits March 7, 2021 13:07
Since we're planning substantial changes, it will be easier to build
from scratch.
Constants will only be used in one place and there's not enough of them
to warrant a separate module.
These methods form the API to the repository abstraction.
This allows us to add a neat string representation.
This adds the core logic of branding management. In comparison with the
previous version, we now maintain all state in Redis, which allows the
bot to seamlessly restart without losing any information.

The 'send_info_embed' function is intentionally implemented with the
consideration of allowing users to invoke it on-demand. It always
reads information from the cache, even if the caller could pass
a 'MetaFile' instance. So while this may look needlessly indirect
right now, it should begin to make sense once the command API
is implemented.
Sync make also be invoked with a command; avoid logic duplication.
kwzrd added 3 commits March 13, 2021 18:38
This is a prequel to adding a calendar command. To avoid re-querying
the branding repo on command invocation, event information will be
cached whenever we make requests. The command can then simply get an
up-to-date event schedule from the cache, with the option of forcing
an update via the 'populate_cache_events' function.

Since we cannot easily serialize entire 'Event' instances, we simply
store what's needed - the event name, and its duration.

The author has verified that the cache maintains order; in this case
chronological order based on event start date.
Base automatically changed from master to main March 13, 2021 19:40
kwzrd added 7 commits March 13, 2021 21:57
It makes more sense for the init and the rotation to be separate
operations.

In a subsequent commit, the separation of responsibility will allow
the `rotate_icons` function to have a meaningful return value.
The sync command will now be able to use present this information
to the invoking user.

This commit also prevents the cached banner & icon hash from being
overwritten in the case of asset upload failure. As a result, the
daemon will attempt to re-apply the assets the following day.
Now that the boolean flags are propagating from 'apply_asset', we can
present them to the user.
Discord.py doesn't await the return value.
The notification is now sent conditionally depending on whether we're
entering a new event. This prevents sending a repeating notification
in the case of a manual resynchronisation.

A practical example of when this may trigger is when a staff member
temporarily applies custom assets & then uses the sync command to
reapply the current event.
Knowing which event failed would probably be quite useful.
kwzrd added 8 commits March 25, 2021 11:25
Previously, the event description & duration strings were only stored
on event entry. In the case that the description or duration change
for an on-going event, the cached values wouldn't be updated.

After this commit, the cache is refreshed daily by the daemon.
The fetch helpers will now raise when the request fails rather than
logging a warning and returning a fallback value.

This allows better error logging as the caller is able to log the
propagated exception while adding its own context.

Additionally, the caller in some cases no longer needs to check for
the None return and raise its own exception.
The default KeyError message from dict lookup is just the missing key.
In order to give more context in the log message, we raise our own.
No reason for this to be async.
Logs are now proper sentences ended with full stops. Exceptions are
logged with full tracebacks, and log level are revised to be more
sensible and consistent across the extension.
kwzrd added 2 commits March 27, 2021 19:40
The fallback event should not produce a notification.
It would be strange to just send the embed with no explanation of what
it means.
@kwzrd kwzrd marked this pull request as ready for review March 28, 2021 11:16
@kwzrd kwzrd requested a review from Akarys42 as a code owner March 28, 2021 11:16
@kwzrd
Copy link
Copy Markdown
Contributor Author

kwzrd commented Mar 28, 2021

Ready for review! But please point your attention here first: python-discord/branding#129.

Conflict in the lockfile resolved by re-locking the merged Pipfile.

Conflict in Branding constants resolved by keeping my local version.
Change in the cog's target branch to 'main' from 'master' is currently
irrelevant as we targets a development branch anyway.
@Xithrius Xithrius removed the s: WIP Work In Progress label Mar 28, 2021
Copy link
Copy Markdown
Contributor

@lemonsaurus lemonsaurus left a comment

Choose a reason for hiding this comment

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

This is an unusually large PR, and it probably has to be, so I'm not going to go through every single line of code here, but I'll comment on the high level stuff.

First of all, I'd like to say that this is an exceptionally excellent pull request. The code is idiomatic, well documented, and frankly, quite beautiful. It is a real pleasure to read it.

You mention in the PR description that you haven't written any unit tests for this, and I think that's absolutely fine. We probably should have someone do a functional review of this by actually checking the code out and experimenting with it, though.

I think your approach here is extremely well thought-out and resilient. I love the use of caching to minimize the network impact, I really like the objects you've created to keep track of this, I like the use of NamedTuple classes for the metadata, and I think the suite provides all the different bot commands that will be useful to us. I generally have a hard time finding anything to complain about.

I'm approving this, and I just want to thank you for writing it. I think it'll be extremely useful to us going forward, and look forward to having it in production. 👏🏼 🏆

Copy link
Copy Markdown
Contributor

@Shivansh-007 Shivansh-007 left a comment

Choose a reason for hiding this comment

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

I haven’t tested this yet. This is a pure code review. It was a lovely time reviewing this.

Like lemon said, the code is idiomatic, well documented, and frankly, quite beautiful. It is a real pleasure to read it.

<:lemon_hyperpleased:754441879822663811> (Can discord show this? hmm)

Comment thread bot/exts/backend/branding/_cog.py
Comment thread bot/exts/backend/branding/_cog.py Outdated
Comment thread bot/exts/backend/branding/_cog.py
Comment thread bot/exts/backend/branding/_cog.py Outdated
Comment thread bot/exts/backend/branding/_cog.py Outdated
self.available_icons = list(icons_dir.values())
We cache `event` information to ensure that we:
* Remember which event we're currently in across restarts
* Provide an on-demand information embed without re-querying the branding repository
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

an may seem as redundant with the uncountable pronoun information here.

Suggested change
* Provide an on-demand information embed without re-querying the branding repository
* Provide on-demand information embed without re-querying the branding repository

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

How is 'information' a pronoun?

'... to ensure that we provide on-demand information embed' seems worse than the current revision. We're providing the embed, not the information. However, using 'an informational embed' would probably be better.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Right agreed, "an informational embed" sounds better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Amended in 220590c.

Comment thread bot/exts/backend/branding/_repository.py Outdated
Comment thread bot/exts/backend/branding/_repository.py
Comment thread bot/exts/backend/branding/_repository.py
Comment thread bot/exts/backend/branding/_cog.py Outdated
Comment thread bot/exts/backend/branding/_cog.py Outdated
Copy link
Copy Markdown
Contributor

@Shivansh-007 Shivansh-007 left a comment

Choose a reason for hiding this comment

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

Thanks! Looks Good To Me! :shipit:

Copy link
Copy Markdown
Member

@jb3 jb3 left a comment

Choose a reason for hiding this comment

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

This all looks great and works locally! :shipit:

Only minor notes, both are optional:

  • The fetch_file and fetch_directory could have some info level logs when a request is successful.
  • The pending comment about the wording of the "informational embed".

Other than that, there is only a merge conflict to resolve and then we should be good!

Copy link
Copy Markdown
Contributor

@whispersofthedawn whispersofthedawn left a comment

Choose a reason for hiding this comment

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

LGTM! And that's a lot of code. Wonderful job kwzrd!

kwzrd and others added 4 commits March 31, 2021 19:56
Lockfile conflict resolved by re-locking on the merged Pipfile.
With the branding-side PR merged, we can now target the production
branch.
No code changes in this commit.

Co-authored-by: Shivansh-007 <Shivansh-007@users.noreply.github.com>
Co-authored-by: Joe Banks <joseph@josephbanks.me>
Co-authored-by: Shivansh-007 <Shivansh-007@users.noreply.github.com>
Co-authored-by: Joe Banks <joseph@josephbanks.me>
@kwzrd
Copy link
Copy Markdown
Contributor Author

kwzrd commented Mar 31, 2021

@Shivansh-007 Thanks for the review!

@jb3 Thanks, I improved docs in 220590c and added fetch success logs in b778c25. I've chosen debug level though, hope you agree that works. Since each event requires a fetch, info would be noisy in production.

@dawnofmidnight Thanks. 🤗

Solution now targets branding's production branch and should be good to go.

@jb3 jb3 merged commit c2f6644 into python-discord:main Mar 31, 2021
@Xithrius
Copy link
Copy Markdown
Contributor

🎉

@kwzrd
Copy link
Copy Markdown
Contributor Author

kwzrd commented Mar 31, 2021

Also thank you @lemonsaurus for the review and the kind words! It was fun to work on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: backend Related to internal functionality and utilities (error_handler, logging, security, utils and core) t: enhancement Changes or improvements to existing features t: feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rework branding manager Refresh the branding data before starting the daemon

7 participants