forked from walmartlabs/lacinia
-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
쿼리 복잡도 분석 #13
Merged
Merged
쿼리 복잡도 분석 #13
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
38d5234
add complexity analysis
1e16miin 6653697
add comment
1e16miin 872194e
apply review
1e16miin c1adf2b
nit
1e16miin 2b65f6a
refactor
1e16miin 7ce8d07
nit
1e16miin 34c2bf4
refactor match to cond
1e16miin 714b2e3
apply review
1e16miin c8c504c
nit
1e16miin 4000dbd
remove
1e16miin 2c5ffd3
fix lint
1e16miin f95f096
private
1e16miin 6c5c2ef
add test
1e16miin 8886a39
refactor
1e16miin 66cc17f
add test schema edn
1e16miin 6549c19
private
1e16miin a5eb59b
fix schema
1e16miin 2ac359c
add test
1e16miin a25afd8
fix schema
1e16miin 98b0c93
add test
1e16miin 279b65e
remove interface implement
1e16miin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]}] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혹시 여기서 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 재귀 돌면서 |
||
(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
148
test/com/walmartlabs/lacinia/complexity_analysis_test.clj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재는 여기서 options에 :max-complexity 라는 key에 value를 넣어서 최대 리소스를 제한하는 것을 고려하였는데요.
system.edn 의 config 에 max-complexity를 하나 정의하여 context로 부터 가져와도 될 것 같다고 생각하는데 고민이긴 합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저희 백엔드 코드에서 build-lacinia-options할 때 현재 환경이 dev / staging인 경우에만 :max-complexity 값을 하드코딩해서 넘겨도 충분할 것 같습니다(ex:
:max-complexity 1000000
)굳이 system.edn에는 추가 안 해도 될 것 같슴다
https://github.com/green-labs/farmmorning-backend/blob/68978493665d1027f45c4a64100a0855d5cb7781/bases/core-api/src/farmmorning/core_api/graphql/handler.clj#L97
다른 lacinia 옵션값들도 그냥 하드코딩하고 있음
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
근데 지금 바로 반영된다면, 앱 코드들에 first: 9999 이런 잔재 때문에 프론트 개발자분들이 무조건 에러 응답들을 받아보실 텐데요..
우리 백엔드 미들웨어에서 라시니아 내부에서 뱉는 max-complexity 관련 에러는 로깅이나 슬랙만 보내고 by pass 한다던지의 의사결정만 될 듯 합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
한 10만 정도로 설정해두고,
error에 code 하나 심어서 해당 code가 에러에 잡히면 근재님께서 제안해준 것 처럼 로깅이나 슬랙만 보내고 by pass하는게 좋을거같네요!