Ship production optimization M1-M12.5A: queues, metrics, HTTP observability, and vector search reliability.
Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\IndexArtworkJob;
|
||||
use App\Jobs\IndexUserJob;
|
||||
use App\Jobs\RankBuildScopeListsJob;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
it('assigns artwork and user search index jobs to the search queue', function () {
|
||||
expect((new IndexArtworkJob(15))->queue)->toBe('search')
|
||||
->and((new IndexUserJob(22))->queue)->toBe('search');
|
||||
|
||||
Queue::fake();
|
||||
IndexUserJob::dispatch(22);
|
||||
Queue::assertPushedOn('search', IndexUserJob::class);
|
||||
});
|
||||
|
||||
it('collapses duplicate index jobs with unique ids', function () {
|
||||
$artwork = new IndexArtworkJob(15);
|
||||
$user = new IndexUserJob(22);
|
||||
|
||||
expect($artwork)->toBeInstanceOf(ShouldBeUniqueUntilProcessing::class)
|
||||
->and($artwork->uniqueId())->toBe('15')
|
||||
->and($artwork->uniqueFor)->toBe(120)
|
||||
->and($user)->toBeInstanceOf(ShouldBeUniqueUntilProcessing::class)
|
||||
->and($user->uniqueId())->toBe('22')
|
||||
->and($user->uniqueFor)->toBe(60);
|
||||
});
|
||||
|
||||
it('uniques ranking scope rebuilds so hourly fan-out cannot stack the same scope', function () {
|
||||
$job = new RankBuildScopeListsJob('category', 44);
|
||||
|
||||
expect($job)->toBeInstanceOf(ShouldBeUnique::class)
|
||||
->and($job->uniqueId())->toBe('category:44')
|
||||
->and($job->uniqueFor)->toBe(21600)
|
||||
->and($job->uniqueFor)->toBeGreaterThan(360)
|
||||
->and((new RankBuildScopeListsJob('content_type', 3))->uniqueId())->toBe('content_type:3')
|
||||
->and((new RankBuildScopeListsJob('category', 44))->uniqueId())
|
||||
->not->toBe((new RankBuildScopeListsJob('category', 45))->uniqueId());
|
||||
});
|
||||
|
||||
it('does not enqueue the same ranking scope again after 360s while the unique lock remains', function () {
|
||||
Queue::fake();
|
||||
$this->freezeTime();
|
||||
|
||||
RankBuildScopeListsJob::dispatch('category', 88);
|
||||
$this->travel(361)->seconds();
|
||||
RankBuildScopeListsJob::dispatch('category', 88);
|
||||
RankBuildScopeListsJob::dispatch('category', 89);
|
||||
|
||||
Queue::assertPushed(RankBuildScopeListsJob::class, 2);
|
||||
Queue::assertPushed(RankBuildScopeListsJob::class, fn (RankBuildScopeListsJob $job): bool => $job->uniqueId() === 'category:88');
|
||||
Queue::assertPushed(RankBuildScopeListsJob::class, fn (RankBuildScopeListsJob $job): bool => $job->uniqueId() === 'category:89');
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\IndexUserJob;
|
||||
use App\Jobs\RecComputeSimilarByTagsJob;
|
||||
use App\Support\Queues\QueuedJobClassMatcher;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
it('matches only RecComputeSimilarByTagsJob payloads', function () {
|
||||
$tags = json_encode([
|
||||
'displayName' => RecComputeSimilarByTagsJob::class,
|
||||
'data' => ['commandName' => RecComputeSimilarByTagsJob::class],
|
||||
]);
|
||||
$other = json_encode([
|
||||
'displayName' => IndexUserJob::class,
|
||||
'data' => ['commandName' => IndexUserJob::class],
|
||||
]);
|
||||
|
||||
expect(QueuedJobClassMatcher::isClass($tags, RecComputeSimilarByTagsJob::class))->toBeTrue()
|
||||
->and(QueuedJobClassMatcher::isClass($other, RecComputeSimilarByTagsJob::class))->toBeFalse();
|
||||
});
|
||||
|
||||
it('dry-run counts stale tags jobs and does not remove them', function () {
|
||||
$tags = json_encode(['displayName' => RecComputeSimilarByTagsJob::class, 'uuid' => 'aaa']);
|
||||
$other = json_encode(['displayName' => IndexUserJob::class, 'uuid' => 'bbb']);
|
||||
|
||||
$redis = Mockery::mock();
|
||||
$redis->shouldReceive('llen')->once()->with('queues:default')->andReturn(2);
|
||||
$redis->shouldReceive('lrange')->once()->andReturn([$other, $tags]);
|
||||
$redis->shouldReceive('lrem')->never();
|
||||
Redis::partialMock()->shouldReceive('connection')->andReturn($redis);
|
||||
|
||||
$this->artisan('skinbase:purge-queued-rec-tags --dry-run')
|
||||
->expectsOutputToContain('matched=1')
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
it('execute removes only tags jobs and preserves unrelated payloads', function () {
|
||||
$tagsA = json_encode(['displayName' => RecComputeSimilarByTagsJob::class, 'uuid' => 't1']);
|
||||
$tagsB = json_encode(['displayName' => RecComputeSimilarByTagsJob::class, 'uuid' => 't2']);
|
||||
$other = json_encode(['displayName' => IndexUserJob::class, 'uuid' => 'keep']);
|
||||
|
||||
$redis = Mockery::mock();
|
||||
$redis->shouldReceive('llen')->once()->with('queues:default')->andReturn(3);
|
||||
$redis->shouldReceive('lrange')->once()->andReturn([$other, $tagsA, $tagsB]);
|
||||
$redis->shouldReceive('lrem')->twice()->andReturn(1);
|
||||
Redis::partialMock()->shouldReceive('connection')->andReturn($redis);
|
||||
|
||||
$this->artisan('skinbase:purge-queued-rec-tags --execute')
|
||||
->expectsOutputToContain('matched=2')
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
it('refuses to run without dry-run or execute', function () {
|
||||
$this->artisan('skinbase:purge-queued-rec-tags')
|
||||
->assertFailed();
|
||||
});
|
||||
Reference in New Issue
Block a user