Author SHA1 Message Date
Ninjaws e1b04745b2 [Updated] Readme 2024-11-03 13:39:50 +01:00
Ninjaws b82dfdbe17 [Updated] Frontend now uses categories retrieved from the backend 2024-11-03 13:29:52 +01:00
15 changed files with 81 additions and 58 deletions
+12 -1
View File
@@ -35,4 +35,15 @@ sudo reboot
```bash ```bash
docker compose up docker compose up
``` ```
- Visit localhost:4200 to play the quiz! - 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)
- In the /scripts/benchmarks folder you can find some benchmarks I've done
## 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
+8
View File
@@ -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 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
@@ -1,5 +1,6 @@
package com.ninjaws.quiz.controllers; package com.ninjaws.quiz.controllers;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired; 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.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.ninjaws.quiz.entities.Category;
import com.ninjaws.quiz.models.Answers; import com.ninjaws.quiz.models.Answers;
import com.ninjaws.quiz.models.QuizSettings; import com.ninjaws.quiz.models.QuizSettings;
import com.ninjaws.quiz.models.Session; import com.ninjaws.quiz.models.Session;
import com.ninjaws.quiz.services.DataService;
import com.ninjaws.quiz.services.QuizService; import com.ninjaws.quiz.services.QuizService;
@RestController @RestController
@@ -22,6 +25,8 @@ public class QuizController {
@Autowired @Autowired
private QuizService quizService; private QuizService quizService;
@Autowired
private DataService dataService;
/** /**
@@ -69,4 +74,9 @@ public class QuizController {
String jsonString = String.format("{\"score\":\"%s\"}", quizService.getScore(answers)); String jsonString = String.format("{\"score\":\"%s\"}", quizService.getScore(answers));
return jsonString; return jsonString;
} }
@GetMapping("/categories")
public List<Category> getCategories(){
return dataService.getCategories();
}
} }
@@ -50,6 +50,9 @@ public class DataService {
return categoryRepository.saveAll(categories); return categoryRepository.saveAll(categories);
} }
public List<Category> getCategories(){
return categoryRepository.findAll();
}
/** /**
* Moving the collected questions to the questions list * Moving the collected questions to the questions list
* *
@@ -24,6 +24,8 @@ public class QuizService {
private DataService dataService; private DataService dataService;
@Autowired @Autowired
private CleanerService cleanerService; private CleanerService cleanerService;
@Autowired
private StateManagementService stateManager;
/** /**
* Actions: * Actions:
@@ -38,19 +40,20 @@ public class QuizService {
/** First, we check if there are already enough items in our db */ /** First, we check if there are already enough items in our db */
List<QuestionEntity> questionsRetrieved = dataService.lookupQuestions(quizSettings); List<QuestionEntity> 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()) { if(!questionsRetrieved.isEmpty() && questionsRetrieved.size() == quizSettings.getAmount()) {
session.setQuestions(cleanerService.questionListEntitytoDTO(questionsRetrieved)); session.setQuestions(cleanerService.questionListEntitytoDTO(questionsRetrieved));
} cacheService.addToCache(session.getId(), session);
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()) {
return Optional.of(session.getId()); return Optional.of(session.getId());
} }
cacheService.addToCache(session.getId(), session);
boolean addedToQueue = queueService.addToQueue(new Request(session.getId(), quizSettings)); 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) */ /** 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) { if(!addedToQueue) {
@@ -75,16 +78,17 @@ public class QuizService {
throw new IllegalArgumentException("Invalid session ID: " + sessionId); 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 */ /** Still waiting for a response */
if (session.getQuestions() == null) { if (session.getQuestions() == null) {
return Optional.empty(); 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); return Optional.of(session);
} }
@@ -93,7 +97,7 @@ public class QuizService {
* Receives: sessionId and answers * Receives: sessionId and answers
* *
* Actions: * Actions:
* - retrieves session from cache * - Retrieves session from cache
* - Deletes session from cache * - Deletes session from cache
* - Matches answers answers to correctAnswers * - Matches answers answers to correctAnswers
* - Calculates score * - Calculates score
@@ -3,6 +3,7 @@ import io.gatling.core.Predef._
import io.gatling.http.Predef._ import io.gatling.http.Predef._
import scala.concurrent.duration._ import scala.concurrent.duration._
/** Does not work, use the LoadTestMultiple.sh in the /scripts folder in the root instead */
class QuestionStatusSimulation extends Simulation { class QuestionStatusSimulation extends Simulation {
val httpProtocol = http.baseUrl("http://localhost:9090").acceptHeader("application/json") val httpProtocol = http.baseUrl("http://localhost:9090").acceptHeader("application/json")
+4
View File
@@ -0,0 +1,4 @@
export interface Category {
id: number;
name: string;
}
+3 -1
View File
@@ -1,5 +1,7 @@
import { Category } from "./category";
export interface QuizParams { export interface QuizParams {
category: string; category: number;
type: string; type: string;
amount: number; amount: number;
difficulty: string; difficulty: string;
+8 -31
View File
@@ -19,6 +19,7 @@ import { StorageService } from './storage.service';
import { CleanerService } from './cleaner.service'; import { CleanerService } from './cleaner.service';
import { ResponseHandlerService } from './response-handler.service'; import { ResponseHandlerService } from './response-handler.service';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { Category } from '../models/category';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -40,8 +41,7 @@ export class QuizService {
} }
this.storageService.setSession(sessionIdObj.sessionId); this.storageService.setSession(sessionIdObj.sessionId);
const sessionObj = await lastValueFrom(this.pollQuestions(sessionIdObj.sessionId)); const sessionObj = await lastValueFrom(this.pollQuestions(sessionIdObj.sessionId));
return this.reponseHandler.handleResponse(sessionObj.statusCode, sessionObj);
return this.reponseHandler.handleResponse(sessionIdObj.statusCode, sessionObj);
} }
public pollQuestions(sessionId: string) { public pollQuestions(sessionId: string) {
@@ -67,7 +67,7 @@ export class QuizService {
private convertParamsToUrl(quizParams: QuizParams) { private convertParamsToUrl(quizParams: QuizParams) {
let url = `?amount=${quizParams.amount}`; let url = `?amount=${quizParams.amount}`;
const catIndex = this.getCategories().indexOf(quizParams.category); const catIndex = quizParams.category;
url += catIndex !== 0 ? `&category=${catIndex}` : ''; url += catIndex !== 0 ? `&category=${catIndex}` : '';
const diff = quizParams.difficulty.toLowerCase(); const diff = quizParams.difficulty.toLowerCase();
@@ -83,34 +83,11 @@ export class QuizService {
return url; return url;
} }
public getCategories(): string[] { public async getCategories(): Promise<Category[]> {
return [ let categories: Category[] = await firstValueFrom(this.http.get<any>(this.apiUrl + '/categories'));
'Any Category', categories.push({id:0,name:"Any Category"});
'General Knowledge', categories.sort((a,b) => a.id - b.id);
'Entertainment: Books', return categories;
'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 getDifficulties(): string[] { public getDifficulties(): string[] {
@@ -5,7 +5,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="category" class="block mb-1">Category</label> <label for="category" class="block mb-1">Category</label>
<select id="category" class="w-full p-2 border rounded" [(ngModel)]="category"> <select id="category" class="w-full p-2 border rounded" [(ngModel)]="category">
<option *ngFor="let category of quizService.getCategories()" [value]="category">{{ category }}</option> <option *ngFor="let category of categories" [value]="category.id">{{ category.name }}</option>
</select> </select>
</div> </div>
+10 -6
View File
@@ -7,6 +7,7 @@ import { QuizParams } from '../models/quiz-params';
import { SpinnerService } from '../../Spinner/services/spinner.service'; import { SpinnerService } from '../../Spinner/services/spinner.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Question } from '../models/question'; import { Question } from '../models/question';
import { Category } from '../models/category';
@Component({ @Component({
selector: 'app-setup', selector: 'app-setup',
@@ -17,8 +18,8 @@ import { Question } from '../models/question';
}) })
export class SetupComponent { export class SetupComponent {
categories!: Category[];
category: string = ""; category!: number;
difficulty: string = ""; difficulty: string = "";
type: string = ""; type: string = "";
amount: number = 10; amount: number = 10;
@@ -28,10 +29,13 @@ export class SetupComponent {
public quizService: QuizService, public quizService: QuizService,
private spinnerService: SpinnerService private spinnerService: SpinnerService
) { ) {
/** Setting default values */ quizService.getCategories().then((categories: Category[]) => {
this.category = this.quizService.getCategories()[0]; /** Setting default values */
this.difficulty = this.quizService.getDifficulties()[0]; this.categories = categories;
this.type = this.quizService.getTypes()[0]; this.category = this.categories.find(item => item.id == 0)!.id;
this.difficulty = this.quizService.getDifficulties()[0];
this.type = this.quizService.getTypes()[0];
});
} }
startQuiz() { startQuiz() {
+3 -4
View File
@@ -2,16 +2,15 @@
start_time=$(date +%s) start_time=$(date +%s)
for i in {1..1000}; do for i in {1..6}; do
./LoadTestOne-Silent.sh & ./LoadTestOne-Silent.sh &
# sleep 0.1 # sleep 0.1
done 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)) elapsed_time=$((end_time - start_time))
echo "Done!" echo "Done!"
echo "Total time taken: $elapsed_time seconds" echo "Total time taken: $elapsed_time seconds"

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB