Skip to content

GetMarketPrice RPC can hang indefinitely on price feed failure #8073

Description

@a86zk

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

  1. Run Bisq daemon with the gRPC API enabled.
  2. Call GetMarketPrice periodically, for example every few seconds.
  3. Make the configured price provider unavailable, or encounter a Tor connection timeout to the provider.
  4. 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:

request(false);

while periodic refresh is enabled by:

request(true);

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions