Open warner opened 3 months ago
Ok so vaultManager.js has this function named observeQuoteNotifier
:
observeQuoteNotifier() {
const { state } = this;
const { collateralBrand, collateralUnit, debtBrand, storageNode } =
state;
const ephemera = collateralEphemera(collateralBrand);
const quoteNotifier = E(priceAuthority).makeQuoteNotifier(
collateralUnit,
debtBrand,
);
// @ts-expect-error XXX quotes
ephemera.storedQuotesNotifier = makeStoredNotifier(
// @ts-expect-error XXX quotes
quoteNotifier,
E(storageNode).makeChildNode('quotes'),
marshaller,
);
trace(
'helper.start() awaiting observe storedQuotesNotifier',
collateralBrand,
);
// NB: upon restart, there may not be a price for a while. If manager
// operations are permitted, ones that depend on price information
// will throw. See https://github.com/Agoric/agoric-sdk/issues/4317
const quoteWatcher = harden({
onFulfilled(value) {
ephemera.storedCollateralQuote = value;
},
onRejected() {
// NOTE: drastic action, if the quoteNotifier fails, we don't know
// the value of the asset, nor do we know how long we'll be in
// ignorance. Best choice is to disable actions that require
// prices and restart when we have a new price. If we restart the
// notifier immediately, we'll trigger an infinite loop, so try
// to restart each time we get a request.
ephemera.storedCollateralQuote = null;
},
});
void watchQuoteNotifier(quoteNotifier, quoteWatcher);
},
The goal is to populate ephemera.storedCollateralQuote
. Every time this function is called, it will always send a new makeQuoteNotifier()
to the priceAuthority, and will always build a new makeStoredNotifier()
around it, and will alway uses watchQuoteNotifier()
to glue an ephemeral quoteWatcher
to that, and that watcher is the one place where storedCollateralQuote
can be filled.
This function is called from several places in vaultManager that need a stored quote, with a clause like:
if (!storedCollateralQuote) {
facets.helper.observeQuoteNotifier();
throw Fail`maxDebtFor called before a collateral quote was available for ${collateralBrand}`;
}
and at least one of those places can be triggered by an hourly timer wakeup.
Which means every timer wakeup will provoke this new set of objects and promises, until some QuoteNotifier finally reports either a price or an error. And something deeper in the process is unable to do either without a PushPrice coming in from off-chain, which isn't happening on the testnet.
I think we need that const quoteNotifier = E(priceAuthority).makeQuoteNotifier()
to have a wider scope.
We want observeQuoteNotifier()
to not make a new request if there's already a request pending. And we need it to be reliable in the face of upgrade, and errors.
Maybe something that starts with:
if (!ephemera.quoteNotifier) {
ephemera.quoteNotifier = E(priceAuthority).makeQuoteNotifier();
ephemera.quoteNotifier.catch(_err => ephemera.quoteNotifier = undefined);
}
It should probably defer building the rest of the wrapping objects until we get a quote notifier back. The goal is to not ever build redundant objects, or make redundant requests.
Note that I still don't know exactly which promise is the one being accumulated. I know it's one of the getUpdateSince
that gets sent to aQuoteNotifier
, and I know that we're making new QuoteNotifiers each cycle, but I don't know if the getUpdateSince
s are all being sent to some pre-existing QuoteNotifier, or if each one is going to a new QuoteNotifier.
In https://github.com/Agoric/agoric-private/issues/169 we observed constant growth in the number of unresolved promises, in a testnet that was mainfork-cloned from our mainnet, but which lacked its own oracles, and thus was not supplied with price quotes. My analysis notes from that ticket:
We don't expect to be in this situation on a real network, at least not for very long. Oracles will do their job, prices will flow, auctions will happen. But:
The task is to understand exactly where these promises are coming from, what action whose lack is causing them to be generated, and then fix the code to prevent the long-term growth.