Skip to content

Commit

Permalink
쿼리 복잡도 분석 (#13)
Browse files Browse the repository at this point in the history
* add complexity analysis

* add comment

* apply review

* nit

* refactor

* nit

* refactor match to cond

* apply review

* nit

* remove

* fix lint

* private

* add test

* refactor

* add test schema edn

* private

* fix schema

* add test

* fix schema

* add test

* remove interface implement
  • Loading branch information
1e16miin authored Aug 22, 2024
1 parent 10880ae commit c360d04
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 9 deletions.
86 changes: 86 additions & 0 deletions dev-resources/complexity-analysis-error.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{:interfaces
{:Node {:fields {:id {:type (non-null ID)}}}
:Edge {:fields {:cursor {:type (non-null String)}
:node {:type (non-null :Node)}}}
:Connection {:fields {:edges {:type (non-null (list (non-null :Edge)))}
:pageInfo {:type (non-null :PageInfo)}}}
:User {:fields {:id {:type (non-null ID)}
:name {:type (non-null String)}}}}

:objects
{:PageInfo
{:fields {:startCursor {:type (non-null String)}
:endCursor {:type (non-null String)}
:hasNextPage {:type (non-null Boolean)}
:hasPreviousPage {:type (non-null Boolean)}}}

:Product
{:implements [:Node]
:fields {:id {:type (non-null ID)}
:seller {:type (non-null :Seller)}
:reviews
{:type (non-null :ReviewConnection)
:args {:first {:type Int}}
:resolve :resolve-reviews}
:likers
{:type (non-null :UserConnection)
:args {:first {:type Int
:default-value 5}}
:resolve :resolve-likers}}}

:ProductEdge
{:implements [:Edge]
:fields {:cursor {:type (non-null String)}
:node {:type (non-null :Product)}}}

:ProductConnection
{:implements [:Connection]
:fields {:edges {:type (non-null (list (non-null :ProductEdge)))}
:pageInfo {:type (non-null :PageInfo)}}}

:Review
{:implements [:Node]
:fields {:id {:type (non-null ID)}
:author {:type (non-null :User)}
:product {:type (non-null :Product)}}}

:ReviewEdge
{:implements [:Edge]
:fields {:cursor {:type (non-null String)}
:node {:type (non-null :Review)}}}

:ReviewConnection
{:implements [:Connection]
:fields {:edges {:type (non-null (list (non-null :ReviewEdge)))}
:pageInfo {:type (non-null :PageInfo)}}}

:Seller
{:implements [:Node :User]
:fields {:id {:type (non-null ID)}
:name {:type (non-null String)}
:products
{:type (non-null :ProductConnection)
:args {:first {:type Int}}
:resolve :resolve-products}}}

:Buyer
{:implements [:Node :User]
:fields {:id {:type (non-null ID)}
:name {:type (non-null String)}
:followings
{:type (non-null :UserConnection)
:args {:first {:type Int}}
:resolve :resolve-followings}}}

:UserEdge
{:fields {:cursor {:type (non-null String)}
:node {:type (non-null :User)}}}

:UserConnection
{:fields {:edges {:type (non-null (list (non-null :UserEdge)))}
:pageInfo {:type (non-null :PageInfo)}}}}

:queries {:node
{:type :Node
:args {:id {:type (non-null ID)}}
:resolve :resolve-node}}}
27 changes: 18 additions & 9 deletions src/com/walmartlabs/lacinia.clj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
[com.walmartlabs.lacinia.internal-utils :refer [cond-let]]
[com.walmartlabs.lacinia.util :refer [as-error-map]]
[com.walmartlabs.lacinia.resolve :as resolve]
[com.walmartlabs.lacinia.tracing :as tracing])
[com.walmartlabs.lacinia.tracing :as tracing]
[com.walmartlabs.lacinia.complexity-analysis :as complexity-analysis])
(:import (clojure.lang ExceptionInfo)))

(defn ^:private as-errors
Expand All @@ -33,11 +34,13 @@
Returns a [[ResolverResult]] that will deliver the result map, or an exception."
{:added "0.16.0"}
[parsed-query variables context]
{:pre [(map? parsed-query)
(or (nil? context)
(map? context))]}
(cond-let
([parsed-query variables context]
(execute-parsed-query-async parsed-query variables context nil))
([parsed-query variables context options]
{:pre [(map? parsed-query)
(or (nil? context)
(map? context))]}
(cond-let
:let [{:keys [::tracing/timing-start]} parsed-query
;; Validation phase encompasses preparing with query variables and actual validation.
;; It's somewhat all mixed together.
Expand All @@ -55,11 +58,17 @@

(seq validation-errors)
(resolve/resolve-as {:errors validation-errors})

:let [complexity-error (when (:max-complexity options)
(complexity-analysis/complexity-analysis prepared options))]

(some? complexity-error)
(resolve/resolve-as {:errors complexity-error})

:else
(executor/execute-query (assoc context constants/parsed-query-key prepared
::tracing/validation {:start-offset start-offset
:duration (tracing/duration start-nanos)}))))
::tracing/validation {:start-offset start-offset
:duration (tracing/duration start-nanos)})))))

(defn execute-parsed-query
"Prepares a query, by applying query variables to it, resulting in a prepared
Expand All @@ -76,7 +85,7 @@
{:keys [timeout-ms timeout-error]
:or {timeout-ms 0
timeout-error {:message "Query execution timed out."}}} options
execution-result (execute-parsed-query-async parsed-query variables context)
execution-result (execute-parsed-query-async parsed-query variables context options)
result (do
(resolve/on-deliver! execution-result *result)
;; Block on that deliver, then return the final result.
Expand Down
47 changes: 47 additions & 0 deletions src/com/walmartlabs/lacinia/complexity_analysis.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
(ns com.walmartlabs.lacinia.complexity-analysis
(:require [com.walmartlabs.lacinia.selection :as selection]))

(defn ^:private list-args? [arguments]
(some? (or (:first arguments)
(:last arguments))))

(defn ^:private summarize-selection
"Recursively summarizes the selection, handling field, inline fragment, and named fragment."
[{:keys [arguments selections field-name leaf? fragment-name] :as selection} fragment-map]
(let [selection-kind (selection/selection-kind selection)]
(cond
;; If it's a leaf node or `pageInfo`, return nil.
(or leaf? (= :pageInfo field-name))
nil

;; If it's a named fragment, look it up in the fragment-map and process its selections.
(= :named-fragment selection-kind)
(let [sub-selections (:selections (fragment-map fragment-name))]
(mapcat #(summarize-selection % fragment-map) sub-selections))

;; If it's an inline fragment or `edges` field, process its selections.
(or (= :inline-fragment selection-kind) (= field-name :edges))
(mapcat #(summarize-selection % fragment-map) selections)

;; Otherwise, handle a regular field with potential nested selections.
:else
(let [n-nodes (or (-> arguments (select-keys [:first :last]) vals first) 1)]
[{:field-name field-name
:selections (mapcat #(summarize-selection % fragment-map) selections)
:list-args? (list-args? arguments)
:n-nodes n-nodes}]))))

(defn ^:private calculate-complexity
[{:keys [selections list-args? n-nodes]}]
(let [children-complexity (apply + (map calculate-complexity selections))]
(if list-args?
(* n-nodes children-complexity)
(+ n-nodes children-complexity))))

(defn complexity-analysis
[query {:keys [max-complexity] :as _options}]
(let [{:keys [fragments selections]} query
summarized-selections (mapcat #(summarize-selection % fragments) selections)
complexity (calculate-complexity (first summarized-selections))]
(when (> complexity max-complexity)
{:message (format "Over max complexity! Current number of resources to be queried: %s" complexity)})))
148 changes: 148 additions & 0 deletions test/com/walmartlabs/lacinia/complexity_analysis_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
; Copyright (c) 2017-present Walmart, Inc.
;
; Licensed under the Apache License, Version 2.0 (the "License")
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.

(ns com.walmartlabs.lacinia.complexity-analysis-test
(:require
[clojure.test :refer [deftest is run-test testing]]
[com.walmartlabs.lacinia :refer [execute]]
[com.walmartlabs.test-utils :as utils]))


(defn ^:private resolve-products
[_ _ _]
{:edges []
:pageInfo {}})

(defn ^:private resolve-followings
[_ _ _]
{:edges []
:pageInfo {}})

(defn ^:private resolve-reviews
[_ _ _]
{:edges []
:pageInfo {}})

(defn ^:private resolve-likers
[_ _ _]
{:edges []
:pageInfo {}})

(defn ^:private resolve-node
[_ _ _]
{:edges []
:pageInfo {}})

(def ^:private schema
(utils/compile-schema "complexity-analysis-error.edn"
{:resolve-products resolve-products
:resolve-followings resolve-followings
:resolve-reviews resolve-reviews
:resolve-likers resolve-likers
:resolve-node resolve-node}))

(defn ^:private q [query variables]
(utils/simplify (execute schema query variables nil {:max-complexity 10})))

(deftest over-complexity-analysis
(testing "It is possible to calculate the complexity of a query in the Relay connection spec
by taking into account both named fragments and inline fragments."
(is (= {:errors {:message "Over max complexity! Current number of resources to be queried: 27"}}
(q "query ProductDetail($productId: ID){
node(id: $productId) {
... on Product {
...ProductLikersFragment
seller{
id
products(first: 5){
edges{
node{
id
}
}
}
}
reviews(first: 5){
edges{
node{
id
author{
id
}
}
}
}
}
}
}
fragment ProductLikersFragment on Product {
likers(first: 10){
edges{
node{
... on Seller{
id
}
... on Buyer{
id
}
}
}
}
}" {:productId "id"}))))
(testing "If no arguments are passed in the query, the calculation uses the default value defined in the schema."
(is (= {:errors {:message "Over max complexity! Current number of resources to be queried: 22"}}
(q "query ProductDetail($productId: ID){
node(id: $productId) {
... on Product {
...ProductLikersFragment
seller{
id
products(first: 5){
edges{
node{
id
}
}
}
}
reviews(first: 5){
edges{
node{
id
author{
id
}
}
}
}
}
}
}
fragment ProductLikersFragment on Product {
likers{
edges{
node{
... on Seller{
id
}
... on Buyer{
id
}
}
}
}
}" {:productId "id"})))))

(comment
(run-test over-complexity-analysis))

0 comments on commit c360d04

Please sign in to comment.