From b82dfdbe17668a93920c36f76790728cc954ecfb Mon Sep 17 00:00:00 2001 From: Ninjaws Date: Sun, 3 Nov 2024 13:29:52 +0100 Subject: [PATCH] [Updated] Frontend now uses categories retrieved from the backend --- README.md | 12 +++++- Reflection.md | 8 ++++ .../quiz/controllers/QuizController.java | 10 +++++ .../ninjaws/quiz/services/DataService.java | 3 ++ .../ninjaws/quiz/services/QuizService.java | 32 ++++++++------- .../quiz/scala/QuestionStatusSimulation.scala | 1 + frontend/src/app/Quiz/models/category.ts | 4 ++ frontend/src/app/Quiz/models/quiz-params.ts | 4 +- .../src/app/Quiz/services/quiz.service.ts | 39 ++++--------------- .../src/app/Quiz/setup/setup.component.html | 2 +- .../src/app/Quiz/setup/setup.component.ts | 16 +++++--- scripts/LoadTestMultiple.sh | 7 ++-- 12 files changed, 80 insertions(+), 58 deletions(-) create mode 100644 frontend/src/app/Quiz/models/category.ts diff --git a/README.md b/README.md index 4f51dba..e3c838c 100644 --- a/README.md +++ b/README.md @@ -35,4 +35,14 @@ sudo reboot ```bash docker compose up ``` -- Visit localhost:4200 to play the quiz! \ No newline at end of file +- Visit localhost:4200 to play the quiz! + + +## Benchmarking +- Go to the /scripts folder and run the LoadTestMultiple.sh script +- Inside you can change how many sessions should be created (set to 6 by default) + +## Notes +- If the data is not present, it will try to query it from the external API, which takes significantly longer (only 1 request per 5 seconds) +- When running it locally, give the backend a few minutes to gather data in bulk (should be completely done in ~7 minutes) +- It refreshes this data every night in the background, so it stays up to date \ No newline at end of file diff --git a/Reflection.md b/Reflection.md index 88450d4..2e902ef 100644 --- a/Reflection.md +++ b/Reflection.md @@ -28,3 +28,11 @@ Then start replacing the cache items one at a time. Users shouldn't feel this at all, because the active sessions are stored in their own cache items +# Progress so far +There only seem to be ~4100 items, making the collecting phase only last around 7 minutes. + +Swapping dbs is replaced with swapping tables, seems to work just as fine, yet much simpler to set up + +TODO: Add a caching state, and start filling up each unique combination with the items from the db +This should lead to performance gains + diff --git a/backend/src/main/java/com/ninjaws/quiz/controllers/QuizController.java b/backend/src/main/java/com/ninjaws/quiz/controllers/QuizController.java index fca6518..7d7d114 100644 --- a/backend/src/main/java/com/ninjaws/quiz/controllers/QuizController.java +++ b/backend/src/main/java/com/ninjaws/quiz/controllers/QuizController.java @@ -1,5 +1,6 @@ package com.ninjaws.quiz.controllers; +import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; @@ -11,9 +12,11 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.ninjaws.quiz.entities.Category; import com.ninjaws.quiz.models.Answers; import com.ninjaws.quiz.models.QuizSettings; import com.ninjaws.quiz.models.Session; +import com.ninjaws.quiz.services.DataService; import com.ninjaws.quiz.services.QuizService; @RestController @@ -22,6 +25,8 @@ public class QuizController { @Autowired private QuizService quizService; + @Autowired + private DataService dataService; /** @@ -69,4 +74,9 @@ public class QuizController { String jsonString = String.format("{\"score\":\"%s\"}", quizService.getScore(answers)); return jsonString; } + + @GetMapping("/categories") + public List getCategories(){ + return dataService.getCategories(); + } } diff --git a/backend/src/main/java/com/ninjaws/quiz/services/DataService.java b/backend/src/main/java/com/ninjaws/quiz/services/DataService.java index de2f114..717fdc8 100644 --- a/backend/src/main/java/com/ninjaws/quiz/services/DataService.java +++ b/backend/src/main/java/com/ninjaws/quiz/services/DataService.java @@ -50,6 +50,9 @@ public class DataService { return categoryRepository.saveAll(categories); } + public List getCategories(){ + return categoryRepository.findAll(); + } /** * Moving the collected questions to the questions list * diff --git a/backend/src/main/java/com/ninjaws/quiz/services/QuizService.java b/backend/src/main/java/com/ninjaws/quiz/services/QuizService.java index 5bb92b3..4bb1484 100644 --- a/backend/src/main/java/com/ninjaws/quiz/services/QuizService.java +++ b/backend/src/main/java/com/ninjaws/quiz/services/QuizService.java @@ -24,6 +24,8 @@ public class QuizService { private DataService dataService; @Autowired private CleanerService cleanerService; + @Autowired + private StateManagementService stateManager; /** * Actions: @@ -38,19 +40,20 @@ public class QuizService { /** First, we check if there are already enough items in our db */ List questionsRetrieved = dataService.lookupQuestions(quizSettings); + /** If the items are not matching the required amount, and the db is not currently being filled up, then they simply don't exist */ + if (questionsRetrieved.size() < quizSettings.getAmount() && stateManager.getCurrentProcessingState() != StateManagementService.ProcessingState.STARTUP) { + session.setStatusCode(1); + cacheService.addToCache(session.getId(), session); + return Optional.of(session.getId()); + } + /** All items are collected in from the database, it doesn't need to be added to the queue */ if(!questionsRetrieved.isEmpty() && questionsRetrieved.size() == quizSettings.getAmount()) { session.setQuestions(cleanerService.questionListEntitytoDTO(questionsRetrieved)); - } - - cacheService.addToCache(session.getId(), session); - - /** - * If the questions were retrieved from the DB, then it doesn't need to be added to the queue - */ - if(!questionsRetrieved.isEmpty() && questionsRetrieved.size() == quizSettings.getAmount()) { + cacheService.addToCache(session.getId(), session); return Optional.of(session.getId()); } + cacheService.addToCache(session.getId(), session); boolean addedToQueue = queueService.addToQueue(new Request(session.getId(), quizSettings)); /** In this order, because doing the queue before the cache could lead to race conditions (request completion immediately calls cache, but then the item doesn't exist) */ if(!addedToQueue) { @@ -75,16 +78,17 @@ public class QuizService { throw new IllegalArgumentException("Invalid session ID: " + sessionId); } + /** Anything other than statuscode 0 doesn't result in active sessions, so remove them from the cache after reporting to the frontend */ + if(session.getStatusCode() != 0) { + cacheService.removeFromCache(session.getId()); + return Optional.of(session); + } + /** Still waiting for a response */ if (session.getQuestions() == null) { return Optional.empty(); } - /** Anything other than statuscode 0 doesn't result in active sessions, so remove them from the cache after reporting to the frontend */ - // if(session.getStatusCode() != 0) { - /** Regardless of status code, this link is not reusable */ - cacheService.removeFromCache(session.getId()); - // } return Optional.of(session); } @@ -93,7 +97,7 @@ public class QuizService { * Receives: sessionId and answers * * Actions: - * - retrieves session from cache + * - Retrieves session from cache * - Deletes session from cache * - Matches answers answers to correctAnswers * - Calculates score diff --git a/backend/src/test/java/com/ninjaws/quiz/scala/QuestionStatusSimulation.scala b/backend/src/test/java/com/ninjaws/quiz/scala/QuestionStatusSimulation.scala index 9f75a56..13900dc 100644 --- a/backend/src/test/java/com/ninjaws/quiz/scala/QuestionStatusSimulation.scala +++ b/backend/src/test/java/com/ninjaws/quiz/scala/QuestionStatusSimulation.scala @@ -3,6 +3,7 @@ import io.gatling.core.Predef._ import io.gatling.http.Predef._ import scala.concurrent.duration._ +/** Does not work, use the LoadTestMultiple.sh in the /scripts folder in the root instead */ class QuestionStatusSimulation extends Simulation { val httpProtocol = http.baseUrl("http://localhost:9090").acceptHeader("application/json") diff --git a/frontend/src/app/Quiz/models/category.ts b/frontend/src/app/Quiz/models/category.ts new file mode 100644 index 0000000..dae9760 --- /dev/null +++ b/frontend/src/app/Quiz/models/category.ts @@ -0,0 +1,4 @@ +export interface Category { + id: number; + name: string; +} \ No newline at end of file diff --git a/frontend/src/app/Quiz/models/quiz-params.ts b/frontend/src/app/Quiz/models/quiz-params.ts index b37a2b2..7b8fff8 100644 --- a/frontend/src/app/Quiz/models/quiz-params.ts +++ b/frontend/src/app/Quiz/models/quiz-params.ts @@ -1,5 +1,7 @@ +import { Category } from "./category"; + export interface QuizParams { - category: string; + category: number; type: string; amount: number; difficulty: string; diff --git a/frontend/src/app/Quiz/services/quiz.service.ts b/frontend/src/app/Quiz/services/quiz.service.ts index 184570f..e9c141a 100644 --- a/frontend/src/app/Quiz/services/quiz.service.ts +++ b/frontend/src/app/Quiz/services/quiz.service.ts @@ -19,6 +19,7 @@ import { StorageService } from './storage.service'; import { CleanerService } from './cleaner.service'; import { ResponseHandlerService } from './response-handler.service'; import { environment } from '../../../environments/environment'; +import { Category } from '../models/category'; @Injectable({ providedIn: 'root', @@ -40,8 +41,7 @@ export class QuizService { } this.storageService.setSession(sessionIdObj.sessionId); const sessionObj = await lastValueFrom(this.pollQuestions(sessionIdObj.sessionId)); - - return this.reponseHandler.handleResponse(sessionIdObj.statusCode, sessionObj); + return this.reponseHandler.handleResponse(sessionObj.statusCode, sessionObj); } public pollQuestions(sessionId: string) { @@ -67,7 +67,7 @@ export class QuizService { private convertParamsToUrl(quizParams: QuizParams) { let url = `?amount=${quizParams.amount}`; - const catIndex = this.getCategories().indexOf(quizParams.category); + const catIndex = quizParams.category; url += catIndex !== 0 ? `&category=${catIndex}` : ''; const diff = quizParams.difficulty.toLowerCase(); @@ -83,34 +83,11 @@ export class QuizService { return url; } - public getCategories(): string[] { - return [ - 'Any Category', - 'General Knowledge', - 'Entertainment: Books', - 'Entertainment: Film', - 'Entertainment: Music', - 'Entertainment: Musicals & Theatres', - 'Entertainment: Television', - 'Entertainment: Video Games', - 'Entertainment: Board Games', - 'Science & Nature', - 'Science: Computers', - 'Science: Mathematics', - 'Mythology', - 'Sports', - 'Geography', - 'History', - 'Politics', - 'Art', - 'Celebrities', - 'Animals', - 'Vehicles', - 'Entertainment: Comics', - 'Science: Gadgets', - 'Entertainment: Japanese Anime & Manga', - 'Entertainment: Cartoon & Animations', - ]; + public async getCategories(): Promise { + let categories: Category[] = await firstValueFrom(this.http.get(this.apiUrl + '/categories')); + categories.push({id:0,name:"Any Category"}); + categories.sort((a,b) => a.id - b.id); + return categories; } public getDifficulties(): string[] { diff --git a/frontend/src/app/Quiz/setup/setup.component.html b/frontend/src/app/Quiz/setup/setup.component.html index 9cc3b1d..26d99e3 100644 --- a/frontend/src/app/Quiz/setup/setup.component.html +++ b/frontend/src/app/Quiz/setup/setup.component.html @@ -5,7 +5,7 @@
diff --git a/frontend/src/app/Quiz/setup/setup.component.ts b/frontend/src/app/Quiz/setup/setup.component.ts index 34c9056..bf28d90 100644 --- a/frontend/src/app/Quiz/setup/setup.component.ts +++ b/frontend/src/app/Quiz/setup/setup.component.ts @@ -7,6 +7,7 @@ import { QuizParams } from '../models/quiz-params'; import { SpinnerService } from '../../Spinner/services/spinner.service'; import { CommonModule } from '@angular/common'; import { Question } from '../models/question'; +import { Category } from '../models/category'; @Component({ selector: 'app-setup', @@ -17,8 +18,8 @@ import { Question } from '../models/question'; }) export class SetupComponent { - - category: string = ""; + categories!: Category[]; + category!: number; difficulty: string = ""; type: string = ""; amount: number = 10; @@ -28,10 +29,13 @@ export class SetupComponent { public quizService: QuizService, private spinnerService: SpinnerService ) { - /** Setting default values */ - this.category = this.quizService.getCategories()[0]; - this.difficulty = this.quizService.getDifficulties()[0]; - this.type = this.quizService.getTypes()[0]; + quizService.getCategories().then((categories: Category[]) => { + /** Setting default values */ + this.categories = categories; + this.category = this.categories.find(item => item.id == 0)!.id; + this.difficulty = this.quizService.getDifficulties()[0]; + this.type = this.quizService.getTypes()[0]; + }); } startQuiz() { diff --git a/scripts/LoadTestMultiple.sh b/scripts/LoadTestMultiple.sh index 1bc9f85..0692369 100755 --- a/scripts/LoadTestMultiple.sh +++ b/scripts/LoadTestMultiple.sh @@ -2,16 +2,15 @@ start_time=$(date +%s) -for i in {1..1000}; do +for i in {1..6}; do ./LoadTestOne-Silent.sh & # sleep 0.1 done -wait # Wait for all background jobs to finish +wait -end_time=$(date +%s) # Capture the end time +end_time=$(date +%s) -# Calculate and print the elapsed time elapsed_time=$((end_time - start_time)) echo "Done!" echo "Total time taken: $elapsed_time seconds"