From 4fc49c1655c02b30235f221b88b23136e1437dd2 Mon Sep 17 00:00:00 2001 From: jq1836 Date: Fri, 18 Oct 2024 18:20:36 +0800 Subject: [PATCH 1/8] Implement basic working websocket --- frontend/components/matching/find-match.tsx | 75 +++++++++- frontend/lib/join-match-queue.ts | 24 ++++ frontend/package-lock.json | 71 ++++++++++ frontend/yarn.lock | 128 ++++++------------ .../app/exceptions/socket_exceptions.py | 2 + matching-service/app/logic/matching.py | 82 +++++++++++ matching-service/app/models/match.py | 8 +- matching-service/app/routers/match.py | 70 ++++++---- matching-service/app/utils/redis.py | 29 ++++ matching-service/app/utils/redis_utils.py | 111 --------------- matching-service/app/utils/socketmanager.py | 76 +++++++++++ 11 files changed, 446 insertions(+), 230 deletions(-) create mode 100644 frontend/lib/join-match-queue.ts create mode 100644 matching-service/app/exceptions/socket_exceptions.py create mode 100644 matching-service/app/logic/matching.py create mode 100644 matching-service/app/utils/redis.py delete mode 100644 matching-service/app/utils/redis_utils.py create mode 100644 matching-service/app/utils/socketmanager.py diff --git a/frontend/components/matching/find-match.tsx b/frontend/components/matching/find-match.tsx index 272f171397..14c2933266 100644 --- a/frontend/components/matching/find-match.tsx +++ b/frontend/components/matching/find-match.tsx @@ -4,13 +4,17 @@ import { MatchForm } from "@/components/matching/matching-form"; import { SearchProgress } from "@/components/matching/search-progress"; import { SelectionSummary } from "@/components/matching/selection-summary"; import { useToast } from "@/components/hooks/use-toast"; +import { useAuth } from "@/app/auth/auth-context"; +import { joinMatchQueue } from "@/lib/join-match-queue"; export default function FindMatch() { const [selectedDifficulty, setSelectedDifficulty] = useState(""); const [selectedTopic, setSelectedTopic] = useState(""); const [isSearching, setIsSearching] = useState(false); const [waitTime, setWaitTime] = useState(0); + const [websocket, setWebsocket] = useState(); const { toast } = useToast(); + const auth = useAuth(); useEffect(() => { let interval: NodeJS.Timeout | undefined; @@ -24,15 +28,78 @@ export default function FindMatch() { return () => clearInterval(interval); }, [isSearching]); - const handleSearch = () => { - if (selectedDifficulty && selectedTopic) { - setIsSearching(true); - } else { + useEffect(() => { + return () => { + if (websocket) { + websocket.close(); + } + }; + }, [websocket]); + + const handleSearch = async () => { + if (!selectedDifficulty || !selectedTopic) { toast({ title: "Invalid Selection", description: "Please select both a difficulty level and a topic", variant: "destructive", }); + return; + } + + if (!auth || !auth.token) { + toast({ + title: "Access denied", + description: "No authentication token found", + variant: "destructive", + }); + return; + } + + if (!auth.user) { + toast({ + title: "Access denied", + description: "Not logged in", + variant: "destructive", + }); + return; + } + + const response = await joinMatchQueue( + auth.token, + auth?.user?.id, + selectedTopic, + selectedDifficulty + ); + switch (response.status) { + case 200: + toast({ + title: "Matched", + description: "Successfully matched", + variant: "success", + }); + return; + case 202: + setIsSearching(true); + const ws = new WebSocket( + `ws://localhost:6969/match/subscribe/${auth?.user?.id}/${selectedTopic}/${selectedDifficulty}` + ); + ws.onmessage = () => { + setIsSearching(false); + toast({ + title: "Matched", + description: "Successfully matched", + variant: "success", + }); + }; + setWebsocket(ws); + return; + default: + toast({ + title: "Unknown Error", + description: "An unexpected error has occured", + variant: "destructive", + }); + return; } }; diff --git a/frontend/lib/join-match-queue.ts b/frontend/lib/join-match-queue.ts new file mode 100644 index 0000000000..a28f85862e --- /dev/null +++ b/frontend/lib/join-match-queue.ts @@ -0,0 +1,24 @@ +import { matchingServiceUri } from "@/lib/api-uri"; + +export const joinMatchQueue = async ( + jwtToken: string, + userId: string, + category: string, + complexity: string +) => { + const params = new URLSearchParams({ + topic: category, + difficulty: complexity, + }).toString(); + const response = await fetch( + `${matchingServiceUri(window.location.hostname)}/match/queue/${userId}?${params}`, + { + method: "POST", + headers: { + Authorization: `Bearer ${jwtToken}`, + "Content-Type": "application/json", + }, + } + ); + return response; +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 49929a83e6..dc65e6004d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,9 +9,11 @@ "version": "0.1.0", "dependencies": { "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", @@ -558,6 +560,36 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz", + "integrity": "sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", @@ -948,6 +980,45 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz", + "integrity": "sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 0fd4e45766..ee06417da5 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -141,46 +141,6 @@ dependencies: glob "10.3.10" -"@next/swc-darwin-arm64@14.2.11": - version "14.2.11" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.11.tgz#0022b52ccc62e21c34a34311ee0251e31cd5ff49" - integrity sha512-eiY9u7wEJZWp/Pga07Qy3ZmNEfALmmSS1HtsJF3y1QEyaExu7boENz11fWqDmZ3uvcyAxCMhTrA1jfVxITQW8g== - -"@next/swc-darwin-x64@14.2.11": - version "14.2.11" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.11.tgz#41151d22b5009a2a83bb57e6d98efb8a8995a3cd" - integrity sha512-lnB0zYCld4yE0IX3ANrVMmtAbziBb7MYekcmR6iE9bujmgERl6+FK+b0MBq0pl304lYe7zO4yxJus9H/Af8jbg== - -"@next/swc-linux-arm64-gnu@14.2.11": - version "14.2.11" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.11.tgz#8fdc732c39b79dc7519bb561c48a2417e0eb6b24" - integrity sha512-Ulo9TZVocYmUAtzvZ7FfldtwUoQY0+9z3BiXZCLSUwU2bp7GqHA7/bqrfsArDlUb2xeGwn3ZuBbKtNK8TR0A8w== - -"@next/swc-linux-arm64-musl@14.2.11": - version "14.2.11" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.11.tgz#04c3730ca4bc2f0c56c73db53dd03cf00126a243" - integrity sha512-fH377DnKGyUnkWlmUpFF1T90m0dADBfK11dF8sOQkiELF9M+YwDRCGe8ZyDzvQcUd20Rr5U7vpZRrAxKwd3Rzg== - -"@next/swc-linux-x64-gnu@14.2.11": - version "14.2.11" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.11.tgz#533823c0a96d31122671f670b58da65bdb71f330" - integrity sha512-a0TH4ZZp4NS0LgXP/488kgvWelNpwfgGTUCDXVhPGH6pInb7yIYNgM4kmNWOxBFt+TIuOH6Pi9NnGG4XWFUyXQ== - -"@next/swc-linux-x64-musl@14.2.11": - version "14.2.11" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.11.tgz#fa81b56095ef4a64bae7f5798e6cdeac9de2906f" - integrity sha512-DYYZcO4Uir2gZxA4D2JcOAKVs8ZxbOFYPpXSVIgeoQbREbeEHxysVsg3nY4FrQy51e5opxt5mOHl/LzIyZBoKA== - -"@next/swc-win32-arm64-msvc@14.2.11": - version "14.2.11" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.11.tgz#3ad4a72282f9b5e72f600522156e5bab6efb63a8" - integrity sha512-PwqHeKG3/kKfPpM6of1B9UJ+Er6ySUy59PeFu0Un0LBzJTRKKAg2V6J60Yqzp99m55mLa+YTbU6xj61ImTv9mg== - -"@next/swc-win32-ia32-msvc@14.2.11": - version "14.2.11" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.11.tgz#80a6dd48e2f8035c518c2162bf6058bd61921a1b" - integrity sha512-0U7PWMnOYIvM74GY6rbH6w7v+vNPDVH1gUhlwHpfInJnNe5LkmUZqhp7FNWeNa5wbVgRcRi1F1cyxp4dmeLLvA== - "@next/swc-win32-x64-msvc@14.2.11": version "14.2.11" resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.11.tgz" @@ -194,7 +154,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -246,7 +206,7 @@ "@radix-ui/react-checkbox@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz#6465b800420923ecc39cbeaa8f357b5f09dbfd52" + resolved "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.2.tgz" integrity sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw== dependencies: "@radix-ui/primitive" "1.1.0" @@ -410,7 +370,7 @@ "@radix-ui/react-progress@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.0.tgz#28c267885ec154fc557ec7a66cb462787312f7e2" + resolved "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz" integrity sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg== dependencies: "@radix-ui/react-context" "1.1.0" @@ -458,7 +418,7 @@ aria-hidden "^1.1.1" react-remove-scroll "2.6.0" -"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.1.0": +"@radix-ui/react-slot@^1.1.0", "@radix-ui/react-slot@1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz" integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw== @@ -610,14 +570,14 @@ resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz" integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== -"@types/react-dom@^18": +"@types/react-dom@*", "@types/react-dom@^18": version "18.3.0" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz" integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^18": +"@types/react@*", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^16.9.0 || ^17.0.0 || ^18.0.0", "@types/react@^18": version "18.3.5" resolved "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz" integrity sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA== @@ -647,7 +607,7 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0": +"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0", "@typescript-eslint/parser@^7.0.0": version "7.2.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz" integrity sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg== @@ -726,7 +686,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.9.0: +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.9.0: version "8.12.1" resolved "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== @@ -1011,21 +971,21 @@ class-variance-authority@^0.7.0: dependencies: clsx "2.0.0" -client-only@0.0.1, client-only@^0.0.1: +client-only@^0.0.1, client-only@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== -clsx@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz" - integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== - clsx@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== +clsx@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz" + integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" @@ -1401,7 +1361,7 @@ eslint-module-utils@^2.8.1, eslint-module-utils@^2.9.0: dependencies: debug "^3.2.7" -eslint-plugin-import@^2.28.1: +eslint-plugin-import@*, eslint-plugin-import@^2.28.1: version "2.30.0" resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz" integrity sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw== @@ -1489,7 +1449,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8: +eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^7.23.0 || ^8.0.0", eslint@^8, eslint@^8.56.0: version "8.57.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz" integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== @@ -1655,11 +1615,6 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -1712,7 +1667,7 @@ get-tsconfig@^4.7.5: dependencies: resolve-pkg-maps "^1.0.0" -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -1726,7 +1681,14 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@10.3.10, glob@^10.3.10: +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^10.3.10, glob@10.3.10: version "10.3.10" resolved "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz" integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== @@ -2241,13 +2203,6 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.3" picomatch "^2.3.1" -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -2262,6 +2217,13 @@ minimatch@^9.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" @@ -2546,6 +2508,15 @@ postcss-value-parser@^4.0.0: resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== +postcss@^8, postcss@^8.0.0, postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.23, postcss@>=8.0.9: + version "8.4.47" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz" + integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.0" + source-map-js "^1.2.1" + postcss@8.4.31: version "8.4.31" resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz" @@ -2555,15 +2526,6 @@ postcss@8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8, postcss@^8.4.23: - version "8.4.47" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz" - integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== - dependencies: - nanoid "^3.3.7" - picocolors "^1.1.0" - source-map-js "^1.2.1" - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -2593,7 +2555,7 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -react-dom@^18: +"react-dom@^16.8 || ^17 || ^18", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", react-dom@^18, react-dom@^18.2.0, react-dom@>=16.8.0: version "18.3.1" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== @@ -2634,7 +2596,7 @@ react-style-singleton@^2.2.1: invariant "^2.2.4" tslib "^2.0.0" -react@^18: +"react@^16.11.0 || ^17.0.0 || ^18.0.0", "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc", "react@^16.8 || ^17 || ^18", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.x || ^17.x || ^18.x", react@^18, react@^18.2.0, react@^18.3.1, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", react@>=16.8.0: version "18.3.1" resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== @@ -3002,7 +2964,7 @@ tailwindcss-animate@^1.0.7: resolved "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz" integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA== -tailwindcss@^3.4.1: +tailwindcss@^3.4.1, "tailwindcss@>=3.0.0 || insiders": version "3.4.11" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz" integrity sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg== @@ -3142,7 +3104,7 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" -typescript@^5: +typescript@^5, typescript@>=3.3.1, typescript@>=4.2.0: version "5.6.2" resolved "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz" integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== diff --git a/matching-service/app/exceptions/socket_exceptions.py b/matching-service/app/exceptions/socket_exceptions.py new file mode 100644 index 0000000000..c2c1932cef --- /dev/null +++ b/matching-service/app/exceptions/socket_exceptions.py @@ -0,0 +1,2 @@ +class NoExistingConnectionException(Exception): + pass diff --git a/matching-service/app/logic/matching.py b/matching-service/app/logic/matching.py new file mode 100644 index 0000000000..9771624a92 --- /dev/null +++ b/matching-service/app/logic/matching.py @@ -0,0 +1,82 @@ +from typing import Union + +from models.match import MatchModel, MessageModel +from utils.redis import get_redis, acquire_lock, release_lock +from utils.socketmanager import manager + +async def find_match_else_enqueue( + user_id: str, + topic: str, + difficulty: str +) -> Union[MessageModel, MatchModel]: + redis_client = await get_redis() + queue_key = _build_queue_key(topic, difficulty) + + result = None + + # ACQUIRE LOCK + islocked = await acquire_lock(redis_client, queue_key) + + if not islocked: + raise Exception("Could not acquire lock") + + # Check if the user is already in the queue + user_in_queue = await redis_client.lrange(queue_key, 0, -1) + if user_id in user_in_queue: + result = MessageModel( + message=f"User {user_id} is already in the queue, waiting for a match" + ) + else: + queue_length = await redis_client.llen(queue_key) + if queue_length > 0: + matched_user = await redis_client.rpop(queue_key) + result = MatchModel( + user1=user_id, + user2=matched_user, + topic=topic, + difficulty=difficulty, + ) + await manager.broadcast(matched_user, topic, difficulty, result.json()) + # await manager.disconnect(matched_user, topic, difficulty) + else: + await redis_client.lpush(queue_key, user_id) + result = MessageModel( + message=f"User {user_id} enqueued, waiting for a match" + ) + + # RELEASE LOCK + await release_lock(redis_client, queue_key) + return result + +async def remove_user_from_queue( + user_id: str, + topic: str, + difficulty: str +) -> MessageModel: + redis_client = await get_redis() + queue_key = _build_queue_key(topic, difficulty) + + # ACQUIRE LOCK + islocked = await acquire_lock(redis_client, queue_key) + + if not islocked: + raise Exception("Could not acquire lock") + + # Check if the user is already in the queue + user_in_queue = await redis_client.lrange(queue_key, 0, -1) + if user_id in user_in_queue: + await redis_client.lrem(queue_key, 0, user_id) + result = MessageModel(message=f"User {user_id} removed from the queue") + else: + result = MessageModel(message=f"User {user_id} is not in the queue") + + # RELEASE LOCK + await release_lock(redis_client, queue_key) + return result + +''' +Helper functions for matching. +''' +# Builds a queue key based on topic and difficulty +def _build_queue_key(topic: str, difficulty: str): + return f"{topic}:{difficulty}" diff --git a/matching-service/app/models/match.py b/matching-service/app/models/match.py index 7c920e5441..11c230b884 100644 --- a/matching-service/app/models/match.py +++ b/matching-service/app/models/match.py @@ -1,8 +1,10 @@ from pydantic import BaseModel -# Define the MatchData model -class MatchData(BaseModel): +class MatchModel(BaseModel): user1: str user2: str topic: str - difficulty: str \ No newline at end of file + difficulty: str + +class MessageModel(BaseModel): + message: str diff --git a/matching-service/app/routers/match.py b/matching-service/app/routers/match.py index 2bf104d21d..e945e508ce 100644 --- a/matching-service/app/routers/match.py +++ b/matching-service/app/routers/match.py @@ -1,38 +1,50 @@ -from utils.redis_utils import get_redis, build_queue_key, listen_for_matches, find_match_else_enqueue, match_channel, remove_user_from_queue -from models.match import MatchData -from fastapi import APIRouter import asyncio +from fastapi import APIRouter, WebSocket, WebSocketDisconnect +from fastapi.responses import JSONResponse +from typing import Union -router = APIRouter() - -# List to store matched pairs -matched_pairs = [] - -# Start the match listener in a background task +from logic.matching import find_match_else_enqueue, remove_user_from_queue +from models.match import MatchModel, MessageModel +from utils.socketmanager import manager - -@router.on_event("startup") -async def startup_event(): - redis_client = await get_redis() - asyncio.create_task(listen_for_matches(redis_client)) +router = APIRouter() # Add a user to the queue - - @router.post("/queue/{user_id}") -async def enqueue_user(user_id: str, topic: str, difficulty: str): - # redis_client = await get_redis() - return await find_match_else_enqueue(user_id, topic, difficulty) - -# Get all matched pairs - - -@router.get("/matches") -async def get_matched_pairs(): - return {"matched_pairs": matched_pairs} +async def enqueue_user( + user_id: str, + topic: str, + difficulty: str +): + response = await find_match_else_enqueue(user_id, topic, difficulty) + if isinstance(response, MatchModel): + return JSONResponse(status_code=200, content=response.json()) + elif isinstance(response, MessageModel): + return JSONResponse(status_code=202, content=response.json()) # Remove a user from the queue @router.delete("/queue/{user_id}") -async def dequeue_user(user_id: str, topic: str, difficulty: str): - print("removing user from queue") - return await remove_user_from_queue(user_id, topic, difficulty) \ No newline at end of file +async def dequeue_user( + user_id: str, + topic: str, + difficulty: str +): + return JSONResponse( + status_code=200, + content=await remove_user_from_queue(user_id, topic, difficulty) + ) + +@router.websocket("/subscribe/{user_id}/{topic}/{difficulty}") +async def subscribe( + websocket: WebSocket, + user_id: str, + topic: str, + difficulty: str, +): + await manager.connect(user_id, topic, difficulty, websocket) + try: + # Keep the socket listening + while True: + await websocket.receive_text() + except WebSocketDisconnect: + pass diff --git a/matching-service/app/utils/redis.py b/matching-service/app/utils/redis.py new file mode 100644 index 0000000000..bf3929f2e3 --- /dev/null +++ b/matching-service/app/utils/redis.py @@ -0,0 +1,29 @@ +import asyncio +import os +import redis.asyncio as redis + +# Initialize Redis client +async def get_redis(): + redis_host = os.getenv("REDIS_HOST", "localhost") + redis_port = os.getenv("REDIS_PORT", "6379") + return redis.Redis(host=redis_host, port=int(redis_port), db=0, decode_responses=True) + + +async def acquire_lock(redis_client, key, lock_timeout_ms=30000, retry_interval_ms=100, max_retries=100) -> bool: + lock_key = f"{key}:lock" + retries = 0 + + while retries < max_retries: + locked = await redis_client.set(lock_key, "locked", nx=True, px=lock_timeout_ms) + if locked: + return True + else: + retries += 1 + # Convert ms to seconds + await asyncio.sleep(retry_interval_ms / 1000) + + return False + +async def release_lock(redis_client, key) -> None: + lock_key = f"{key}:lock" + await redis_client.delete(lock_key) diff --git a/matching-service/app/utils/redis_utils.py b/matching-service/app/utils/redis_utils.py deleted file mode 100644 index 57aea1a227..0000000000 --- a/matching-service/app/utils/redis_utils.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -import redis.asyncio as redis -from models.match import MatchData -import asyncio -match_channel = 'match_channel' - -# Initialize Redis client - - -async def get_redis(): - redis_host = os.getenv("REDIS_HOST", "localhost") - redis_port = os.getenv("REDIS_PORT", "6379") - return redis.Redis(host=redis_host, port=int(redis_port), db=0, decode_responses=True) - - -async def acquire_lock(redis_client, key, lock_timeout_ms=30000, retry_interval_ms=100, max_retries=100) -> bool: - lock_key = f"{key}:lock" - retries = 0 - - while retries < max_retries: - locked = await redis_client.set(lock_key, "locked", nx=True, px=lock_timeout_ms) - if locked: - return True - else: - retries += 1 - # Convert ms to seconds - await asyncio.sleep(retry_interval_ms / 1000) - - return False - - -async def release_lock(redis_client, key) -> None: - lock_key = f"{key}:lock" - await redis_client.delete(lock_key) - - -# Helper function to build a unique queue key based on topic and difficulty -def build_queue_key(topic, difficulty): - return f"{topic}:{difficulty}" - -# Asynchronous task to listen for matches - - -async def listen_for_matches(redis_client): - pubsub = redis_client.pubsub() - await pubsub.subscribe(match_channel) - async for message in pubsub.listen(): - if message["type"] == "message": - print(f"Match notification: {message['data']}") - -# Asynchronous matching logic - - -async def find_match_else_enqueue(user_id, topic, difficulty): - redis_client = await get_redis() - queue_key = build_queue_key(topic, difficulty) - - result = None - - # ACQUIRE LOCK - islocked = await acquire_lock(redis_client, queue_key) - - if not islocked: - raise Exception("Could not acquire lock") - - # Check if the user is already in the queue - user_in_queue = await redis_client.lrange(queue_key, 0, -1) - if user_id in user_in_queue: - result = {"message": f"User { - user_id} is already in the queue, waiting for a match"} - else: - queue_length = await redis_client.llen(queue_key) - if queue_length > 0: - matched_user = await redis_client.rpop(queue_key) - match_data = MatchData( - user1=user_id, user2=matched_user, topic=topic, difficulty=difficulty) - await redis_client.publish(match_channel, match_data.json()) - result = {"message": "Match found", "match": match_data} - else: - await redis_client.lpush(queue_key, user_id) - result = {"message": f"User { - user_id} enqueued, waiting for a match"} - - # RELEASE LOCK - await release_lock(redis_client, queue_key) - return result - - -async def remove_user_from_queue(user_id, topic, difficulty): - redis_client = await get_redis() - queue_key = build_queue_key(topic, difficulty) - - # ACQUIRE LOCK - islocked = await acquire_lock(redis_client, queue_key) - - if not islocked: - raise Exception("Could not acquire lock") - - # Check if the user is already in the queue - user_in_queue = await redis_client.lrange(queue_key, 0, -1) - if user_id in user_in_queue: - await redis_client.lrem(queue_key, 0, user_id) - result = {"message": f"User { - user_id} removed from the queue"} - else: - result = {"message": f"User { - user_id} is not in the queue"} - - # RELEASE LOCK - await release_lock(redis_client, queue_key) - return result diff --git a/matching-service/app/utils/socketmanager.py b/matching-service/app/utils/socketmanager.py new file mode 100644 index 0000000000..d80e899438 --- /dev/null +++ b/matching-service/app/utils/socketmanager.py @@ -0,0 +1,76 @@ +import asyncio +from fastapi import WebSocket +from typing import Dict, List, Tuple + +from exceptions.socket_exceptions import NoExistingConnectionException + +''' +Web socket connection manager which manages connections by users. +''' +class ConnectionManager: + def __init__(self): + self.connection_map: Dict[ + Tuple[ + Annotated[str, "user_id"], + Annotated[str, "topic"], + Annotated[str, "complexity"] + ], List[WebSocket] + ] = {} + + ''' + Associates a (user_id, topic, complexity) with a websocket and accepts + the websocket connection. + ''' + async def connect( + self, + user_id: str, + topic: str, + complexity: str, + websocket: WebSocket, + ) -> None: + await websocket.accept() + key: Tuple[str, str, str] = (user_id, topic, complexity) + if key not in self.connection_map: + self.connection_map[key] = [] + self.connection_map[key].append(websocket) + + ''' + Disconnects all connections associated with (user_id, topic, complexity) + ''' + async def disconnect( + self, + user_id: str, + topic: str, + complexity: str, + websocket: WebSocket, + ) -> None: + key: Tuple[str, str, str] = (user_id, topic, complexity) + if not key in self.connection_map: + return + + await asyncio.gather( + *[websocket for websocket in self.connection_map[key]] + ) + del self.connection_map[key] + + ''' + Data is sent to through all connections associated with + (user_id, topic, complexity) + ''' + async def broadcast( + self, + user_id: str, + topic: str, + complexity: str, + data: str, + ): + key: Tuple[str, str, str] = (user_id, topic, complexity) + if not key in self.connection_map: + return + + await asyncio.gather( + *[websocket.send_json(data) for websocket in + self.connection_map[(user_id, topic, complexity)]] + ) + +manager = ConnectionManager() From a91a65ece20d1cb7a081006bc7f3eba23cbb7ec7 Mon Sep 17 00:00:00 2001 From: jq1836 Date: Fri, 18 Oct 2024 23:06:07 +0800 Subject: [PATCH 2/8] Add queue leaving --- frontend/components/matching/find-match.tsx | 72 +++++++++++++++++++-- frontend/lib/api-uri.ts | 6 ++ frontend/lib/leave-match-queue.ts | 24 +++++++ frontend/lib/subscribe-match.ts | 15 +++++ matching-service/app/logic/matching.py | 70 ++++++++------------ matching-service/app/routers/match.py | 16 ++--- matching-service/app/utils/redis.py | 14 ++-- matching-service/app/utils/socketmanager.py | 28 ++++++-- 8 files changed, 172 insertions(+), 73 deletions(-) create mode 100644 frontend/lib/leave-match-queue.ts create mode 100644 frontend/lib/subscribe-match.ts diff --git a/frontend/components/matching/find-match.tsx b/frontend/components/matching/find-match.tsx index 14c2933266..06a6e2e353 100644 --- a/frontend/components/matching/find-match.tsx +++ b/frontend/components/matching/find-match.tsx @@ -6,6 +6,9 @@ import { SelectionSummary } from "@/components/matching/selection-summary"; import { useToast } from "@/components/hooks/use-toast"; import { useAuth } from "@/app/auth/auth-context"; import { joinMatchQueue } from "@/lib/join-match-queue"; +import { leaveMatchQueue } from "@/lib/leave-match-queue"; +import { matchingServiceWebSockUri } from "@/lib/api-uri"; +import { subscribeMatch } from "@/lib/subscribe-match"; export default function FindMatch() { const [selectedDifficulty, setSelectedDifficulty] = useState(""); @@ -71,7 +74,7 @@ export default function FindMatch() { selectedDifficulty ); switch (response.status) { - case 200: + case 201: toast({ title: "Matched", description: "Successfully matched", @@ -79,9 +82,12 @@ export default function FindMatch() { }); return; case 202: + case 304: setIsSearching(true); - const ws = new WebSocket( - `ws://localhost:6969/match/subscribe/${auth?.user?.id}/${selectedTopic}/${selectedDifficulty}` + const ws = await subscribeMatch( + auth?.user.id, + selectedTopic, + selectedDifficulty ); ws.onmessage = () => { setIsSearching(false); @@ -90,7 +96,16 @@ export default function FindMatch() { description: "Successfully matched", variant: "success", }); + ws.onclose = () => null; }; + ws.onclose = () => { + setIsSearching(false); + toast({ + title: "Matching Stopped", + description: "Matching has been stopped", + variant: "destructive", + }); + } setWebsocket(ws); return; default: @@ -103,9 +118,54 @@ export default function FindMatch() { } }; - const handleCancel = () => { - setIsSearching(false); - setWaitTime(0); + const handleCancel = async () => { + if (!selectedDifficulty || !selectedTopic) { + toast({ + title: "Invalid Selection", + description: "Please select both a difficulty level and a topic", + variant: "destructive", + }); + return; + } + + if (!auth || !auth.token) { + toast({ + title: "Access denied", + description: "No authentication token found", + variant: "destructive", + }); + return; + } + + if (!auth.user) { + toast({ + title: "Access denied", + description: "Not logged in", + variant: "destructive", + }); + return; + } + + const response = await leaveMatchQueue(auth.token, auth.user?.id, selectedTopic, selectedDifficulty); + switch (response.status) { + case 200: + setIsSearching(false); + setWaitTime(0); + setWebsocket(undefined); + toast({ + title: "Success", + description: "Successfully left queue", + variant: "success", + }); + return; + default: + toast({ + title: "Unknown Error", + description: "An unexpected error has occured", + variant: "destructive", + }); + return; + } }; return ( diff --git a/frontend/lib/api-uri.ts b/frontend/lib/api-uri.ts index 6ec8e42783..b107b69b58 100644 --- a/frontend/lib/api-uri.ts +++ b/frontend/lib/api-uri.ts @@ -1,9 +1,15 @@ const constructUri = (baseUri: string, port: string | undefined) => `http://${process.env.NEXT_PUBLIC_BASE_URI || baseUri}:${port}`; +const constructWebSockUri = (baseUri: string, port: string | undefined) => + `ws://${process.env.NEXT_PUBLIC_BASE_URI || baseUri}:${port}`; + export const userServiceUri: (baseUri: string) => string = (baseUri) => constructUri(baseUri, process.env.NEXT_PUBLIC_USER_SVC_PORT); export const questionServiceUri: (baseUri: string) => string = (baseUri) => constructUri(baseUri, process.env.NEXT_PUBLIC_QUESTION_SVC_PORT); export const matchingServiceUri: (baseUri: string) => string = (baseUri) => constructUri(baseUri, process.env.NEXT_PUBLIC_MATCHING_SVC_PORT); + +export const matchingServiceWebSockUri: (baseUri: string) => string = (baseUri) => + constructWebSockUri(baseUri, process.env.NEXT_PUBLIC_MATCHING_SVC_PORT); diff --git a/frontend/lib/leave-match-queue.ts b/frontend/lib/leave-match-queue.ts new file mode 100644 index 0000000000..4e4c453438 --- /dev/null +++ b/frontend/lib/leave-match-queue.ts @@ -0,0 +1,24 @@ +import { matchingServiceUri } from "@/lib/api-uri"; + +export const leaveMatchQueue = async ( + jwtToken: string, + userId: string, + category: string, + complexity: string +) => { + const params = new URLSearchParams({ + topic: category, + difficulty: complexity, + }).toString(); + const response = await fetch( + `${matchingServiceUri(window.location.hostname)}/match/queue/${userId}?${params}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${jwtToken}`, + "Content-Type": "application/json", + }, + } + ); + return response; +}; diff --git a/frontend/lib/subscribe-match.ts b/frontend/lib/subscribe-match.ts new file mode 100644 index 0000000000..2167d612f6 --- /dev/null +++ b/frontend/lib/subscribe-match.ts @@ -0,0 +1,15 @@ +import { matchingServiceWebSockUri } from "@/lib/api-uri"; + +export const subscribeMatch = async ( + userId: string, + category: string, + complexity: string +) => { + const params = new URLSearchParams({ + topic: category, + difficulty: complexity, + }) + return new WebSocket( + `${matchingServiceWebSockUri(window.location.hostname)}/match/subscribe/${userId}?${params}` + ); +}; diff --git a/matching-service/app/logic/matching.py b/matching-service/app/logic/matching.py index 9771624a92..d086a786ca 100644 --- a/matching-service/app/logic/matching.py +++ b/matching-service/app/logic/matching.py @@ -1,78 +1,62 @@ +from fastapi.responses import JSONResponse, Response from typing import Union from models.match import MatchModel, MessageModel -from utils.redis import get_redis, acquire_lock, release_lock +from utils.redis import acquire_lock, redis_client, release_lock from utils.socketmanager import manager async def find_match_else_enqueue( user_id: str, topic: str, difficulty: str -) -> Union[MessageModel, MatchModel]: - redis_client = await get_redis() +) -> Union[Response, JSONResponse]: queue_key = _build_queue_key(topic, difficulty) - - result = None - - # ACQUIRE LOCK islocked = await acquire_lock(redis_client, queue_key) if not islocked: raise Exception("Could not acquire lock") # Check if the user is already in the queue - user_in_queue = await redis_client.lrange(queue_key, 0, -1) - if user_id in user_in_queue: - result = MessageModel( - message=f"User {user_id} is already in the queue, waiting for a match" - ) - else: - queue_length = await redis_client.llen(queue_key) - if queue_length > 0: - matched_user = await redis_client.rpop(queue_key) - result = MatchModel( - user1=user_id, - user2=matched_user, - topic=topic, - difficulty=difficulty, - ) - await manager.broadcast(matched_user, topic, difficulty, result.json()) - # await manager.disconnect(matched_user, topic, difficulty) - else: - await redis_client.lpush(queue_key, user_id) - result = MessageModel( - message=f"User {user_id} enqueued, waiting for a match" - ) + if user_id in await redis_client.lrange(queue_key, 0, -1): + await release_lock(redis_client, queue_key) + return Response(status_code=304) - # RELEASE LOCK + # Check if there are no other users in the queue + if await redis_client.llen(queue_key) == 0: + await redis_client.lpush(queue_key, user_id) + await release_lock(redis_client, queue_key) + return Response(status_code=202) + + # There is a user in the queue + matched_user = await redis_client.rpop(queue_key) await release_lock(redis_client, queue_key) - return result + response = MatchModel( + user1=matched_user, + user2=user_id, + topic=topic, + difficulty=difficulty, + ) + await manager.broadcast(matched_user, topic, difficulty, response.json()) + await manager.disconnect_all(matched_user, topic, difficulty) + return JSONResponse(status_code=201, content=response.json()) async def remove_user_from_queue( user_id: str, topic: str, difficulty: str -) -> MessageModel: - redis_client = await get_redis() +) -> Response: queue_key = _build_queue_key(topic, difficulty) - - # ACQUIRE LOCK islocked = await acquire_lock(redis_client, queue_key) if not islocked: raise Exception("Could not acquire lock") - # Check if the user is already in the queue - user_in_queue = await redis_client.lrange(queue_key, 0, -1) - if user_id in user_in_queue: + if user_id in await redis_client.lrange(queue_key, 0, -1): await redis_client.lrem(queue_key, 0, user_id) - result = MessageModel(message=f"User {user_id} removed from the queue") - else: - result = MessageModel(message=f"User {user_id} is not in the queue") - # RELEASE LOCK await release_lock(redis_client, queue_key) - return result + await manager.disconnect_all(user_id, topic, difficulty) + return Response(status_code=200) ''' Helper functions for matching. diff --git a/matching-service/app/routers/match.py b/matching-service/app/routers/match.py index e945e508ce..83d6642ea9 100644 --- a/matching-service/app/routers/match.py +++ b/matching-service/app/routers/match.py @@ -1,6 +1,5 @@ import asyncio from fastapi import APIRouter, WebSocket, WebSocketDisconnect -from fastapi.responses import JSONResponse from typing import Union from logic.matching import find_match_else_enqueue, remove_user_from_queue @@ -16,11 +15,7 @@ async def enqueue_user( topic: str, difficulty: str ): - response = await find_match_else_enqueue(user_id, topic, difficulty) - if isinstance(response, MatchModel): - return JSONResponse(status_code=200, content=response.json()) - elif isinstance(response, MessageModel): - return JSONResponse(status_code=202, content=response.json()) + return await find_match_else_enqueue(user_id, topic, difficulty) # Remove a user from the queue @router.delete("/queue/{user_id}") @@ -29,12 +24,9 @@ async def dequeue_user( topic: str, difficulty: str ): - return JSONResponse( - status_code=200, - content=await remove_user_from_queue(user_id, topic, difficulty) - ) + return await remove_user_from_queue(user_id, topic, difficulty) -@router.websocket("/subscribe/{user_id}/{topic}/{difficulty}") +@router.websocket("/subscribe/{user_id}") async def subscribe( websocket: WebSocket, user_id: str, @@ -47,4 +39,4 @@ async def subscribe( while True: await websocket.receive_text() except WebSocketDisconnect: - pass + manager.disconnect(user_id, topic, difficulty, websocket) diff --git a/matching-service/app/utils/redis.py b/matching-service/app/utils/redis.py index bf3929f2e3..acf1ea6c70 100644 --- a/matching-service/app/utils/redis.py +++ b/matching-service/app/utils/redis.py @@ -2,13 +2,6 @@ import os import redis.asyncio as redis -# Initialize Redis client -async def get_redis(): - redis_host = os.getenv("REDIS_HOST", "localhost") - redis_port = os.getenv("REDIS_PORT", "6379") - return redis.Redis(host=redis_host, port=int(redis_port), db=0, decode_responses=True) - - async def acquire_lock(redis_client, key, lock_timeout_ms=30000, retry_interval_ms=100, max_retries=100) -> bool: lock_key = f"{key}:lock" retries = 0 @@ -27,3 +20,10 @@ async def acquire_lock(redis_client, key, lock_timeout_ms=30000, retry_interval_ async def release_lock(redis_client, key) -> None: lock_key = f"{key}:lock" await redis_client.delete(lock_key) + +redis_client = redis.Redis( + host=os.environ.get("REDIS_HOST", "localhost"), + port=int(os.environ.get("REDIS_PORT", "6379")), + db=0, + decode_responses=True +) diff --git a/matching-service/app/utils/socketmanager.py b/matching-service/app/utils/socketmanager.py index d80e899438..e96dae46e1 100644 --- a/matching-service/app/utils/socketmanager.py +++ b/matching-service/app/utils/socketmanager.py @@ -37,22 +37,40 @@ async def connect( ''' Disconnects all connections associated with (user_id, topic, complexity) ''' - async def disconnect( + async def disconnect_all( self, user_id: str, topic: str, complexity: str, - websocket: WebSocket, ) -> None: key: Tuple[str, str, str] = (user_id, topic, complexity) - if not key in self.connection_map: + if key not in self.connection_map: return await asyncio.gather( - *[websocket for websocket in self.connection_map[key]] + *[websocket.close() for websocket in self.connection_map[key]] ) del self.connection_map[key] + ''' + Disconnects the single connection. + ''' + async def disconnect( + self, + user_id: str, + topic: str, + complexity: str, + websocket: WebSocket, + ): + key: Tuple[str, str, str] = (user_id, topic, complexity) + if key not in self.connection_map: + return + + self.connection_map[key].remove(websocket) + if len(self.connection_map[key]) == 0: + del self.connections_map[key] + websocket.close() + ''' Data is sent to through all connections associated with (user_id, topic, complexity) @@ -65,7 +83,7 @@ async def broadcast( data: str, ): key: Tuple[str, str, str] = (user_id, topic, complexity) - if not key in self.connection_map: + if key not in self.connection_map: return await asyncio.gather( From 04a860a602e60a2ff8b50e2b837615d4140c27f2 Mon Sep 17 00:00:00 2001 From: jq1836 Date: Fri, 18 Oct 2024 23:06:53 +0800 Subject: [PATCH 3/8] Format frontend --- frontend/components/matching/find-match.tsx | 10 +++++++--- frontend/lib/api-uri.ts | 5 +++-- frontend/lib/subscribe-match.ts | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/components/matching/find-match.tsx b/frontend/components/matching/find-match.tsx index 06a6e2e353..a42f5b2e16 100644 --- a/frontend/components/matching/find-match.tsx +++ b/frontend/components/matching/find-match.tsx @@ -7,7 +7,6 @@ import { useToast } from "@/components/hooks/use-toast"; import { useAuth } from "@/app/auth/auth-context"; import { joinMatchQueue } from "@/lib/join-match-queue"; import { leaveMatchQueue } from "@/lib/leave-match-queue"; -import { matchingServiceWebSockUri } from "@/lib/api-uri"; import { subscribeMatch } from "@/lib/subscribe-match"; export default function FindMatch() { @@ -105,7 +104,7 @@ export default function FindMatch() { description: "Matching has been stopped", variant: "destructive", }); - } + }; setWebsocket(ws); return; default: @@ -146,7 +145,12 @@ export default function FindMatch() { return; } - const response = await leaveMatchQueue(auth.token, auth.user?.id, selectedTopic, selectedDifficulty); + const response = await leaveMatchQueue( + auth.token, + auth.user?.id, + selectedTopic, + selectedDifficulty + ); switch (response.status) { case 200: setIsSearching(false); diff --git a/frontend/lib/api-uri.ts b/frontend/lib/api-uri.ts index b107b69b58..cad6a33664 100644 --- a/frontend/lib/api-uri.ts +++ b/frontend/lib/api-uri.ts @@ -11,5 +11,6 @@ export const questionServiceUri: (baseUri: string) => string = (baseUri) => export const matchingServiceUri: (baseUri: string) => string = (baseUri) => constructUri(baseUri, process.env.NEXT_PUBLIC_MATCHING_SVC_PORT); -export const matchingServiceWebSockUri: (baseUri: string) => string = (baseUri) => - constructWebSockUri(baseUri, process.env.NEXT_PUBLIC_MATCHING_SVC_PORT); +export const matchingServiceWebSockUri: (baseUri: string) => string = ( + baseUri +) => constructWebSockUri(baseUri, process.env.NEXT_PUBLIC_MATCHING_SVC_PORT); diff --git a/frontend/lib/subscribe-match.ts b/frontend/lib/subscribe-match.ts index 2167d612f6..b63ff73f48 100644 --- a/frontend/lib/subscribe-match.ts +++ b/frontend/lib/subscribe-match.ts @@ -8,7 +8,7 @@ export const subscribeMatch = async ( const params = new URLSearchParams({ topic: category, difficulty: complexity, - }) + }); return new WebSocket( `${matchingServiceWebSockUri(window.location.hostname)}/match/subscribe/${userId}?${params}` ); From c56d6b4a3ddb7e43fea1c4e13bd9968ecaf00e76 Mon Sep 17 00:00:00 2001 From: jq1836 Date: Sat, 19 Oct 2024 00:43:10 +0800 Subject: [PATCH 4/8] Add 60s timeout --- frontend/components/matching/find-match.tsx | 17 ++++++++++------- matching-service/app/routers/match.py | 2 +- matching-service/app/utils/socketmanager.py | 14 ++++++++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/frontend/components/matching/find-match.tsx b/frontend/components/matching/find-match.tsx index a42f5b2e16..4e7b82f13b 100644 --- a/frontend/components/matching/find-match.tsx +++ b/frontend/components/matching/find-match.tsx @@ -15,6 +15,7 @@ export default function FindMatch() { const [isSearching, setIsSearching] = useState(false); const [waitTime, setWaitTime] = useState(0); const [websocket, setWebsocket] = useState(); + const [queueTimeout, setQueueTimeout] = useState(); const { toast } = useToast(); const auth = useAuth(); @@ -26,17 +27,14 @@ export default function FindMatch() { }, 1000); } else { setWaitTime(0); + clearTimeout(queueTimeout); + setQueueTimeout(undefined); } - return () => clearInterval(interval); - }, [isSearching]); - useEffect(() => { return () => { - if (websocket) { - websocket.close(); - } + clearInterval(interval); }; - }, [websocket]); + }, [isSearching]); const handleSearch = async () => { if (!selectedDifficulty || !selectedTopic) { @@ -105,6 +103,11 @@ export default function FindMatch() { variant: "destructive", }); }; + setQueueTimeout( + setTimeout(() => { + handleCancel(); + }, 60000) + ); setWebsocket(ws); return; default: diff --git a/matching-service/app/routers/match.py b/matching-service/app/routers/match.py index 83d6642ea9..728b8c7a83 100644 --- a/matching-service/app/routers/match.py +++ b/matching-service/app/routers/match.py @@ -39,4 +39,4 @@ async def subscribe( while True: await websocket.receive_text() except WebSocketDisconnect: - manager.disconnect(user_id, topic, difficulty, websocket) + await manager.disconnect(user_id, topic, difficulty, websocket) diff --git a/matching-service/app/utils/socketmanager.py b/matching-service/app/utils/socketmanager.py index e96dae46e1..9447c84a7d 100644 --- a/matching-service/app/utils/socketmanager.py +++ b/matching-service/app/utils/socketmanager.py @@ -48,9 +48,9 @@ async def disconnect_all( return await asyncio.gather( - *[websocket.close() for websocket in self.connection_map[key]] + *[self._close_and_ignore(websocket) for websocket in self.connection_map[key]] ) - del self.connection_map[key] + self.connection_map.pop(key, None) ''' Disconnects the single connection. @@ -68,8 +68,14 @@ async def disconnect( self.connection_map[key].remove(websocket) if len(self.connection_map[key]) == 0: - del self.connections_map[key] - websocket.close() + self.connection_map.pop(key, None) + self._close_and_ignore(websocket) + + async def _close_and_ignore(self, websocket: WebSocket): + try: + await websocket.close() + except Exception: + pass ''' Data is sent to through all connections associated with From aa881b3dee2fcc59715fa060a41905d90512b8bc Mon Sep 17 00:00:00 2001 From: jq1836 Date: Sat, 19 Oct 2024 02:30:26 +0800 Subject: [PATCH 5/8] Fix refresh bug --- matching-service/app/routers/match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matching-service/app/routers/match.py b/matching-service/app/routers/match.py index 728b8c7a83..a0ae48c635 100644 --- a/matching-service/app/routers/match.py +++ b/matching-service/app/routers/match.py @@ -39,4 +39,4 @@ async def subscribe( while True: await websocket.receive_text() except WebSocketDisconnect: - await manager.disconnect(user_id, topic, difficulty, websocket) + await remove_user_from_queue(user_id, topic, difficulty) From 39d85cfa7d35906b19f36d58fe76062c05d9bcc6 Mon Sep 17 00:00:00 2001 From: jq1836 Date: Sat, 19 Oct 2024 02:34:23 +0800 Subject: [PATCH 6/8] Cleanup frontend --- frontend/components/matching/find-match.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/frontend/components/matching/find-match.tsx b/frontend/components/matching/find-match.tsx index 4e7b82f13b..6aef746d48 100644 --- a/frontend/components/matching/find-match.tsx +++ b/frontend/components/matching/find-match.tsx @@ -14,8 +14,6 @@ export default function FindMatch() { const [selectedTopic, setSelectedTopic] = useState(""); const [isSearching, setIsSearching] = useState(false); const [waitTime, setWaitTime] = useState(0); - const [websocket, setWebsocket] = useState(); - const [queueTimeout, setQueueTimeout] = useState(); const { toast } = useToast(); const auth = useAuth(); @@ -27,8 +25,6 @@ export default function FindMatch() { }, 1000); } else { setWaitTime(0); - clearTimeout(queueTimeout); - setQueueTimeout(undefined); } return () => { @@ -86,8 +82,12 @@ export default function FindMatch() { selectedTopic, selectedDifficulty ); + const queueTimeout = setTimeout(() => { + handleCancel(); + }, 60000); ws.onmessage = () => { setIsSearching(false); + clearTimeout(queueTimeout); toast({ title: "Matched", description: "Successfully matched", @@ -97,18 +97,13 @@ export default function FindMatch() { }; ws.onclose = () => { setIsSearching(false); + clearTimeout(queueTimeout); toast({ title: "Matching Stopped", description: "Matching has been stopped", variant: "destructive", }); }; - setQueueTimeout( - setTimeout(() => { - handleCancel(); - }, 60000) - ); - setWebsocket(ws); return; default: toast({ @@ -158,7 +153,6 @@ export default function FindMatch() { case 200: setIsSearching(false); setWaitTime(0); - setWebsocket(undefined); toast({ title: "Success", description: "Successfully left queue", From 49f57f6aeb8121992190df2dfe99dd2227e03dc2 Mon Sep 17 00:00:00 2001 From: jq1836 Date: Sun, 20 Oct 2024 22:36:27 +0800 Subject: [PATCH 7/8] Add queue debug --- matching-service/app/logic/matching.py | 25 +++++++++++++++++++++++-- test.py => matching-service/app/test.py | 0 2 files changed, 23 insertions(+), 2 deletions(-) rename test.py => matching-service/app/test.py (100%) diff --git a/matching-service/app/logic/matching.py b/matching-service/app/logic/matching.py index d086a786ca..1919b64c54 100644 --- a/matching-service/app/logic/matching.py +++ b/matching-service/app/logic/matching.py @@ -16,19 +16,28 @@ async def find_match_else_enqueue( if not islocked: raise Exception("Could not acquire lock") + queue = await redis_client.lrange(queue_key, 0, -1) + _print_queue_state(topic, difficulty, queue, True) + # Check if the user is already in the queue - if user_id in await redis_client.lrange(queue_key, 0, -1): + if user_id in queue: await release_lock(redis_client, queue_key) return Response(status_code=304) # Check if there are no other users in the queue if await redis_client.llen(queue_key) == 0: await redis_client.lpush(queue_key, user_id) + print(f"QUEUE: Added {user_id} to queue") + queue = await redis_client.lrange(queue_key, 0, -1) + _print_queue_state(topic, difficulty, queue, False) await release_lock(redis_client, queue_key) return Response(status_code=202) # There is a user in the queue matched_user = await redis_client.rpop(queue_key) + print(f"QUEUE: Match found for {user_id} and {matched_user}") + queue = await redis_client.lrange(queue_key, 0, -1) + _print_queue_state(topic, difficulty, queue, False) await release_lock(redis_client, queue_key) response = MatchModel( user1=matched_user, @@ -51,8 +60,14 @@ async def remove_user_from_queue( if not islocked: raise Exception("Could not acquire lock") - if user_id in await redis_client.lrange(queue_key, 0, -1): + queue = await redis_client.lrange(queue_key, 0, -1) + _print_queue_state(topic, difficulty, queue, True) + + if user_id in queue: await redis_client.lrem(queue_key, 0, user_id) + print(f"QUEUE: Removed {user_id} from queue") + queue = await redis_client.lrange(queue_key, 0, -1) + _print_queue_state(topic, difficulty, queue, False) await release_lock(redis_client, queue_key) await manager.disconnect_all(user_id, topic, difficulty) @@ -64,3 +79,9 @@ async def remove_user_from_queue( # Builds a queue key based on topic and difficulty def _build_queue_key(topic: str, difficulty: str): return f"{topic}:{difficulty}" + +def _print_queue_state(topic, difficulty, queue, before: bool): + if before: + print(f"QUEUE: Before Queue for {(topic, difficulty)}: ", queue) + else: + print(f"QUEUE: After Queue for {(topic, difficulty)}: ", queue) diff --git a/test.py b/matching-service/app/test.py similarity index 100% rename from test.py rename to matching-service/app/test.py From 3518f5b6d8bf9b9b8294800fae539ab9dda79d00 Mon Sep 17 00:00:00 2001 From: jq1836 Date: Mon, 21 Oct 2024 00:06:14 +0800 Subject: [PATCH 8/8] Fix timeout and add logging --- frontend/components/matching/find-match.tsx | 28 ++++++++++++++------- matching-service/app/logger.py | 9 +++++++ matching-service/app/logic/matching.py | 28 +++++++++++---------- 3 files changed, 43 insertions(+), 22 deletions(-) create mode 100644 matching-service/app/logger.py diff --git a/frontend/components/matching/find-match.tsx b/frontend/components/matching/find-match.tsx index 6aef746d48..580bdc0d59 100644 --- a/frontend/components/matching/find-match.tsx +++ b/frontend/components/matching/find-match.tsx @@ -17,6 +17,8 @@ export default function FindMatch() { const { toast } = useToast(); const auth = useAuth(); + const waitTimeout = 60000; + useEffect(() => { let interval: NodeJS.Timeout | undefined; if (isSearching) { @@ -83,8 +85,8 @@ export default function FindMatch() { selectedDifficulty ); const queueTimeout = setTimeout(() => { - handleCancel(); - }, 60000); + handleCancel(true); + }, waitTimeout); ws.onmessage = () => { setIsSearching(false); clearTimeout(queueTimeout); @@ -115,7 +117,7 @@ export default function FindMatch() { } }; - const handleCancel = async () => { + const handleCancel = async (timedOut: boolean) => { if (!selectedDifficulty || !selectedTopic) { toast({ title: "Invalid Selection", @@ -153,11 +155,19 @@ export default function FindMatch() { case 200: setIsSearching(false); setWaitTime(0); - toast({ - title: "Success", - description: "Successfully left queue", - variant: "success", - }); + if (timedOut) { + toast({ + title: "Timed Out", + description: "Matching has been stopped", + variant: "destructive", + }); + } else { + toast({ + title: "Matching Stopped", + description: "Matching has been stopped", + variant: "destructive", + }); + } return; default: toast({ @@ -178,7 +188,7 @@ export default function FindMatch() { setSelectedTopic={setSelectedTopic} handleSearch={handleSearch} isSearching={isSearching} - handleCancel={handleCancel} + handleCancel={() => handleCancel(false)} /> {isSearching && } diff --git a/matching-service/app/logger.py b/matching-service/app/logger.py new file mode 100644 index 0000000000..74860eee9b --- /dev/null +++ b/matching-service/app/logger.py @@ -0,0 +1,9 @@ +import logging +import sys + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +stream_handler = logging.StreamHandler(sys.stdout) +log_formatter = logging.Formatter("%(levelname)s: %(message)s") +stream_handler.setFormatter(log_formatter) +logger.addHandler(stream_handler) diff --git a/matching-service/app/logic/matching.py b/matching-service/app/logic/matching.py index 1919b64c54..6c52014dae 100644 --- a/matching-service/app/logic/matching.py +++ b/matching-service/app/logic/matching.py @@ -1,6 +1,7 @@ from fastapi.responses import JSONResponse, Response from typing import Union +from logger import logger from models.match import MatchModel, MessageModel from utils.redis import acquire_lock, redis_client, release_lock from utils.socketmanager import manager @@ -17,7 +18,7 @@ async def find_match_else_enqueue( raise Exception("Could not acquire lock") queue = await redis_client.lrange(queue_key, 0, -1) - _print_queue_state(topic, difficulty, queue, True) + logger.debug(_get_queue_state_message(topic, difficulty, queue, True)) # Check if the user is already in the queue if user_id in queue: @@ -27,17 +28,17 @@ async def find_match_else_enqueue( # Check if there are no other users in the queue if await redis_client.llen(queue_key) == 0: await redis_client.lpush(queue_key, user_id) - print(f"QUEUE: Added {user_id} to queue") + logger.debug(f"Added {user_id} to Queue {(topic, difficulty)}") queue = await redis_client.lrange(queue_key, 0, -1) - _print_queue_state(topic, difficulty, queue, False) + logger.debug(_get_queue_state_message(topic, difficulty, queue, False)) await release_lock(redis_client, queue_key) return Response(status_code=202) # There is a user in the queue matched_user = await redis_client.rpop(queue_key) - print(f"QUEUE: Match found for {user_id} and {matched_user}") + logger.debug(f"Match found for {user_id} and {matched_user}") queue = await redis_client.lrange(queue_key, 0, -1) - _print_queue_state(topic, difficulty, queue, False) + logger.debug(_get_queue_state_message(topic, difficulty, queue, False)) await release_lock(redis_client, queue_key) response = MatchModel( user1=matched_user, @@ -61,13 +62,14 @@ async def remove_user_from_queue( raise Exception("Could not acquire lock") queue = await redis_client.lrange(queue_key, 0, -1) - _print_queue_state(topic, difficulty, queue, True) + logger.debug(_get_queue_state_message(topic, difficulty, queue, True)) if user_id in queue: await redis_client.lrem(queue_key, 0, user_id) - print(f"QUEUE: Removed {user_id} from queue") - queue = await redis_client.lrange(queue_key, 0, -1) - _print_queue_state(topic, difficulty, queue, False) + logger.debug(f"Removed {user_id} from queue {(topic, difficulty)}") + + queue = await redis_client.lrange(queue_key, 0, -1) + logger.debug(_get_queue_state_message(topic, difficulty, queue, False)) await release_lock(redis_client, queue_key) await manager.disconnect_all(user_id, topic, difficulty) @@ -80,8 +82,8 @@ async def remove_user_from_queue( def _build_queue_key(topic: str, difficulty: str): return f"{topic}:{difficulty}" -def _print_queue_state(topic, difficulty, queue, before: bool): +def _get_queue_state_message(topic, difficulty, queue, before: bool): + postfix = f"Queue for {(topic, difficulty)}: {queue}" if before: - print(f"QUEUE: Before Queue for {(topic, difficulty)}: ", queue) - else: - print(f"QUEUE: After Queue for {(topic, difficulty)}: ", queue) + return "Before - " + postfix + return "After - " + postfix