Skip to content

Commit 5e1a8e3

Browse files
BE-707 | Skip processing quote requests when system is not healthy (#633)
This PR introduces a middleware for `/router/quote` endpoint that would return early instead of computing a quote when system haven't received data required for computation from the Node resulting in cached empty results.
1 parent ae25c8f commit 5e1a8e3

File tree

4 files changed

+114
-6
lines changed

4 files changed

+114
-6
lines changed

app/sidecar_query_server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ func NewSideCarQueryServer(ctx context.Context, appCodec codec.Codec, config dom
247247
types.NewQueryClient(grpcClient),
248248
config.ChainID,
249249
)
250-
routerHttpDelivery.NewRouterHandler(e, routerUsecase, tokensUseCase, quoteSimulator, logger)
250+
routerHttpDelivery.NewRouterHandler(e, routerUsecase, tokensUseCase, quoteSimulator, chainInfoUseCase, logger)
251251

252252
// Create a Numia HTTP client
253253
passthroughConfig := config.Passthrough
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/osmosis-labs/sqs/domain"
7+
"github.com/osmosis-labs/sqs/domain/mvc"
8+
9+
"github.com/labstack/echo/v4"
10+
)
11+
12+
// QuoteChainStateValidatorMiddleware is a middleware that checks the chain readiness before processing the quote requests.
13+
// It ensures that SQS has data to process for the quote requests and will not cache empty or outdated quote data resulting in incorrect quotes.
14+
// NOTE: This approach is safe as it is not relying on external chain info data but rather is using the data that is already available in the SQS,
15+
// meaning that if Node would go down, the SQS will be able to return already cached quotes - as it works without the middleware.
16+
func QuoteChainStateValidatorMiddleware(chainUsecase mvc.ChainInfoUsecase) func(next echo.HandlerFunc) echo.HandlerFunc {
17+
return func(next echo.HandlerFunc) echo.HandlerFunc {
18+
return func(c echo.Context) error {
19+
_, err := chainUsecase.GetLatestHeight()
20+
if err != nil {
21+
return c.JSON(http.StatusInternalServerError, domain.ResponseError{Message: "no candidate routes found"})
22+
}
23+
err = chainUsecase.ValidateCandidateRouteSearchDataUpdates()
24+
if err != nil {
25+
return c.JSON(http.StatusInternalServerError, domain.ResponseError{Message: "no candidate routes found"})
26+
}
27+
return next(c)
28+
}
29+
}
30+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package middleware
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/labstack/echo/v4"
10+
"github.com/osmosis-labs/sqs/domain/mocks"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestQuoteChainStateValidatorMiddleware(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
getLatestHeightErr error
18+
validateUpdatesErr error
19+
expectedStatusCode int
20+
expectedResponseBody string
21+
}{
22+
{
23+
name: "Success",
24+
expectedStatusCode: http.StatusOK,
25+
},
26+
{
27+
name: "GetLatestHeight Error",
28+
getLatestHeightErr: errors.New("failed to get latest height"),
29+
expectedStatusCode: http.StatusInternalServerError,
30+
expectedResponseBody: `{"message":"no candidate routes found"}`,
31+
},
32+
{
33+
name: "ValidateCandidateRouteSearchDataUpdates Error",
34+
validateUpdatesErr: errors.New("failed to validate updates"),
35+
expectedStatusCode: http.StatusInternalServerError,
36+
expectedResponseBody: `{"message":"no candidate routes found"}`,
37+
},
38+
}
39+
40+
for _, tt := range tests {
41+
t.Run(tt.name, func(t *testing.T) {
42+
// Setup
43+
e := echo.New()
44+
req := httptest.NewRequest(http.MethodGet, "/", nil)
45+
rec := httptest.NewRecorder()
46+
c := e.NewContext(req, rec)
47+
48+
mockChainUsecase := &mocks.ChainInfoUsecaseMock{
49+
GetLatestHeightFunc: func() (uint64, error) {
50+
return 0, tt.getLatestHeightErr
51+
},
52+
ValidateCandidateRouteSearchDataUpdatesFunc: func() error {
53+
return tt.validateUpdatesErr
54+
},
55+
}
56+
57+
middleware := QuoteChainStateValidatorMiddleware(mockChainUsecase)
58+
59+
// Test
60+
handler := middleware(func(c echo.Context) error {
61+
return c.String(http.StatusOK, "OK")
62+
})
63+
64+
err := handler(c)
65+
66+
// Assert
67+
if tt.expectedStatusCode == http.StatusOK {
68+
assert.NoError(t, err)
69+
assert.Equal(t, http.StatusOK, rec.Code)
70+
assert.Equal(t, "OK", rec.Body.String())
71+
} else {
72+
assert.NoError(t, err)
73+
assert.Equal(t, tt.expectedStatusCode, rec.Code)
74+
assert.JSONEq(t, tt.expectedResponseBody, rec.Body.String())
75+
}
76+
})
77+
}
78+
}

router/delivery/http/router_handler.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/osmosis-labs/sqs/domain"
1717
"github.com/osmosis-labs/sqs/domain/mvc"
1818
"github.com/osmosis-labs/sqs/log"
19+
"github.com/osmosis-labs/sqs/router/delivery/http/middleware"
1920
"github.com/osmosis-labs/sqs/router/types"
2021
)
2122

@@ -29,23 +30,22 @@ type RouterHandler struct {
2930

3031
const routerResource = "/router"
3132

32-
var (
33-
oneDec = osmomath.OneDec()
34-
)
33+
var oneDec = osmomath.OneDec()
3534

3635
func formatRouterResource(resource string) string {
3736
return routerResource + resource
3837
}
3938

4039
// NewRouterHandler will initialize the pools/ resources endpoint
41-
func NewRouterHandler(e *echo.Echo, us mvc.RouterUsecase, tu mvc.TokensUsecase, qs domain.QuoteSimulator, logger log.Logger) {
40+
func NewRouterHandler(e *echo.Echo, us mvc.RouterUsecase, tu mvc.TokensUsecase, qs domain.QuoteSimulator, chainUsecase mvc.ChainInfoUsecase, logger log.Logger) {
4241
handler := &RouterHandler{
4342
RUsecase: us,
4443
TUsecase: tu,
4544
QuoteSimulator: qs,
4645
logger: logger,
4746
}
48-
e.GET(formatRouterResource("/quote"), handler.GetOptimalQuote)
47+
48+
e.GET(formatRouterResource("/quote"), handler.GetOptimalQuote, middleware.QuoteChainStateValidatorMiddleware(chainUsecase))
4949
e.GET(formatRouterResource("/routes"), handler.GetCandidateRoutes)
5050
e.GET(formatRouterResource("/cached-routes"), handler.GetCachedCandidateRoutes)
5151
e.GET(formatRouterResource("/spot-price-pool/:id"), handler.GetSpotPriceForPool)

0 commit comments

Comments
 (0)