perf(sync): Preload and cache exchange rate data when syncing. #2404
Reference in New Issue
Block a user
Delete Branch "preload-exchange-rate"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
When we are performing transaction syncs, we are looking up for the exchange rate of the transaction if the currency of the transaction is different from the account currency. This look up is performed on every transactions that is being synced, which results in a
N+1query.This change loads and caches the exchange rates when performing syncs.
I agree with your diagnosis here and like the overall direction you're taking this with the
exchange_ratesmethod in thesync_cache.That said, I don't think
cached_exchange_tobelongs in theMoneyclass. That class should not be aware of caching, and if we need these bulk loaded rates, I'd rather just encapsulate the "find rate from cache" logic directly in oursync_cache.rbclass.Also, I'm thinking we should remove
QueryAssertionsas this adds complexity and makes our tests brittle by asserting things that aren't directly relevant to the subject under test. Rails already ships with a helper for this, but I still think we ought to be very sparse with our usage of it and let our APM tools notify us of these performance issues.Hi Zach, I've updated the PR to:
cached_exchange_totofind_rate_by_cachefind_rate_by_cachelogic over tosync_cache.rbPlease do take a look and give it another review!
@@ -16,2 +16,4 @@enddef find_rate_by_cache(amount_money, to_currency, date: Date.current, fallback_rate: 1)raise TypeError unless amount_money.respond_to?(:amount) && amount_money.respond_to?(:currency)I think this method needs another look. A few issues I'm seeing at first glance:
find_or_fetch_rateif we're preloading to a cache already)We probably should not be using this
assert_queries_countat such a broad level. This test is brittle because if we make any change toForwardCalculator(highly possible in the future), the number of queries may change and we'd have to come back and update all these tests.@@ -16,2 +16,4 @@enddef find_rate_by_cache(amount_money, to_currency, date: Date.current, fallback_rate: 1)raise TypeError unless amount_money.respond_to?(:amount) && amount_money.respond_to?(:currency)Hmm, I'm confused on the "inconsistent return values part".
entryis passed as a Money object and it is returned if it doesn't need any currency conversions. Then we are creating a newMoneyobject from the currency conversion. In both cases we are returning a consistent return type ofMoney.I would love to know your opinion on this.
I agree that using
assert_queries_countat such a broad level is quite brittle and it is subjected to changes, but I argue that it would help in putting an emphasis in considering performance when we are making changes toForwardCalculator. Avoiding the need to add unnecessary queries to the database.I'll remove the use of using
assert_queries_countat a broad level for now.@@ -16,2 +16,4 @@enddef find_rate_by_cache(amount_money, to_currency, date: Date.current, fallback_rate: 1)raise TypeError unless amount_money.respond_to?(:amount) && amount_money.respond_to?(:currency)Ahh, I see. I was thrown off by the naming of
entry. In the app,Entryis a first class concept, so we should not be naming this paramentryunless it truly represents anEntryobject. I'd probably just call this parammoneyoramount_moneyto be clear.I agree that thinking about performance is important, but there are a lot of reasons why we might need to add another query / remove a query from the calculator, and we don't want to have to constantly update our test assertions every time we make a change.
The test suite should be testing the outcome rather than the implementation. If we were testing a Query Object, I could definitely see the use case for
assert_queries_count. This is just too high of a level to be using it I think.I've just pushed a couple testcases aimed at testing
find_rates_by_cacheand performing more query assertions there instead.@@ -16,2 +16,4 @@enddef find_rate_by_cache(amount_money, to_currency, date: Date.current, fallback_rate: 1)raise TypeError unless amount_money.respond_to?(:amount) && amount_money.respond_to?(:currency)I've updated the param names from:
entry->moneyother_currency->to_currencyHopefully this reduces the confusions around the names and makes the code more readable.
In the event that we don't have the exchange rates in our cache, we want to be able to fetch for that exchange rate and load it for use. Hence,
fetch_rateHi @zachgoll, I've just pushed some code to provide a clearer naming, better error handling, and new test cases for all the changes. Please take a look.
This repo is no longer maintained. Want to submit your PR here instead, @Joelute? 🙏
Pull request closed