Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c2c561182 | ||
|
|
e1b04745b2 | ||
|
|
b82dfdbe17 |
@@ -35,4 +35,15 @@ sudo reboot
|
||||
```bash
|
||||
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
|
||||
+38
-5
@@ -1,20 +1,20 @@
|
||||
# Issues
|
||||
# Reflection
|
||||
|
||||
## Issues during testing
|
||||
## Issues during testing (October 30th)
|
||||
### Scheduler
|
||||
I don't know if it was the scheduler specifically, but whenever I try to test it, both the cache and the queue seem to become empty after the first function is completed.
|
||||
I don't know how to solve this yet
|
||||
|
||||
|
||||
## Caching solution
|
||||
## Caching solution (October 30th)
|
||||
### Lack of data
|
||||
Not being able to gather blocks of 50 is a huge issue. The idea of reusing blocks becomes less viable if I can just get a big chunk every time for others to use
|
||||
Not being able to gather blocks of 50 is a big issue. The idea of reusing blocks becomes less viable if I can't just get a big chunk every time for others to use.
|
||||
I can't force a big chunk, because it might just return a status 1, and then the user gets nothing.
|
||||
|
||||
Perhaps I can separate the questions myself and make a collection, that i can build the requests from. However, for every quest tracking if someone has already seen is a huge pain.
|
||||
|
||||
|
||||
## Possible solution
|
||||
## Possible solution (October 31st)
|
||||
Cache all data, 50 at a time
|
||||
Store it in a database
|
||||
When its done, let the db make blocks for quick cache access
|
||||
@@ -28,3 +28,36 @@ 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 (November 3rd)
|
||||
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
|
||||
|
||||
## Possible future alternative
|
||||
In the current state (when the cache is done), the backend might hold data 3 times (Cache, active DB table, and background DB table during the night)
|
||||
|
||||
### Purely Cached solution
|
||||
- Every night
|
||||
- Check for each category+difficulty combination how many questions there are (API has an endpoint for this)
|
||||
- Track which one you are on, and how many you have
|
||||
- Take blocks of 50, until the remaining items are less than 50, then get that exact amount
|
||||
- When they are gathered, either
|
||||
1. Divide them between 3 cache items: cat:dif:any/multiple/boolean
|
||||
- (Manually filter on the type, since the API sends questions with either option together), or
|
||||
2. In one like cat:dif:any and then filter it by hand when a user asks for it
|
||||
- (This will replace the collection put there the night before)
|
||||
- Then move to the next one
|
||||
- Stop when the last combination has been collected
|
||||
|
||||
#### When a user requests data:
|
||||
- Get the items from the cache
|
||||
- Less items than requested: Inform the user to try a different combination
|
||||
- More items than requested: Randomize and select a subset
|
||||
|
||||
### Tradeoffs
|
||||
- Advantage: Less total data held on the server
|
||||
|
||||
- Downside: Slower gathering, since there are 25+3+4=300 unique combinations (each of which will end with a request of less than 50 items), it will require a decent number requests more than the bulk collecting that the DB does.
|
||||
@@ -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<Category> getCategories(){
|
||||
return dataService.getCategories();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ public class DataService {
|
||||
return categoryRepository.saveAll(categories);
|
||||
}
|
||||
|
||||
public List<Category> getCategories(){
|
||||
return categoryRepository.findAll();
|
||||
}
|
||||
/**
|
||||
* Moving the collected questions to the questions list
|
||||
*
|
||||
|
||||
@@ -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<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()) {
|
||||
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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Category } from "./category";
|
||||
|
||||
export interface QuizParams {
|
||||
category: string;
|
||||
category: number;
|
||||
type: string;
|
||||
amount: number;
|
||||
difficulty: string;
|
||||
|
||||
@@ -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<Category[]> {
|
||||
let categories: Category[] = await firstValueFrom(this.http.get<any>(this.apiUrl + '/categories'));
|
||||
categories.push({id:0,name:"Any Category"});
|
||||
categories.sort((a,b) => a.id - b.id);
|
||||
return categories;
|
||||
}
|
||||
|
||||
public getDifficulties(): string[] {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="mb-4">
|
||||
<label for="category" class="block mb-1">Category</label>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Reference in New Issue
Block a user