diff --git a/db/blip_connected_client.go b/db/blip_connected_client.go index 960add23ee..f0e7ae8872 100644 --- a/db/blip_connected_client.go +++ b/db/blip_connected_client.go @@ -98,7 +98,7 @@ func (bh *blipHandler) handleFunction(rq *blip.Message) error { } bh.logEndpointEntry(rq.Profile(), fmt.Sprintf("name: %s", name)) - return WithTimeout(bh.loggingCtx, UserFunctionTimeout, func(ctx context.Context) error { + return WithTimeout(bh.loggingCtx, bh.db.UserFunctionTimeout, func(ctx context.Context) error { // Call the function: fn, err := bh.db.GetUserFunction(name, requestParams, true, ctx) if err != nil { @@ -169,7 +169,7 @@ func (bh *blipHandler) handleGraphQL(rq *blip.Message) error { } bh.logEndpointEntry(rq.Profile(), fmt.Sprintf("query: %s", query)) - return WithTimeout(bh.loggingCtx, UserFunctionTimeout, func(ctx context.Context) error { + return WithTimeout(bh.loggingCtx, bh.db.UserFunctionTimeout, func(ctx context.Context) error { result, err := bh.db.UserGraphQLQuery(query, operationName, variables, true, ctx) if err != nil { return err diff --git a/db/database.go b/db/database.go index c7300911a8..e1026af8e2 100644 --- a/db/database.go +++ b/db/database.go @@ -124,6 +124,7 @@ type DatabaseContext struct { NoX509HTTPClient *http.Client // A HTTP Client from gocb to use the management endpoints ServerContextHasStarted chan struct{} // Closed via PostStartup once the server has fully started userFunctions *UserFunctions // client-callable JavaScript functions + UserFunctionTimeout time.Duration // Default timeout for N1QL, JavaScript and GraphQL queries. (Applies to REST and BLIP requests.) graphQL *GraphQL // GraphQL query evaluator Scopes map[string]Scope // A map keyed by scope name containing a set of scopes/collections. Nil if running with only _default._default CollectionByID map[uint32]*DatabaseCollection // A map keyed by collection ID to Collection @@ -408,16 +409,17 @@ func NewDatabaseContext(ctx context.Context, dbName string, bucket base.Bucket, return nil, err } dbContext := &DatabaseContext{ - Name: dbName, - UUID: cbgt.NewUUID(), - MetadataStore: metadataStore, - Bucket: bucket, - StartTime: time.Now(), - autoImport: autoImport, - Options: options, - DbStats: dbStats, - CollectionByID: make(map[uint32]*DatabaseCollection), - ServerUUID: serverUUID, + Name: dbName, + UUID: cbgt.NewUUID(), + MetadataStore: metadataStore, + Bucket: bucket, + StartTime: time.Now(), + autoImport: autoImport, + Options: options, + DbStats: dbStats, + CollectionByID: make(map[uint32]*DatabaseCollection), + ServerUUID: serverUUID, + UserFunctionTimeout: defaultUserFunctionTimeout, } // Initialize metadata ID and keys diff --git a/db/functions.go b/db/functions.go index d23fcbe3ae..80d3a67e02 100644 --- a/db/functions.go +++ b/db/functions.go @@ -26,7 +26,7 @@ import ( /* This is the interface to the functions and GraphQL APIs implemented in the functions package. */ // Timeout for N1QL, JavaScript and GraphQL queries. (Applies to REST and BLIP requests.) -const UserFunctionTimeout = 60 * time.Second +const defaultUserFunctionTimeout = 60 * time.Second //////// USER FUNCTIONS @@ -144,7 +144,6 @@ func EstimateSizeOfJSON(args any) int { } return size default: - //log.Printf("*** sizeOfArgs doesn't handle %T", arg) return 1 } } diff --git a/rest/functions_api.go b/rest/functions_api.go index 81bc96e317..4a40c007c2 100644 --- a/rest/functions_api.go +++ b/rest/functions_api.go @@ -40,7 +40,7 @@ func (h *handler) handleFunctionCall() error { } canMutate := h.rq.Method != "GET" - return db.WithTimeout(h.ctx(), db.UserFunctionTimeout, func(ctx context.Context) error { + return db.WithTimeout(h.ctx(), h.db.UserFunctionTimeout, func(ctx context.Context) error { fn, err := h.db.GetUserFunction(fnName, fnParams, canMutate, ctx) if err != nil { return err @@ -224,7 +224,7 @@ func (h *handler) handleGraphQL() error { return base.HTTPErrorf(http.StatusBadRequest, "Missing/empty `query` property") } - return db.WithTimeout(h.ctx(), db.UserFunctionTimeout, func(ctx context.Context) error { + return db.WithTimeout(h.ctx(), h.db.UserFunctionTimeout, func(ctx context.Context) error { result, err := h.db.UserGraphQLQuery(queryString, operationName, variables, canMutate, ctx) if err == nil { h.writeJSON(result) diff --git a/rest/functionsapitest/graphql_queries_test.go b/rest/functionsapitest/graphql_queries_test.go index 81fd944503..223cb2fe79 100644 --- a/rest/functionsapitest/graphql_queries_test.go +++ b/rest/functionsapitest/graphql_queries_test.go @@ -13,11 +13,11 @@ import ( "fmt" "strings" "testing" + "time" "github.com/graphql-go/graphql" "github.com/couchbase/sync_gateway/base" - "github.com/couchbase/sync_gateway/db" "github.com/couchbase/sync_gateway/db/functions" "github.com/couchbase/sync_gateway/rest" "github.com/stretchr/testify/assert" @@ -284,18 +284,20 @@ func TestContextDeadline(t *testing.T) { return } defer rt.Close() + timeout := 500 * time.Millisecond + rt.GetDatabase().UserFunctionTimeout = timeout t.Run("AsAdmin - exceedContextDeadline", func(t *testing.T) { - requestQuery := fmt.Sprintf(`{"query": "query{ checkContextDeadline(Timeout:%d) }"}`, db.UserFunctionTimeout.Milliseconds()*2) + requestQuery := fmt.Sprintf(`{"query": "query{ checkContextDeadline(Timeout:%d) }"}`, timeout.Milliseconds()*2) response := rt.SendAdminRequest("POST", "/db/_graphql", requestQuery) assert.Equal(t, 200, response.Result().StatusCode) testErrorMessage(t, response, "context deadline exceeded") }) t.Run("AsAdmin - doNotExceedContextDeadline", func(t *testing.T) { - requestQuery := fmt.Sprintf(`{"query": "query{ checkContextDeadline(Timeout:%d) }"}`, db.UserFunctionTimeout.Milliseconds()/2) + requestQuery := `{"query": "query{ checkContextDeadline(Timeout:1) }"}` response := rt.SendAdminRequest("POST", "/db/_graphql", requestQuery) - assert.Equal(t, 200, response.Result().StatusCode) + assert.Equal(t, `{"data":{"checkContextDeadline":0}}`, string(response.BodyBytes())) }) } diff --git a/rest/functionsapitest/user_functions_queries_test.go b/rest/functionsapitest/user_functions_queries_test.go index c51ed954e9..5b0c618fd4 100644 --- a/rest/functionsapitest/user_functions_queries_test.go +++ b/rest/functionsapitest/user_functions_queries_test.go @@ -14,8 +14,7 @@ import ( "net/http" "strings" "testing" - - "github.com/couchbase/sync_gateway/db" + "time" "github.com/couchbase/sync_gateway/base" "github.com/couchbase/sync_gateway/db/functions" @@ -454,17 +453,18 @@ func TestFunctionTimeout(t *testing.T) { return } defer rt.Close() - + timeout := 500 * time.Millisecond + rt.GetDatabase().UserFunctionTimeout = timeout // positive case: - reqBody := fmt.Sprintf(`{"ms": %d}`, db.UserFunctionTimeout.Milliseconds()/2) t.Run("under time limit", func(t *testing.T) { + reqBody := `{"ms": 1}` response := rt.SendAdminRequest("POST", "/db/_function/sleep", reqBody) assert.Equal(t, 200, response.Result().StatusCode) }) // negative case: - reqBody = fmt.Sprintf(`{"ms": %d}`, 2*db.UserFunctionTimeout.Milliseconds()) t.Run("over time limit", func(t *testing.T) { + reqBody := fmt.Sprintf(`{"ms": %d}`, 2*timeout) response := rt.SendAdminRequest("POST", "/db/_function/sleep", reqBody) assert.Equal(t, 500, response.Result().StatusCode) })