Description
The GetMarketPrice gRPC call can remain pending indefinitely when the external price-feed request fails.
The issue became visible after adding a client-side gRPC deadline. Without the deadline, the RPC could remain pending instead of returning an error.
CorePriceService.getMarketPrice() currently calls PriceFeedService.requestPriceFeed(), so an API request depends on an external price-provider request over Tor rather than simply returning the already cached market price.
There may also be an issue with overlapping calls because PriceFeedService stores a single priceConsumer and faultHandler, which are replaced by each new requestPriceFeed() call.
Version
Bisq 1.10.8
Steps to reproduce
- Run Bisq daemon with the gRPC API enabled.
- Call
GetMarketPrice periodically, for example every few seconds.
- Make the configured price provider unavailable, or encounter a Tor connection timeout to the provider.
- Observe the daemon logs and the pending gRPC request.
With a client-side deadline configured, the call eventually fails with:
4 DEADLINE_EXCEEDED: Deadline exceeded after 10.000s
Without a client-side deadline, the call can remain pending.
Expected behaviour
GetMarketPrice should either:
- return the most recent valid cached market price immediately, or
- return an appropriate gRPC error such as
UNAVAILABLE if no recent price is available.
Price-feed refresh should run independently in the daemon rather than being triggered by API reads.
Actual behaviour
GetMarketPrice eventually calls:
CorePriceService.getMarketPrice()
-> PriceFeedService.requestPriceFeed()
-> external price provider via Tor
If the external request fails, CorePriceService currently supplies log::warn as the fault handler. The error is logged, but the gRPC StreamObserver is not completed with onError().
This can leave the RPC pending indefinitely.
In addition, PriceFeedService.requestPriceFeed() assigns:
this.priceConsumer = resultHandler;
this.faultHandler = faultHandler;
so overlapping GetMarketPrice calls can replace the callback associated with an earlier request while an HTTP request is still pending.
Screenshots
N/A
Device or machine
Bisq daemon running on Debian.
Additional info
Example daemon log:
WARN PriceFeedService: We received an error at the request from provider ...
WARN CorePriceService: Could not load marketPrices
PriceRequestException: java.io.IOException:
Request via SOCKS proxy ... failed: Connect timed out
PriceFeedService already maintains a cache and has:
public MarketPrice getMarketPrice(String currencyCode)
A possible approach would be to separate price-feed maintenance from API reads:
- start the repeating price-feed refresh automatically at daemon startup
- let
GetMarketPrice read the current cached value
- validate it with
isRecentExternalPriceAvailable()
- throw
NotAvailableException when no recent price exists, which is already mapped to gRPC UNAVAILABLE
Currently initialRequestPriceFeed() calls:
while periodic refresh is enabled by:
So the resulting behavior could be:
daemon -> periodically refresh price cache
GetMarketPrice -> read cache -> return immediately
instead of making each API read depend on an external Tor request.
Description
The
GetMarketPricegRPC call can remain pending indefinitely when the external price-feed request fails.The issue became visible after adding a client-side gRPC deadline. Without the deadline, the RPC could remain pending instead of returning an error.
CorePriceService.getMarketPrice()currently callsPriceFeedService.requestPriceFeed(), so an API request depends on an external price-provider request over Tor rather than simply returning the already cached market price.There may also be an issue with overlapping calls because
PriceFeedServicestores a singlepriceConsumerandfaultHandler, which are replaced by each newrequestPriceFeed()call.Version
Bisq 1.10.8
Steps to reproduce
GetMarketPriceperiodically, for example every few seconds.With a client-side deadline configured, the call eventually fails with:
Without a client-side deadline, the call can remain pending.
Expected behaviour
GetMarketPriceshould either:UNAVAILABLEif no recent price is available.Price-feed refresh should run independently in the daemon rather than being triggered by API reads.
Actual behaviour
GetMarketPriceeventually calls:If the external request fails,
CorePriceServicecurrently supplieslog::warnas the fault handler. The error is logged, but the gRPCStreamObserveris not completed withonError().This can leave the RPC pending indefinitely.
In addition,
PriceFeedService.requestPriceFeed()assigns:so overlapping
GetMarketPricecalls can replace the callback associated with an earlier request while an HTTP request is still pending.Screenshots
N/A
Device or machine
Bisq daemon running on Debian.
Additional info
Example daemon log:
PriceFeedServicealready maintains a cache and has:A possible approach would be to separate price-feed maintenance from API reads:
GetMarketPriceread the current cached valueisRecentExternalPriceAvailable()NotAvailableExceptionwhen no recent price exists, which is already mapped to gRPCUNAVAILABLECurrently
initialRequestPriceFeed()calls:while periodic refresh is enabled by:
So the resulting behavior could be:
instead of making each API read depend on an external Tor request.