From 266971d9fb642ca5dfc8692cb427720b8f2cb188 Mon Sep 17 00:00:00 2001 From: Ryan Chia Date: Tue, 12 Nov 2024 15:26:30 +0800 Subject: [PATCH] Squashed commit of the following: commit e0c62de5608652a6ad501119c3e74cecbf23b1c8 Author: Ryan Chia Date: Tue Nov 12 15:23:44 2024 +0800 asd commit d3eb518711444100d107b53dff555735a18a5cc4 Author: Ryan Chia Date: Tue Nov 12 15:21:42 2024 +0800 asd commit 75559763d8bba422279f725250eab82bcd8e29e0 Author: Ryan Chia Date: Tue Nov 12 15:07:33 2024 +0800 asd commit 3657a9c493956ca91a3a6f40ecb624c0c1e7edf4 Author: Ryan Chia Date: Tue Nov 12 15:05:03 2024 +0800 asd commit 64e2651e87df7f34a684b2fadba6b5f2b8a1a0b0 Author: Ryan Chia Date: Tue Nov 12 15:00:01 2024 +0800 asd commit 4c405f4abeea7037ee8e00793d304fd0cc852b04 Author: Ryan Chia Date: Tue Nov 12 14:57:22 2024 +0800 asd commit c4ccd113e48496d0c397e6dc74b15fc4d004c56d Merge: e400a71 31a6bd1 Author: chiaryan <53717471+chiaryan@users.noreply.github.com> Date: Tue Nov 12 14:54:30 2024 +0800 Merge branch 'staging' into browser-tests commit e400a7176baaa4e272963760be97de84c9bf943a Author: Ryan Chia Date: Tue Nov 12 14:53:23 2024 +0800 asd commit e92e18608afc9a882da7568bfffbe0b96cf01146 Author: Ryan Chia Date: Tue Nov 12 14:47:29 2024 +0800 asd commit 3a975ffd4232f436bc9c817df17cd7312086e66f Author: Ryan Chia Date: Tue Nov 12 14:37:56 2024 +0800 asd commit 96636881de0af527908c2a9b97694940c74ad806 Author: Ryan Chia Date: Tue Nov 12 14:19:17 2024 +0800 asd add browsers commit b0b9fb5e1a4b6440e7e48015daa2cde648fae3c3 Merge: bf9f160 f86888b Author: Ryan Chia Date: Tue Nov 12 14:18:56 2024 +0800 Merge branch 'browser-compatibility-tests' into browser-tests commit 31a6bd1f882c4776b60029b1c1d1852f39201824 Merge: 7d931ee 3fd5752 Author: Benjamin Soh Zikang <97374822+bensohh@users.noreply.github.com> Date: Tue Nov 12 10:20:13 2024 +0800 Merge pull request #71 from CS3219-AY2425S1/titus/fix-local-storage-bug fix(frontend): :bug: mount component before accessing local storage commit 3fd5752dc19a7eca8be962f2dcaa2d13c7c61167 Merge: fdce97a 7d931ee Author: Benjamin Soh Zikang <97374822+bensohh@users.noreply.github.com> Date: Mon Nov 11 12:40:38 2024 +0800 Merge branch 'staging' into titus/fix-local-storage-bug commit 7d931eebf8b778a540b23fb49a524b30b3b8787e Merge: 439b410 d75215c Author: Benjamin Soh Zikang <97374822+bensohh@users.noreply.github.com> Date: Mon Nov 11 12:20:10 2024 +0800 Merge pull request #70 from CS3219-AY2425S1/titus/add-message-queue feat: add message queue commit bf9f160e2b80ac46cb3ac3baf817866f2dd7a6b3 Merge: 862051a 311be82 Author: Ryan Chia Date: Mon Nov 11 04:56:37 2024 +0800 Merge branch 'browser-compatibility-tests' into browser-tests commit 862051a6c53d75de3255c07deacaa6ed41156367 Author: Ryan Chia Date: Sun Nov 10 21:16:04 2024 +0800 asd commit f58f0351a0f83babfcc755ed4b4fc0909ed3cc09 Author: Ryan Chia Date: Sun Nov 10 21:13:59 2024 +0800 asd commit c78449683241496b903e01b143093f07c42c7e4c Author: Ryan Chia Date: Sun Nov 10 21:00:52 2024 +0800 asd commit 2f97a34c78b31c528b9d95cb247281236114f0a5 Author: Ryan Chia Date: Sun Nov 10 20:45:26 2024 +0800 asd commit cccfa548c8833c97d17d63f833bd73a1781825a4 Author: Ryan Chia Date: Sun Nov 10 20:44:24 2024 +0800 install pnpm commit 3570555db6221f063e29670e9110f5b2dda79bb4 Author: Ryan Chia Date: Sun Nov 10 20:32:56 2024 +0800 asd commit 45ebeb433a3ca542a98fd90e1ab31092d5bc49f2 Author: Ryan Chia Date: Sun Nov 10 20:32:04 2024 +0800 asd commit f1e7e40222a3f96cce21e6df3d4596c7f257ee71 Author: Ryan Chia Date: Sun Nov 10 20:08:35 2024 +0800 asd commit f5b521a2f4d26ec5207df63c2e128f4d05551801 Author: Ryan Chia Date: Sun Nov 10 20:08:03 2024 +0800 asd commit cc46721c40d11b60c05287e0facbc9c69a0399a7 Author: Ryan Chia Date: Sun Nov 10 13:47:37 2024 +0800 asd commit 9f1efaffdee71440839ca67e2ed7f39e1c0bec9a Author: Ryan Chia Date: Sun Nov 10 11:55:18 2024 +0800 asd s commit 67960833336c4c9818983f855ad11d72eb03b454 Author: Ryan Chia Date: Sun Nov 10 11:32:00 2024 +0800 asd commit 5b79d8be4f25309d4cfe05a61d5a406ddfccf0c6 Author: Ryan Chia Date: Sun Nov 10 11:28:49 2024 +0800 asd commit 240c839afa0bd5fc4690b9c2e8d0b74f45d69b59 Author: Ryan Chia Date: Sun Nov 10 11:27:52 2024 +0800 asd commit 8958cb6151a2423236ab8c5527ddc464b5185079 Author: Ryan Chia Date: Sun Nov 10 11:23:29 2024 +0800 asd commit ab4cc476f7ba56be6b9e0ef2106133a6b835ac17 Author: Ryan Chia Date: Sun Nov 10 11:22:05 2024 +0800 try selenium test commit 9656decd3259bcc36511e47b5e4800571c3c56bb Merge: 439b410 324463e Author: Ryan Chia Date: Sat Nov 9 16:14:56 2024 +0800 Merge branch 'commit-signalling-.dockerignore' into browser-tests commit fdce97ae834ba27204624fa648695b31175c4645 Author: tituschewxj Date: Fri Nov 8 14:17:53 2024 +0800 fix(videoPanel): :bug: default for undefined partnerId Include a check to validate if partnerID is not undefined commit e5e41ce92afd50a32eef8e3889315ba4f56d4eaa Author: tituschewxj Date: Thu Nov 7 14:07:00 2024 +0800 fix(frontend): :bug: mounts component before accessing local storage Only access localStorage on the client side (in the browser) after the component has mounted. commit d75215c120d6b03c06ac889be0e769fa4dc8122d Author: tituschewxj Date: Thu Nov 7 10:40:15 2024 +0800 style(execution-service): :fire: remove commented code Revmoes the previous commented implementation of sending the submission to history service via REST API call. commit fac44307e888ab2a9c72699fa6a329a847f224a7 Author: tituschewxj Date: Thu Nov 7 01:20:36 2024 +0800 docs: update readme commit 2b2bbd59a6c1be61563db2ee1e0e9ff7fc4b73cb Author: tituschewxj Date: Thu Nov 7 01:12:41 2024 +0800 docs: update readme commit 95a82dc161702934cf68a40368d3a74ab2235fe4 Author: tituschewxj Date: Thu Nov 7 01:10:12 2024 +0800 ci: update docker compose test commit bf0c096955050d55b544b487a70f617eb284def6 Author: tituschewxj Date: Thu Nov 7 01:07:32 2024 +0800 docs: update readme commit b8e3004f8d21268f0f7c42eec4fe3deb83b53203 Author: tituschewxj Date: Thu Nov 7 00:54:53 2024 +0800 docs: update readme commit 8bd3d10c71bf007fb2723d5fcd81ef45e088e36a Author: tituschewxj Date: Thu Nov 7 00:52:29 2024 +0800 feat: update readme commit fbd3a4de76d4f1254c6943c01dc40f14816d7fde Author: tituschewxj Date: Thu Nov 7 00:45:09 2024 +0800 feat: update log msg commit 64a1badcfebdedcef17f31d6539c747b05481588 Author: tituschewxj Date: Thu Nov 7 00:33:50 2024 +0800 fix: update docker-compose commit e48bc7ea91cf62bb9661613fefe0194b71ed3525 Author: tituschewxj Date: Wed Nov 6 23:17:41 2024 +0800 feat: update docker compose commit 8a0e046576e4469d485ca3f11128d3419d330069 Author: tituschewxj Date: Wed Nov 6 23:17:35 2024 +0800 feat: implement rabbitmq msg queue commit 324463e3b1ae0812202668ebeacdd1f13c4b7b64 Author: Ryan Chia Date: Wed Nov 6 01:33:41 2024 +0800 branch name --- .github/workflows/test.yml | 28 ++-- apps/docker-compose.yml | 24 +++- apps/execution-service/.env.example | 2 + apps/execution-service/README.md | 123 +++++++++++------- apps/execution-service/go.mod | 1 + apps/execution-service/go.sum | 2 + apps/execution-service/handlers/submit.go | 33 +---- apps/execution-service/main.go | 18 +-- .../messagequeue/rabbitmq.go | 78 +++++++++++ apps/execution-service/utils/log.go | 9 ++ .../__tests__/browser-tests/browser.test.ts | 42 ++++-- .../src/app/collaboration/[id]/page.tsx | 59 ++++++++- .../CollaborativeEditor.tsx | 47 +++---- .../src/components/VideoPanel/VideoPanel.tsx | 20 ++- apps/history-service/.env.example | 8 +- apps/history-service/README.md | 27 +++- apps/history-service/databases/history.go | 32 +++++ apps/history-service/go.mod | 1 + apps/history-service/go.sum | 2 + apps/history-service/handlers/create.go | 19 +-- apps/history-service/main.go | 16 ++- apps/history-service/messagequeue/rabbitmq.go | 108 +++++++++++++++ apps/history-service/utils/log.go | 9 ++ 23 files changed, 543 insertions(+), 165 deletions(-) create mode 100644 apps/execution-service/messagequeue/rabbitmq.go create mode 100644 apps/execution-service/utils/log.go create mode 100644 apps/history-service/databases/history.go create mode 100644 apps/history-service/messagequeue/rabbitmq.go create mode 100644 apps/history-service/utils/log.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8d18e13b1c..fe4ee21edc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - + - name: Set up .env env: QUESTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }} @@ -30,7 +30,7 @@ jobs: echo "FIREBASE_CREDENTIAL_PATH=$QUESTION_FIREBASE_CREDENTIAL_PATH" >> .env echo "JWT_SECRET=$JWT_SECRET" >> .env echo "EXECUTION_SERVICE_URL=$EXECUTION_SERVICE_URL" >> .env - + - name: Set up credentials env: QUESTION_FIREBASE_JSON: ${{ secrets.QUESTION_SERVICE_FIREBASE_CREDENTIAL }} @@ -41,8 +41,8 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 - with: - go-version: '1.23.x' + with: + go-version: "1.23.x" - name: Install Go dependencies run: | @@ -51,7 +51,7 @@ jobs: - name: Install firebase tools run: curl -sL firebase.tools | bash - + - name: Run Go tests with Firebase emulator run: firebase emulators:exec --only firestore 'cd ./apps/question-service; go test -v ./tests' @@ -66,11 +66,11 @@ jobs: run: | cd ./apps/frontend cp .env.example .env - + - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: '22' + node-version: "22" - name: Install pnpm run: npm i -g pnpm @@ -227,17 +227,25 @@ jobs: uses: pnpm/action-setup@v4 with: version: 9.1.4 - + - name: Install dependencies run: | cd ./apps/frontend pnpm i - + - name: Install Chrome WebDriver uses: nanasess/setup-chromedriver@v2 with: chromedriver-version: '130.0.6723.116' - + + - name: Install Edge + uses: browser-actions/setup-edge@v1 + with: + edge-version: stable + + - name: Install Geckodriver + uses: browser-actions/setup-geckodriver@latest + - name: Run Browser Test run: | cd ./apps/frontend diff --git a/apps/docker-compose.yml b/apps/docker-compose.yml index a0565fe1dd..12d02b5661 100644 --- a/apps/docker-compose.yml +++ b/apps/docker-compose.yml @@ -68,7 +68,9 @@ services: - apps_network volumes: - ./history-service:/history-service - + depends_on: + - rabbitmq + signalling-service: build: context: ./signalling-service @@ -95,6 +97,8 @@ services: volumes: - ./execution-service:/execution-service - /var/run/docker.sock:/var/run/docker.sock + depends_on: + - rabbitmq redis: image: redis:latest @@ -104,6 +108,19 @@ services: - 6379:6379 container_name: redis-container + rabbitmq: + image: rabbitmq:3-management + networks: + - apps_network + ports: + - 5672:5672 # Port for RabbitMQ message broker + - 15672:15672 # Port for RabbitMQ Management UI + environment: + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + volumes: + - rabbitmq_data:/var/lib/rabbitmq + python-sandbox: build: context: ./execution-service/execution/python @@ -114,3 +131,8 @@ services: networks: apps_network: + +volumes: + # Mounts a volume for RabbitMQ data persistence. + # This ensures that data is not lost when the container is restarted or removed. + rabbitmq_data: diff --git a/apps/execution-service/.env.example b/apps/execution-service/.env.example index 6a1fb0bd86..4122078fbd 100644 --- a/apps/execution-service/.env.example +++ b/apps/execution-service/.env.example @@ -3,6 +3,8 @@ PORT=8083 # If you are NOT USING docker, use the below variables # HISTORY_SERVICE_URL=http://localhost:8082/ +# RABBITMQ_URL=amqp://guest:guest@localhost:5672/ # If you are USING docker, use the below variables HISTORY_SERVICE_URL=http://history-service:8082/ +RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672/ diff --git a/apps/execution-service/README.md b/apps/execution-service/README.md index 2961ed152e..c53dc3ab9f 100644 --- a/apps/execution-service/README.md +++ b/apps/execution-service/README.md @@ -20,7 +20,32 @@ go run main.go The server will be available at http://localhost:8083. -## Running the Application via Docker +### Setting up message queue with RabbitMQ + +A message queue is used to pass submission results asynchronously from the execution-service to the history-service. + +1. In order to do so, we can run the following command to set up a docker container for RabbitMQ: + +```bash +docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management +``` + +2. Then we can run the execution-service: + +```bash +go run main.go +``` + +3. We can run the history-service by changing our directory and running the same command: + +```bash +cd ../history-service +go run main.go +``` + +To view more details on the RabbitMQ queue, we can go to `localhost:15672`, and login using `guest` as the username and password. + +### Running the Application via Docker To run the application via Docker, run the following command: @@ -74,10 +99,10 @@ The following json format will be returned: ```json [ - { - "input":"hello", - "expected":"olleh" - } + { + "input": "hello", + "expected": "olleh" + } ] ``` @@ -98,16 +123,16 @@ The following json format will be returned: ```json { - "visibleTestResults":[ + "visibleTestResults": [ { - "input":"hello", - "expected":"olleh", - "actual":"olleh", - "passed":true, - "error":"" + "input": "hello", + "expected": "olleh", + "actual": "olleh", + "passed": true, + "error": "" } ], - "customTestResults":null + "customTestResults": null } ``` @@ -127,29 +152,29 @@ The following json format will be returned: ```json { - "visibleTestResults":[ + "visibleTestResults": [ { - "input":"hello", - "expected":"olleh", - "actual":"olleh", - "passed":true, - "error":"" + "input": "hello", + "expected": "olleh", + "actual": "olleh", + "passed": true, + "error": "" } ], - "customTestResults":[ + "customTestResults": [ { - "input":"Hannah", - "expected":"hannaH", - "actual":"hannaH", - "passed":true, - "error":"" + "input": "Hannah", + "expected": "hannaH", + "actual": "hannaH", + "passed": true, + "error": "" }, { - "input":"abcdefg", - "expected":"gfedcba", - "actual":"gfedcba", - "passed":true, - "error":"" + "input": "abcdefg", + "expected": "gfedcba", + "actual": "gfedcba", + "passed": true, + "error": "" } ] } @@ -178,20 +203,20 @@ The following json format will be returned: ```json { - "visibleTestResults":[ + "visibleTestResults": [ { - "input":"hello", - "expected":"olleh", - "actual":"olleh", - "passed":true, - "error":"" + "input": "hello", + "expected": "olleh", + "actual": "olleh", + "passed": true, + "error": "" } ], - "hiddenTestResults":{ - "passed":2, - "total":2 + "hiddenTestResults": { + "passed": 2, + "total": 2 }, - "status":"Accepted" + "status": "Accepted" } ``` @@ -199,19 +224,19 @@ If compilation error exists or any of the tests (visible and hidden) fails, stat ```json { - "visibleTestResults":[ + "visibleTestResults": [ { - "input":"hello", - "expected":"olleh", - "actual":"", - "passed":false, - "error":"Command execution failed: Traceback (most recent call last):\n File \"/tmp/4149249165.py\", line 2, in \u003cmodule\u003e\n prit(name[::-1])\n ^^^^\nNameError: name 'prit' is not defined. Did you mean: 'print'?\n: %!w(*exec.ExitError=\u0026{0x4000364678 []})" + "input": "hello", + "expected": "olleh", + "actual": "", + "passed": false, + "error": "Command execution failed: Traceback (most recent call last):\n File \"/tmp/4149249165.py\", line 2, in \u003cmodule\u003e\n prit(name[::-1])\n ^^^^\nNameError: name 'prit' is not defined. Did you mean: 'print'?\n: %!w(*exec.ExitError=\u0026{0x4000364678 []})" } ], - "hiddenTestResults":{ - "passed":0, - "total":2 + "hiddenTestResults": { + "passed": 0, + "total": 2 }, - "status":"Attempted" + "status": "Attempted" } ``` diff --git a/apps/execution-service/go.mod b/apps/execution-service/go.mod index 32a516cf23..aff484d2a7 100644 --- a/apps/execution-service/go.mod +++ b/apps/execution-service/go.mod @@ -31,6 +31,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect + github.com/rabbitmq/amqp091-go v1.10.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect diff --git a/apps/execution-service/go.sum b/apps/execution-service/go.sum index 7d3c75e147..8a595690fa 100644 --- a/apps/execution-service/go.sum +++ b/apps/execution-service/go.sum @@ -85,6 +85,8 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/apps/execution-service/handlers/submit.go b/apps/execution-service/handlers/submit.go index 0bd06656a3..1411ffcea7 100644 --- a/apps/execution-service/handlers/submit.go +++ b/apps/execution-service/handlers/submit.go @@ -1,16 +1,16 @@ package handlers import ( - "bytes" "encoding/json" "execution-service/constants" + "execution-service/messagequeue" "execution-service/models" "execution-service/utils" "fmt" + "net/http" + "github.com/go-chi/chi/v5" "google.golang.org/api/iterator" - "net/http" - "os" ) func (s *Service) ExecuteVisibleAndHiddenTestsAndSubmit(w http.ResponseWriter, r *http.Request) { @@ -61,7 +61,6 @@ func (s *Service) ExecuteVisibleAndHiddenTestsAndSubmit(w http.ResponseWriter, r } // Save the collaboration history via the history-service - // TODO: convert to message queue submissionReq := models.Submission{ Code: submission.Code, Language: submission.Language, @@ -84,34 +83,12 @@ func (s *Service) ExecuteVisibleAndHiddenTestsAndSubmit(w http.ResponseWriter, r return } - // get history-service url from os env - historyServiceUrl := os.Getenv("HISTORY_SERVICE_URL") - if historyServiceUrl == "" { - http.Error(w, "HISTORY_SERVICE_URL is not set", http.StatusInternalServerError) - return - } - - req, err := http.NewRequest(http.MethodPost, historyServiceUrl+"histories", - bytes.NewBuffer(jsonData)) + err = messagequeue.PublishSubmissionMessage(jsonData) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, fmt.Sprintf("Failed to save submission history: %v", err), http.StatusInternalServerError) return } - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - http.Error(w, "Failed to save submission history", http.StatusInternalServerError) - } - w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(testResults) diff --git a/apps/execution-service/main.go b/apps/execution-service/main.go index 706d0afcd6..e2da2948be 100644 --- a/apps/execution-service/main.go +++ b/apps/execution-service/main.go @@ -3,6 +3,8 @@ package main import ( "context" "execution-service/handlers" + "execution-service/messagequeue" + "execution-service/utils" "fmt" "log" "net/http" @@ -21,20 +23,20 @@ import ( func main() { // Load .env file err := godotenv.Load() - if err != nil { - log.Fatal("Error loading .env file") - } + utils.FailOnError(err, "Error loading .env file") // Initialize Firestore client ctx := context.Background() client, err := initFirestore(ctx) - if err != nil { - log.Fatalf("Failed to initialize Firestore client: %v", err) - } + utils.FailOnError(err, "Failed to initialize Firestore client") defer client.Close() service := &handlers.Service{Client: client} + amqpConnection, amqpChannel := messagequeue.InitRabbitMQServer() + defer amqpConnection.Close() + defer amqpChannel.Close() + r := initChiRouter(service) initRestServer(r) } @@ -107,7 +109,5 @@ func initRestServer(r *chi.Mux) { // Start the server log.Printf("Starting REST server on http://localhost:%s", port) err := http.ListenAndServe(fmt.Sprintf(":%s", port), r) - if err != nil { - log.Fatalf("Failed to start server: %v", err) - } + utils.FailOnError(err, "Failed to start REST server") } diff --git a/apps/execution-service/messagequeue/rabbitmq.go b/apps/execution-service/messagequeue/rabbitmq.go new file mode 100644 index 0000000000..1a6aafcde7 --- /dev/null +++ b/apps/execution-service/messagequeue/rabbitmq.go @@ -0,0 +1,78 @@ +package messagequeue + +import ( + "execution-service/utils" + "fmt" + "log" + "os" + "time" + + amqp "github.com/rabbitmq/amqp091-go" +) + +const ( + CODE_SUBMISSION_QUEUE_KEY = "code-submission" + NUM_RETRIES = 10 +) + +var ( + codeSubmissionQueue amqp.Queue + rabbitMQChannel *amqp.Channel +) + +func InitRabbitMQServer() (*amqp.Connection, *amqp.Channel) { + conn := connectToRabbitMQ() + + // Create a channel + ch, err := conn.Channel() + utils.FailOnError(err, "Failed to open a channel") + rabbitMQChannel = ch + + // Declare a queue + q, err := ch.QueueDeclare( + CODE_SUBMISSION_QUEUE_KEY, // name + false, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + utils.FailOnError(err, "Failed to declare a queue") + codeSubmissionQueue = q + + return conn, ch +} + +func connectToRabbitMQ() *amqp.Connection { + var conn *amqp.Connection + var err error + rabbitMQURL := os.Getenv("RABBITMQ_URL") + for i := 0; i < NUM_RETRIES; i++ { // Retry up to 10 times + conn, err = amqp.Dial(rabbitMQURL) + if err == nil { + log.Println("Connected to RabbitMQ") + return conn + } + log.Printf("Failed to connect to RabbitMQ, retrying in 5 seconds... (Attempt %d/%d)", i+1, NUM_RETRIES) + time.Sleep(5 * time.Second) + } + utils.FailOnError(err, fmt.Sprintf("Failed to connect to RabbitMQ after %d attempts", NUM_RETRIES)) + return nil +} + +func PublishSubmissionMessage(submission []byte) error { + err := rabbitMQChannel.Publish( + "", // exchange + codeSubmissionQueue.Name, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "application/json", + Body: submission, + }) + if err != nil { + return fmt.Errorf("Failed to publish a message: %v", err) + } + log.Printf("RabbitMQ: [x] Sent %s", submission) + return nil +} diff --git a/apps/execution-service/utils/log.go b/apps/execution-service/utils/log.go new file mode 100644 index 0000000000..a77b2c29ca --- /dev/null +++ b/apps/execution-service/utils/log.go @@ -0,0 +1,9 @@ +package utils + +import "log" + +func FailOnError(err error, msg string) { + if err != nil { + log.Fatalf("%s: %s", msg, err) + } +} diff --git a/apps/frontend/__tests__/browser-tests/browser.test.ts b/apps/frontend/__tests__/browser-tests/browser.test.ts index 22257250c3..20b49ba1bd 100644 --- a/apps/frontend/__tests__/browser-tests/browser.test.ts +++ b/apps/frontend/__tests__/browser-tests/browser.test.ts @@ -1,37 +1,61 @@ -import { Actions, Browser, Builder, By, Key, until, WebDriver } from "selenium-webdriver" +import { Actions, Browser, Builder, By, Capabilities, Key, until, WebDriver } from "selenium-webdriver" + +import {Options as ChromeOptions} from "selenium-webdriver/chrome" +import {Options as EdgeOptions} from "selenium-webdriver/edge" +import {Options as FirefoxOptions} from "selenium-webdriver/firefox" -import Chrome from "selenium-webdriver/chrome" const URL = 'http://localhost:3000/'; const ETERNAL_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTk5fQ.Z4_FVGQ5lIcouP3m4YLMr6pGMF17IJFfo2yOTiN58DY" -describe("chrome browser", () => { - const options = new Chrome.Options() - .addArguments("--headless=new") as Chrome.Options; // uncomment locally to see the steps in action - const builder = new Builder().forBrowser(Browser.CHROME).setChromeOptions(options); +const CHROME_OPTIONS = new ChromeOptions() + .addArguments("--headless=new") as ChromeOptions; // uncomment locally to see the steps in action +const EDGE_OPTIONS = new EdgeOptions() + .setBinaryPath("/opt/hostedtoolcache/msedge/stable/x64/msedge") // need to point to the correct path + .addArguments("--headless=new") as EdgeOptions; + +const FIREFOX_OPTIONS = new FirefoxOptions() + .addArguments("--headless") as FirefoxOptions; + +const builder = new Builder() + .setChromeOptions(CHROME_OPTIONS) + .setEdgeOptions(EDGE_OPTIONS) + .setFirefoxOptions(FIREFOX_OPTIONS) + +describe.each([Browser.CHROME, Browser.EDGE, Browser.FIREFOX])("%s driver test", (browser) => { let driver: WebDriver; + beforeAll(() => { + const cap = new Capabilities().setBrowserName(browser) + builder.withCapabilities(cap); + }) beforeEach(async () => { + console.log(browser + ": building..."); driver = await builder.build(); + console.log(browser + ": built"); }) afterEach(async () => { await driver.quit(); }) - describe("chrome webdriver installed correctly", () => { + describe("webdriver installed correctly", () => { it("does google search", async () => { + console.log("getting url"); await driver.get('http://www.google.com'); + console.log("got url"); await driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN); await driver.wait(until.titleIs('webdriver - Google Search'), 1000); + console.log("did search"); }, 10000); - it("does another google search", async () => { + + it.skip("does another google search", async () => { await driver.get('http://www.google.com'); await driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN); await driver.wait(until.titleIs('webdriver - Google Search'), 1000); }, 10000); }); - describe("browser-test", () => { + describe.skip("browser-test", () => { it("accesses and login to peerprep", async () => { await driver.get(URL); await driver.wait(until.urlIs(`${URL}login`)); diff --git a/apps/frontend/src/app/collaboration/[id]/page.tsx b/apps/frontend/src/app/collaboration/[id]/page.tsx index 6ae67427ef..739eac08a6 100644 --- a/apps/frontend/src/app/collaboration/[id]/page.tsx +++ b/apps/frontend/src/app/collaboration/[id]/page.tsx @@ -34,9 +34,12 @@ import { ExecuteVisibleAndHiddenTestsAndSubmit, ExecutionResults, GetVisibleTests, + isTestResult, SubmissionHiddenTestResultsAndStatus, SubmissionResults, Test, + TestData, + TestResult, } from "@/app/services/execute"; import { QuestionDetailFull } from "@/components/question/QuestionDetailFull/QuestionDetailFull"; import VideoPanel from "@/components/VideoPanel/VideoPanel"; @@ -75,15 +78,17 @@ export default function CollaborationPage(props: CollaborationProps) { ); const [currentUser, setCurrentUser] = useState(undefined); const [matchedUser, setMatchedUser] = useState("Loading..."); - const [sessionDuration, setSessionDuration] = useState(() => { - const storedTime = localStorage.getItem("session-duration"); - return storedTime ? parseInt(storedTime) : 0; - }); // State for count-up timer (TODO: currently using localstorage to store time, change to db stored time in the future) + const [sessionDuration, setSessionDuration] = useState(0); // State for count-up timer (TODO: currently using localstorage to store time, change to db stored time in the future) const stopwatchRef = useRef(null); const [matchedTopics, setMatchedTopics] = useState( undefined ); + useEffect(() => { + const storedTime = localStorage.getItem("session-duration"); + setSessionDuration(storedTime ? parseInt(storedTime) : 0); + }, []); + // Chat states const [messageToSend, setMessageToSend] = useState( undefined @@ -314,6 +319,52 @@ export default function CollaborationPage(props: CollaborationProps) { } }, [isSessionEndModalOpen, countDown]); + // Tabs component items for visibleTestCases + var items: TabsProps["items"] = visibleTestCases.map((item, index) => { + return { + key: index.toString(), + label: ( + + Case {index + 1} + + ), + children: ( +
+ + {isTestResult(item) && ( +
+ + + {item.passed ? "Passed" : "Failed"} + +
+ Actual Output:{" "} + {item.actual} +
+ {item.error && ( + <> + Error: +
{item.error}
+ + )} +
+ )} +
+ ), + }; + }); + // Handles the cleaning of localstorage variables, stopping the timer & signalling collab user on webrtc // type: "initiator" | "peer" const handleCloseCollaboration = (type: string) => { diff --git a/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx b/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx index a7c8051bcf..d32f67612d 100644 --- a/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx +++ b/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx @@ -15,8 +15,8 @@ import * as Y from "yjs"; import { yCollab } from "y-codemirror.next"; import { WebrtcProvider } from "y-webrtc"; import { EditorView, basicSetup } from "codemirror"; -import { keymap } from "@codemirror/view" -import { indentWithTab } from "@codemirror/commands" +import { keymap } from "@codemirror/view"; +import { indentWithTab } from "@codemirror/commands"; import { EditorState, Compartment } from "@codemirror/state"; import { javascript, javascriptLanguage } from "@codemirror/lang-javascript"; import { python, pythonLanguage } from "@codemirror/lang-python"; @@ -68,15 +68,15 @@ interface Awareness { executionResultsState: { executionResults: ExecutionResults; id: number; - } + }; executingState: { executing: boolean; id: number; - } + }; submittingState: { submitting: boolean; id: number; - } + }; } export const usercolors = [ @@ -111,8 +111,7 @@ const CollaborativeEditor = forwardRef( props.onCodeChange(update.state.doc.toString()); } }); - - + // Referenced: https://codemirror.net/examples/config/#dynamic-configuration // const autoLanguage = EditorState.transactionExtender.of((tr) => { // if (!tr.docChanged) return null; @@ -196,10 +195,10 @@ const CollaborativeEditor = forwardRef( }); }; - let latestExecutionId: number = (new Date(0)).getTime(); - let latestSubmissionId: number = (new Date(0)).getTime(); - let latestExecutingId: number = (new Date(0)).getTime(); - let latestSubmittingId: number = (new Date(0)).getTime(); + let latestExecutionId: number = new Date(0).getTime(); + let latestSubmissionId: number = new Date(0).getTime(); + let latestExecutingId: number = new Date(0).getTime(); + let latestSubmittingId: number = new Date(0).getTime(); useImperativeHandle(ref, () => ({ endSession: () => { @@ -311,12 +310,14 @@ const CollaborativeEditor = forwardRef( .get(clientID) as Awareness; if ( - state && + state && state.submissionResultsState && state.submissionResultsState.id !== latestSubmissionId ) { latestSubmissionId = state.submissionResultsState.id; - props.updateSubmissionResults(state.submissionResultsState.submissionResults); + props.updateSubmissionResults( + state.submissionResultsState.submissionResults + ); messageApi.open({ type: "success", content: `${ @@ -326,12 +327,14 @@ const CollaborativeEditor = forwardRef( } if ( - state && - state.executionResultsState && + state && + state.executionResultsState && state.executionResultsState.id !== latestExecutionId ) { latestExecutionId = state.executionResultsState.id; - props.updateExecutionResults(state.executionResultsState.executionResults); + props.updateExecutionResults( + state.executionResultsState.executionResults + ); messageApi.open({ type: "success", content: `${ @@ -341,8 +344,8 @@ const CollaborativeEditor = forwardRef( } if ( - state && - state.executingState && + state && + state.executingState && state.executingState.id !== latestExecutingId ) { latestExecutingId = state.executingState.id; @@ -358,8 +361,8 @@ const CollaborativeEditor = forwardRef( } if ( - state && - state.submittingState && + state && + state.submittingState && state.submittingState.id !== latestSubmittingId ) { latestSubmittingId = state.submittingState.id; @@ -367,9 +370,7 @@ const CollaborativeEditor = forwardRef( if (state.submittingState.submitting) { messageApi.open({ type: "info", - content: `${ - props.matchedUser ?? "Peer" - } is saving code...`, + content: `${props.matchedUser ?? "Peer"} is saving code...`, }); } } diff --git a/apps/frontend/src/components/VideoPanel/VideoPanel.tsx b/apps/frontend/src/components/VideoPanel/VideoPanel.tsx index 36fdd948e2..2db62642e0 100644 --- a/apps/frontend/src/components/VideoPanel/VideoPanel.tsx +++ b/apps/frontend/src/components/VideoPanel/VideoPanel.tsx @@ -11,11 +11,8 @@ import { } from "@ant-design/icons"; const VideoPanel = () => { - const matchId = localStorage.getItem("collabId")?.toString() ?? ""; - const currentUsername = localStorage.getItem("user")?.toString(); - const matchedUsername = localStorage.getItem("matchedUser")?.toString(); - const currentId = currentUsername + "-" + matchId ?? ""; - const partnerId = matchedUsername + "-" + matchId ?? ""; + const [currentId, setCurrentId] = useState(); + const [partnerId, setPartnerId] = useState(); const remoteVideoRef = useRef(null); const currentUserVideoRef = useRef(null); @@ -30,6 +27,15 @@ const VideoPanel = () => { const [muteOn, setMuteOn] = useState(false); const [isCalling, setIsCalling] = useState(false); + useEffect(() => { + const matchId = localStorage.getItem("collabId")?.toString() ?? ""; + const currentUsername = localStorage.getItem("user")?.toString(); + const matchedUsername = localStorage.getItem("matchedUser")?.toString(); + + setCurrentId(currentUsername + "-" + (matchId ?? "")); + setPartnerId(matchedUsername + "-" + (matchId ?? "")); + }, []); + const handleCall = () => { navigator.mediaDevices .getUserMedia({ @@ -37,7 +43,7 @@ const VideoPanel = () => { audio: true, }) .then((stream) => { - if (peerInstance) { + if (peerInstance && partnerId) { const call = peerInstance?.call(partnerId, stream); setCallInstance(call); setIsCalling(true); // Set isCalling as true since it is the initiator @@ -120,7 +126,7 @@ const VideoPanel = () => { } }; } - }, []); + }, [currentId]); // When remote peer initiates end call, we set isCalling to false useEffect(() => { diff --git a/apps/history-service/.env.example b/apps/history-service/.env.example index b3bd4db2b4..0f3f18c101 100644 --- a/apps/history-service/.env.example +++ b/apps/history-service/.env.example @@ -1,2 +1,8 @@ FIREBASE_CREDENTIAL_PATH=cs3219-staging-codehisto-bb61c-firebase-adminsdk-egopb-95cfaf9b87.json -PORT=8082 \ No newline at end of file +PORT=8082 + +# If you are NOT USING docker, use the below variables +# RABBITMQ_URL=amqp://guest:guest@localhost:5672/ + +# If you are USING docker, use the below variables +RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672/ \ No newline at end of file diff --git a/apps/history-service/README.md b/apps/history-service/README.md index 681c629bcd..6fec9e5bbd 100644 --- a/apps/history-service/README.md +++ b/apps/history-service/README.md @@ -47,9 +47,34 @@ To start the server, run the following command: go run main.go ``` +### Setting up message queue with RabbitMQ + +A message queue is used to pass submission results asynchronously from the execution-service to the history-service. + +1. In order to do so, we can run the following command to set up a docker container for RabbitMQ: + +```bash +docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management +``` + +2. Then we can run the history-service: + +```bash +go run main.go +``` + +3. We can run the execution-service by changing our directory and running the same command: + +```bash +cd ../execution-service +go run main.go +``` + +To view more details on the RabbitMQ queue, we can go to `localhost:15672`, and login using `guest` as the username and password. + The server will be available at http://localhost:8082. -## Running the Application via Docker +### Running the Application via Docker To run the application via Docker, run the following command: diff --git a/apps/history-service/databases/history.go b/apps/history-service/databases/history.go new file mode 100644 index 0000000000..e9b41e4b14 --- /dev/null +++ b/apps/history-service/databases/history.go @@ -0,0 +1,32 @@ +package databases + +import ( + "context" + "history-service/models" + + "cloud.google.com/go/firestore" +) + +func CreateHistory(client *firestore.Client, ctx context.Context, submissionHistory models.SubmissionHistory) (*firestore.DocumentRef, error) { + // Document reference ID in firestore mapped to the match ID in model + collection := client.Collection("collaboration-history") + + docRef, _, err := collection.Add(ctx, map[string]interface{}{ + "title": submissionHistory.Title, + "code": submissionHistory.Code, + "language": submissionHistory.Language, + "user": submissionHistory.User, + "matchedUser": submissionHistory.MatchedUser, + "matchedTopics": submissionHistory.MatchedTopics, + "questionDocRefId": submissionHistory.QuestionDocRefID, + "questionDifficulty": submissionHistory.QuestionDifficulty, + "questionTopics": submissionHistory.QuestionTopics, + "status": submissionHistory.Status, + "createdAt": firestore.ServerTimestamp, + "updatedAt": firestore.ServerTimestamp, + }) + if err != nil { + return nil, err + } + return docRef, nil +} diff --git a/apps/history-service/go.mod b/apps/history-service/go.mod index 37d6005bf1..2bdb4f0c8a 100644 --- a/apps/history-service/go.mod +++ b/apps/history-service/go.mod @@ -31,6 +31,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect + github.com/rabbitmq/amqp091-go v1.10.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect diff --git a/apps/history-service/go.sum b/apps/history-service/go.sum index ce7234e8f6..fbc00bcfaa 100644 --- a/apps/history-service/go.sum +++ b/apps/history-service/go.sum @@ -85,6 +85,8 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/apps/history-service/handlers/create.go b/apps/history-service/handlers/create.go index 3dc635d0dd..e4868730b7 100644 --- a/apps/history-service/handlers/create.go +++ b/apps/history-service/handlers/create.go @@ -2,11 +2,11 @@ package handlers import ( "encoding/json" + "history-service/databases" "history-service/models" "history-service/utils" "net/http" - "cloud.google.com/go/firestore" "google.golang.org/api/iterator" ) @@ -22,22 +22,7 @@ func (s *Service) CreateHistory(w http.ResponseWriter, r *http.Request) { } // Document reference ID in firestore mapped to the match ID in model - collection := s.Client.Collection("collaboration-history") - - docRef, _, err := collection.Add(ctx, map[string]interface{}{ - "title": submissionHistory.Title, - "code": submissionHistory.Code, - "language": submissionHistory.Language, - "user": submissionHistory.User, - "matchedUser": submissionHistory.MatchedUser, - "matchedTopics": submissionHistory.MatchedTopics, - "questionDocRefId": submissionHistory.QuestionDocRefID, - "questionDifficulty": submissionHistory.QuestionDifficulty, - "questionTopics": submissionHistory.QuestionTopics, - "status": submissionHistory.Status, - "createdAt": firestore.ServerTimestamp, - "updatedAt": firestore.ServerTimestamp, - }) + docRef, err := databases.CreateHistory(s.Client, ctx, submissionHistory) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/apps/history-service/main.go b/apps/history-service/main.go index 751f2ed525..1b79073caf 100644 --- a/apps/history-service/main.go +++ b/apps/history-service/main.go @@ -3,7 +3,10 @@ package main import ( "context" "fmt" + "history-service/databases" "history-service/handlers" + "history-service/messagequeue" + "history-service/utils" "log" "net/http" "os" @@ -20,20 +23,21 @@ import ( func main() { err := godotenv.Load() - if err != nil { - log.Fatal("Error loading .env file") - } + utils.FailOnError(err, "Error loading .env file") // Initialize Firestore client ctx := context.Background() client, err := initFirestore(ctx) - if err != nil { - log.Fatalf("Failed to initialize Firestore client: %v", err) - } + utils.FailOnError(err, "Failed to initialize Firestore client") defer client.Close() service := &handlers.Service{Client: client} + amqpConnection, amqpChannel := messagequeue.InitRabbitMQServer() + defer amqpConnection.Close() + defer amqpChannel.Close() + go messagequeue.ConsumeSubmissionMessages(client, databases.CreateHistory) + r := initChiRouter(service) initRestServer(r) } diff --git a/apps/history-service/messagequeue/rabbitmq.go b/apps/history-service/messagequeue/rabbitmq.go new file mode 100644 index 0000000000..f945a866d0 --- /dev/null +++ b/apps/history-service/messagequeue/rabbitmq.go @@ -0,0 +1,108 @@ +package messagequeue + +import ( + "context" + "encoding/json" + "fmt" + "history-service/models" + "history-service/utils" + "log" + "os" + "time" + + "cloud.google.com/go/firestore" + amqp "github.com/rabbitmq/amqp091-go" +) + +const ( + CODE_SUBMISSION_QUEUE_KEY = "code-submission" + NUM_RETRIES = 10 +) + +var ( + codeSubmissionQueue amqp.Queue + rabbitMQChannel *amqp.Channel +) + +func InitRabbitMQServer() (*amqp.Connection, *amqp.Channel) { + conn := connectToRabbitMQ() + + // Create a channel + ch, err := conn.Channel() + utils.FailOnError(err, "Failed to open a channel") + rabbitMQChannel = ch + + // Declare a queue + q, err := ch.QueueDeclare( + "code-submission", // name + false, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + utils.FailOnError(err, "Failed to declare a queue") + codeSubmissionQueue = q + + return conn, ch +} + +func connectToRabbitMQ() *amqp.Connection { + var conn *amqp.Connection + var err error + rabbitMQURL := os.Getenv("RABBITMQ_URL") + for i := 0; i < NUM_RETRIES; i++ { // Retry up to 10 times + conn, err = amqp.Dial(rabbitMQURL) + if err == nil { + log.Println("Connected to RabbitMQ") + return conn + } + log.Printf("Failed to connect to RabbitMQ, retrying in 5 seconds... (Attempt %d/%d)", i+1, NUM_RETRIES) + time.Sleep(5 * time.Second) + } + utils.FailOnError(err, fmt.Sprintf("Failed to connect to RabbitMQ after %d attempts", NUM_RETRIES)) + return nil +} + +func ConsumeSubmissionMessages(client *firestore.Client, createSubmission func( + *firestore.Client, context.Context, models.SubmissionHistory) ( + *firestore.DocumentRef, error)) { + ctx := context.Background() + + // Consume messages from the queue + msgs, err := rabbitMQChannel.Consume( + codeSubmissionQueue.Name, // queue + "", // consumer + true, // auto-ack + false, // exclusive + false, // no-local + false, // no-wait + nil, // args + ) + utils.FailOnError(err, "RabbitMQ: Failed to register a consumer") + + // Create a channel to block indefinitely + forever := make(chan bool) + + // Start a goroutine to handle incoming messages + go func() { + for d := range msgs { + log.Printf("RabbitMQ: Received a message") + + // Parse request + var submissionHistory models.SubmissionHistory + if err := json.Unmarshal(d.Body, &submissionHistory); err != nil { + log.Printf("RabbitMQ: Error decoding JSON: %v", err) + continue + } + + _, err := createSubmission(client, ctx, submissionHistory) + if err != nil { + log.Printf("RabbitMQ: %v", err) + } + } + }() + + log.Printf("RabbitMQ: [*] Waiting for messages.") + <-forever +} diff --git a/apps/history-service/utils/log.go b/apps/history-service/utils/log.go new file mode 100644 index 0000000000..a77b2c29ca --- /dev/null +++ b/apps/history-service/utils/log.go @@ -0,0 +1,9 @@ +package utils + +import "log" + +func FailOnError(err error, msg string) { + if err != nil { + log.Fatalf("%s: %s", msg, err) + } +}