Implement some short term caching

This commit is contained in:
Matt Young 2024-07-11 22:52:37 -05:00
parent d803b7fd09
commit 3e6048c5cc
17 changed files with 97 additions and 50 deletions

View File

@ -7,25 +7,30 @@ namespace App\Actions\Tabulation;
use App\Exceptions\TabulationException; use App\Exceptions\TabulationException;
use App\Models\Entry; use App\Models\Entry;
use App\Services\AuditionService; use App\Services\AuditionService;
use Illuminate\Support\Facades\Cache;
class AllJudgesCount implements CalculateEntryScore class AllJudgesCount implements CalculateEntryScore
{ {
protected CalculateScoreSheetTotal $calculator; protected CalculateScoreSheetTotal $calculator;
protected AuditionService $auditionService; protected AuditionService $auditionService;
public function __construct() public function __construct(CalculateScoreSheetTotal $calculator, AuditionService $auditionService)
{ {
$this->calculator = app(CalculateScoreSheetTotal::class); $this->calculator = $calculator;
$this->auditionService = app(AuditionService::class); $this->auditionService = $auditionService;
} }
public function calculate(string $mode, Entry $entry): array public function calculate(string $mode, Entry $entry): array
{ {
$this->basicValidation($mode, $entry); $cacheKey = 'entryScore-'.$entry->id.'-'.$mode;
$this->areAllJudgesIn($entry); return Cache::remember($cacheKey, 10, function () use ($mode, $entry) {
$this->areAllJudgesValid($entry); $this->basicValidation($mode, $entry);
$this->areAllJudgesIn($entry);
$this->areAllJudgesValid($entry);
return $this->getJudgeTotals($mode, $entry);
});
return $this->getJudgeTotals($mode, $entry);
} }
protected function getJudgeTotals($mode, Entry $entry) protected function getJudgeTotals($mode, Entry $entry)

View File

@ -7,6 +7,7 @@ namespace App\Actions\Tabulation;
use App\Exceptions\TabulationException; use App\Exceptions\TabulationException;
use App\Models\Audition; use App\Models\Audition;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Cache;
class RankAuditionEntries class RankAuditionEntries
{ {
@ -17,12 +18,17 @@ class RankAuditionEntries
$this->calculator = $calculator; $this->calculator = $calculator;
} }
public function booo() public function rank(string $mode, Audition $audition): Collection
{ {
return 'blah'; $cacheKey = 'audition'.$audition->id.$mode;
return Cache::remember($cacheKey, 30, function () use ($mode, $audition) {
return $this->calculateRank($mode, $audition);
});
} }
public function rank(string $mode, Audition $audition): Collection public function calculateRank(string $mode, Audition $audition): Collection
{ {
$this->basicValidation($mode, $audition); $this->basicValidation($mode, $audition);
$entries = match ($mode) { $entries = match ($mode) {

View File

@ -69,6 +69,6 @@ class AppServiceProvider extends ServiceProvider
User::observe(UserObserver::class); User::observe(UserObserver::class);
SeatingLimit::observe(SeatingLimitObserver::class); SeatingLimit::observe(SeatingLimitObserver::class);
Model::preventLazyLoading(! app()->isProduction()); #Model::preventLazyLoading(! app()->isProduction());
} }
} }

View File

@ -16,7 +16,10 @@ class AuditionService
public function __construct() public function __construct()
{ {
self::$allAuditionIds = Audition::pluck('id'); $cacheKey = 'allAuditionIds';
self::$allAuditionIds = Cache::remember($cacheKey, 60, function () {
return Audition::pluck('id');
});
} }
/** /**
@ -75,9 +78,4 @@ class AuditionService
throw new AuditionServiceException('Invalid sort requested. Sort must be tiebreak or weight'); throw new AuditionServiceException('Invalid sort requested. Sort must be tiebreak or weight');
} }
} }
public function auditionExists($audition)
{
return self::$allAuditionIds->contains($audition->id);
}
} }

View File

@ -3,6 +3,7 @@
namespace App\Services; namespace App\Services;
use App\Models\Entry; use App\Models\Entry;
use Illuminate\Support\Facades\Cache;
class EntryService class EntryService
{ {
@ -25,11 +26,10 @@ class EntryService
public function entryExists(Entry $entry): bool public function entryExists(Entry $entry): bool
{ {
static $allEntryIds = null; $cacheKey = 'allEntryIds';
$allEntryIds = Cache::remember($cacheKey, 60, function () {
if ($allEntryIds === null) { return Entry::pluck('id');
$allEntryIds = Entry::pluck('id'); });
}
return $allEntryIds->contains($entry->id); return $allEntryIds->contains($entry->id);
} }

View File

@ -3,6 +3,7 @@
namespace App\Services; namespace App\Services;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Cache;
class UserService class UserService
{ {
@ -13,10 +14,10 @@ class UserService
public function userExists(User $user): bool public function userExists(User $user): bool
{ {
static $allUserIds = null; $cacheKey = 'allUserIds';
if ($allUserIds === null) { $allUserIds = Cache::remember($cacheKey, 60, function () {
$allUserIds = User::pluck('id'); return User::pluck('id');
} });
return $allUserIds->contains($user->id); return $allUserIds->contains($user->id);
} }

View File

@ -5,6 +5,7 @@ use App\Exceptions\ScoreEntryException;
use App\Models\Entry; use App\Models\Entry;
use App\Models\User; use App\Models\User;
use App\Settings; use App\Settings;
use Illuminate\Support\Facades\App;
function tw_max_width_class_array(): array function tw_max_width_class_array(): array
{ {
@ -39,6 +40,6 @@ function auditionSetting($key)
*/ */
function enterScore(User $user, Entry $entry, array $scores): \App\Models\ScoreSheet function enterScore(User $user, Entry $entry, array $scores): \App\Models\ScoreSheet
{ {
$scoreEntry = new EnterScore(); $scoreEntry = App::make(EnterScore::class);
return $scoreEntry($user, $entry, $scores); return $scoreEntry($user, $entry, $scores);
} }

View File

@ -74,6 +74,7 @@ return [
'driver' => 'redis', 'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
'prefix' => env('REDIS_CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
], ],
'dynamodb' => [ 'dynamodb' => [

View File

@ -8,17 +8,20 @@ use App\Models\Entry;
use App\Models\Room; use App\Models\Room;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\App;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
it('throws an exception if mode is not seating or advancement', function () { it('throws an exception if mode is not seating or advancement', function () {
$calculator = new AllJudgesCount(); #$calculator = new AllJudgesCount();
$calculator = App::make(AllJudgesCount::class);
$calculator->calculate('WRONG', Entry::factory()->create()); $calculator->calculate('WRONG', Entry::factory()->create());
})->throws(TabulationException::class, 'Mode must be seating or advancement'); })->throws(TabulationException::class, 'Mode must be seating or advancement');
it('throws an exception if entry is not valid', function () { it('throws an exception if entry is not valid', function () {
// Arrange // Arrange
$calculator = new AllJudgesCount(); #$calculator = new AllJudgesCount();
$calculator = App::make(AllJudgesCount::class);
// Act // Act
$calculator->calculate('seating', Entry::factory()->make()); $calculator->calculate('seating', Entry::factory()->make());
// Assert // Assert
@ -38,7 +41,8 @@ it('throws an exception if entry is missing judge scores', function () {
1004 => 80, 1004 => 80,
1005 => 90, 1005 => 90,
]; ];
$calculator = new AllJudgesCount(); #$calculator = new AllJudgesCount();
$calculator = App::make(AllJudgesCount::class);
enterScore($judge1, $entry, $scores); enterScore($judge1, $entry, $scores);
// Act // Act
$calculator->calculate('seating', $entry); $calculator->calculate('seating', $entry);
@ -61,7 +65,8 @@ it('throws an exception if a score exists from an invalid judge', function () {
1004 => 80, 1004 => 80,
1005 => 90, 1005 => 90,
]; ];
$calculator = new AllJudgesCount(); #$calculator = new AllJudgesCount();
$calculator = App::make(AllJudgesCount::class);
enterScore($judge1, $entry, $scores); enterScore($judge1, $entry, $scores);
$scoreSheetToSpoof = enterScore($judge2, $entry, $scores); $scoreSheetToSpoof = enterScore($judge2, $entry, $scores);
$scoreSheetToSpoof->update(['user_id' => $judge3->id]); $scoreSheetToSpoof->update(['user_id' => $judge3->id]);
@ -92,7 +97,8 @@ it('correctly calculates scores for seating', function () {
1004 => 85, 1004 => 85,
1005 => 95, 1005 => 95,
]; ];
$calculator = new AllJudgesCount(); #$calculator = new AllJudgesCount();
$calculator = App::make(AllJudgesCount::class);
enterScore($judge1, $entry, $scores); enterScore($judge1, $entry, $scores);
enterScore($judge2, $entry, $scores2); enterScore($judge2, $entry, $scores2);
// Act // Act
@ -124,7 +130,8 @@ it('correctly calculates scores for advancement', function () {
1004 => 85, 1004 => 85,
1005 => 95, 1005 => 95,
]; ];
$calculator = new AllJudgesCount(); #$calculator = new AllJudgesCount();
$calculator = App::make(AllJudgesCount::class);
enterScore($judge1, $entry, $scores); enterScore($judge1, $entry, $scores);
enterScore($judge2, $entry, $scores2); enterScore($judge2, $entry, $scores2);
// Act // Act

View File

@ -8,11 +8,11 @@ use App\Models\Entry;
use App\Models\Room; use App\Models\Room;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
beforeEach(function () {
$this->calculator = app(CalculateScoreSheetTotal::class);
});
it('throws an exception if an invalid mode is called for', function () { it('throws an exception if an invalid mode is called for', function () {
$calculator = app(CalculateScoreSheetTotal::class); $calculator = app(CalculateScoreSheetTotal::class);
$calculator('anything', Entry::factory()->create(), User::factory()->create()); $calculator('anything', Entry::factory()->create(), User::factory()->create());
@ -27,9 +27,14 @@ it('throws an exception if an invalid entry is provided', function () {
})->throws(TabulationException::class, 'Invalid entry provided'); })->throws(TabulationException::class, 'Invalid entry provided');
it('throws an exception if the specified judge has not scored the entry', function () { it('throws an exception if the specified judge has not scored the entry', function () {
// Arrange // Arrange
loadSampleAudition();
$judge = User::factory()->create();
Room::find(1000)->addJudge($judge);
$entry = Entry::factory()->create(['audition_id' => 1000]);
Artisan::call('cache:clear');
$calculator = app(CalculateScoreSheetTotal::class); $calculator = app(CalculateScoreSheetTotal::class);
// Act // Act
$calculator('seating', Entry::factory()->create(), User::factory()->create()); $calculator('seating', $entry, $judge);
//Assert //Assert
})->throws(TabulationException::class, 'No score sheet by that judge for that entry'); })->throws(TabulationException::class, 'No score sheet by that judge for that entry');
it('correctly calculates final score for seating', function () { it('correctly calculates final score for seating', function () {

View File

@ -9,11 +9,13 @@ use App\Models\Room;
use App\Models\ScoreSheet; use App\Models\ScoreSheet;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\App;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
beforeEach(function () { beforeEach(function () {
$this->scoreEntry = new EnterScore(); #$this->scoreEntry = new EnterScore();
$this->scoreEntry = App::make(EnterScore::class);
}); });
test('throws an exception if the user does not exist', function () { test('throws an exception if the user does not exist', function () {

View File

@ -8,17 +8,20 @@ use App\Models\Entry;
use App\Models\Room; use App\Models\Room;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
it('throws an exception if an invalid mode is specified', function () { it('throws an exception if an invalid mode is specified', function () {
$ranker = new RankAuditionEntries(new AllJudgesCount()); #$ranker = new RankAuditionEntries(new AllJudgesCount());
$ranker = App::make(RankAuditionEntries::class);
$ranker->rank('wrong', Audition::factory()->create()); $ranker->rank('wrong', Audition::factory()->create());
})->throws(TabulationException::class, 'Mode must be seating or advancement'); })->throws(TabulationException::class, 'Mode must be seating or advancement');
it('throws an exception if an invalid audition is provided', function () { it('throws an exception if an invalid audition is provided', function () {
// Arrange // Arrange
$ranker = new RankAuditionEntries(new AllJudgesCount()); #$ranker = new RankAuditionEntries(new AllJudgesCount());
$ranker = App::make(RankAuditionEntries::class);
// Act & Assert // Act & Assert
$ranker->rank('seating', Audition::factory()->make()); $ranker->rank('seating', Audition::factory()->make());
})->throws(TabulationException::class, 'Invalid audition provided'); })->throws(TabulationException::class, 'Invalid audition provided');
@ -26,7 +29,8 @@ it('includes all entries of the given mode in the return', function () {
$audition = Audition::factory()->create(); $audition = Audition::factory()->create();
$entries = Entry::factory()->seatingOnly()->count(10)->create(['audition_id' => $audition->id]); $entries = Entry::factory()->seatingOnly()->count(10)->create(['audition_id' => $audition->id]);
$otherEntries = Entry::factory()->advanceOnly()->count(10)->create(['audition_id' => $audition->id]); $otherEntries = Entry::factory()->advanceOnly()->count(10)->create(['audition_id' => $audition->id]);
$ranker = new RankAuditionEntries(new AllJudgesCount()); #$ranker = new RankAuditionEntries(new AllJudgesCount());
$ranker = App::make(RankAuditionEntries::class);
// Act // Act
$return = $ranker->rank('seating', $audition); $return = $ranker->rank('seating', $audition);
// Assert // Assert
@ -39,6 +43,7 @@ it('includes all entries of the given mode in the return', function () {
}); });
it('places entries in the proper order', function () { it('places entries in the proper order', function () {
// Arrange // Arrange
Artisan::call('cache:clear');
loadSampleAudition(); loadSampleAudition();
$judge = User::factory()->create(); $judge = User::factory()->create();
Room::find(1000)->addJudge($judge); Room::find(1000)->addJudge($judge);
@ -54,7 +59,7 @@ it('places entries in the proper order', function () {
enterScore($judge, $entries[3], $scoreArray4); enterScore($judge, $entries[3], $scoreArray4);
enterScore($judge, $entries[4], $scoreArray5); enterScore($judge, $entries[4], $scoreArray5);
Artisan::call('cache:clear'); Artisan::call('cache:clear');
$ranker = new RankAuditionEntries(new AllJudgesCount()); $ranker = App::make(RankAuditionEntries::class);
$expectedOrder = [4, 1, 3, 5, 2]; $expectedOrder = [4, 1, 3, 5, 2];
// Act // Act
$return = $ranker->rank('seating', Audition::find(1000)); $return = $ranker->rank('seating', Audition::find(1000));

View File

@ -169,6 +169,7 @@ it('has a forAdvancement scope that only returns those entries entered for advan
Entry::factory()->count(10)->create(['for_seating' => true, 'for_advancement' => true]); Entry::factory()->count(10)->create(['for_seating' => true, 'for_advancement' => true]);
Entry::factory()->count(5)->create(['for_seating' => false, 'for_advancement' => true]); Entry::factory()->count(5)->create(['for_seating' => false, 'for_advancement' => true]);
// Act & Assert // Act & Assert
expect(Entry::forAdvancement()->count())->toBe(16) expect(Entry::forAdvancement()->count())->toBe(16)
->and(Entry::forAdvancement()->get()->first())->toBeInstanceOf(Entry::class) ->and(Entry::forAdvancement()->get()->first())->toBeInstanceOf(Entry::class)
->and(Entry::forAdvancement()->get()->first()->student->first_name)->toBe('Advance Only'); ->and(Entry::forAdvancement()->get()->first()->student->first_name)->toBe('Advance Only');

View File

@ -4,9 +4,12 @@ use App\Models\Entry;
use App\Models\School; use App\Models\School;
use App\Models\Student; use App\Models\Student;
use App\Models\User; use App\Models\User;
use App\Services\EntryService;
use App\Services\Invoice\InvoiceOneFeePerEntry;
use App\Settings; use App\Settings;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\App;
use function Pest\Laravel\actingAs; use function Pest\Laravel\actingAs;
use function Pest\Laravel\get; use function Pest\Laravel\get;
@ -62,7 +65,8 @@ it('has a new school link', function () {
}); });
it('shows school data', function () { it('shows school data', function () {
// Arrange // Arrange
$invoiceDataService = new App\Services\Invoice\InvoiceOneFeePerEntry(new App\Services\EntryService(new App\Services\AuditionService())); #$invoiceDataService = new App\Services\Invoice\InvoiceOneFeePerEntry(new App\Services\EntryService(new App\Services\AuditionService()));
$invoiceDataService = App::make(InvoiceOneFeePerEntry::class);
Settings::set('school_fees', 1000); Settings::set('school_fees', 1000);
Settings::set('late_fee', 2500); Settings::set('late_fee', 2500);
actingAs($this->adminUser); actingAs($this->adminUser);

View File

@ -4,6 +4,7 @@ use App\Models\Audition;
use App\Models\Entry; use App\Models\Entry;
use App\Services\DrawService; use App\Services\DrawService;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\App;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
@ -77,7 +78,8 @@ it('sets the draw_number column on each entry in the audition based on the rando
// Arrange // Arrange
$audition = Audition::factory()->hasEntries(10)->create(); $audition = Audition::factory()->hasEntries(10)->create();
Entry::all()->each(fn ($entry) => expect($entry->draw_number)->toBeNull()); Entry::all()->each(fn ($entry) => expect($entry->draw_number)->toBeNull());
$drawService = new DrawService(); #$drawService = new DrawService();
$drawService = App::make(DrawService::class);
$drawService->runOneDraw($audition); $drawService->runOneDraw($audition);
// Act & Assert // Act & Assert
Entry::all()->each(fn ($entry) => expect($entry->draw_number)->not()->toBeNull()); Entry::all()->each(fn ($entry) => expect($entry->draw_number)->not()->toBeNull());
@ -86,7 +88,8 @@ it('only sets draw numbers in the specified audition', function () {
// Arrange // Arrange
$audition = Audition::factory()->hasEntries(10)->create(); $audition = Audition::factory()->hasEntries(10)->create();
$bonusEntry = Entry::factory()->create(); $bonusEntry = Entry::factory()->create();
$drawService = new DrawService(); #$drawService = new DrawService();
$drawService = App::make(DrawService::class);
$drawService->runOneDraw($audition); $drawService->runOneDraw($audition);
// Act & Assert // Act & Assert
expect($bonusEntry->draw_number)->toBeNull(); expect($bonusEntry->draw_number)->toBeNull();

View File

@ -3,25 +3,30 @@
/** @noinspection PhpUnhandledExceptionInspection */ /** @noinspection PhpUnhandledExceptionInspection */
use App\Models\Audition; use App\Models\Audition;
use App\Services\AuditionService;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\App;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
it('throws an exception when an invalid mode is requested', function () { it('throws an exception when an invalid mode is requested', function () {
$auditionService = new \App\Services\AuditionService(); #$auditionService = new \App\Services\AuditionService();
$auditionService = App::make(AuditionService::class);
$this->expectException(\App\Exceptions\AuditionServiceException::class); $this->expectException(\App\Exceptions\AuditionServiceException::class);
$auditionService->getSubscores(new Audition(), 'invalid_mode'); $auditionService->getSubscores(new Audition(), 'invalid_mode');
}); });
it('throws an exception when an invalid sort is requested', function () { it('throws an exception when an invalid sort is requested', function () {
// Arrange // Arrange
$auditionService = new \App\Services\AuditionService(); #$auditionService = new \App\Services\AuditionService();
$auditionService = App::make(AuditionService::class);
$this->expectException(\App\Exceptions\AuditionServiceException::class); $this->expectException(\App\Exceptions\AuditionServiceException::class);
// Act // Act
$auditionService->getSubscores(new Audition(), 'seating', 'invalid_sort'); $auditionService->getSubscores(new Audition(), 'seating', 'invalid_sort');
}); });
it('throws an exception when an invalid audition is provided', function () { it('throws an exception when an invalid audition is provided', function () {
// Arrange // Arrange
$auditionService = new \App\Services\AuditionService(); #$auditionService = new \App\Services\AuditionService();
$auditionService = App::make(AuditionService::class);
$this->expectException(\App\Exceptions\AuditionServiceException::class); $this->expectException(\App\Exceptions\AuditionServiceException::class);
$auditionService->getSubscores(new Audition(), 'seating', 'tiebreak'); $auditionService->getSubscores(new Audition(), 'seating', 'tiebreak');
// Act & Assert // Act & Assert
@ -30,7 +35,8 @@ it('throws an exception when an invalid audition is provided', function () {
it('gets subscores for an audition', function () { it('gets subscores for an audition', function () {
// Arrange // Arrange
loadSampleAudition(); loadSampleAudition();
$auditionService = new \App\Services\AuditionService(); #$auditionService = new \App\Services\AuditionService();
$auditionService = App::make(AuditionService::class);
// Act // Act
$subscores = $auditionService->getSubscores(Audition::find(1000), 'seating', 'tiebreak'); $subscores = $auditionService->getSubscores(Audition::find(1000), 'seating', 'tiebreak');
// Assert // Assert

View File

@ -7,11 +7,13 @@ use App\Models\ScoreSheet;
use App\Models\User; use App\Models\User;
use App\Services\ScoreService; use App\Services\ScoreService;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\App;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
beforeEach(function () { beforeEach(function () {
$this->scoreService = new ScoreService(); #$this->scoreService = new ScoreService();
$this->scoreService = App::make(ScoreService::class);
}); });
it('can check if an entry is fully scored', function () { it('can check if an entry is fully scored', function () {