Denormalize automation trigger_type into an indexed column
The scheduler command ran every minute and loaded all active automations, then filtered by trigger_type in PHP because that value lived buried in the nodes JSON array — effectively a full-table scan plus a JSON decode per row each minute, discarding every non-schedule automation. Derive trigger_type into a real, indexed column on save (recomputed in the existing saving() hook so it can never drift from nodes) and filter on it in SQL. Applies to both the schedule firer and the post-trigger dispatcher.
This commit is contained in:
parent
605261e1b8
commit
a129b6b5cd
6 changed files with 57 additions and 6 deletions
|
|
@ -5,6 +5,7 @@
|
|||
namespace App\Actions\Automation\Trigger;
|
||||
|
||||
use App\Actions\Automation\Run\AdvanceAutomationRun;
|
||||
use App\Enums\Automation\Node\Type as NodeType;
|
||||
use App\Enums\Automation\Run\Status as RunStatus;
|
||||
use App\Enums\Automation\Status as AutomationStatus;
|
||||
use App\Enums\Automation\Trigger\Type as TriggerType;
|
||||
|
|
@ -32,11 +33,13 @@ public function __invoke(Post $post, TriggerType $triggerType): void
|
|||
$automations = Automation::query()
|
||||
->where('workspace_id', $post->workspace_id)
|
||||
->where('status', AutomationStatus::Active)
|
||||
->where('trigger_type', $triggerType->value)
|
||||
->get();
|
||||
|
||||
foreach ($automations as $automation) {
|
||||
$triggerNode = collect($automation->nodes ?? [])->firstWhere('type', 'trigger');
|
||||
if (data_get($triggerNode, 'data.trigger_type') !== $triggerType->value) {
|
||||
$triggerNode = collect($automation->nodes ?? [])->firstWhere('type', NodeType::Trigger->value);
|
||||
|
||||
if ($triggerNode === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use App\Actions\Automation\Trigger\FireScheduleTrigger;
|
||||
use App\Enums\Automation\Status;
|
||||
use App\Enums\Automation\Trigger\Type as TriggerType;
|
||||
use App\Models\Automation;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
|
|
@ -19,12 +20,9 @@ public function handle(FireScheduleTrigger $fire): int
|
|||
{
|
||||
Automation::query()
|
||||
->where('status', Status::Active)
|
||||
->where('trigger_type', TriggerType::Schedule->value)
|
||||
->chunkById(50, function ($automations) use ($fire) {
|
||||
foreach ($automations as $automation) {
|
||||
$triggerNode = collect($automation->nodes ?? [])->firstWhere('type', 'trigger');
|
||||
if (($triggerNode['data']['trigger_type'] ?? null) !== 'schedule') {
|
||||
continue;
|
||||
}
|
||||
$fire($automation);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\Automation\Node\Type as NodeType;
|
||||
use App\Enums\Automation\Status;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -49,6 +50,7 @@ protected static function booted(): void
|
|||
$automation->variables ?? [],
|
||||
$automation->getOriginal('variables') ?? [],
|
||||
);
|
||||
$automation->trigger_type = self::deriveTriggerType($automation->nodes ?? []);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +102,20 @@ public function runs(): HasMany
|
|||
* never received the real value) so we keep the existing ciphertext. Plain
|
||||
* text values get encrypted; already-encrypted strings pass through.
|
||||
*
|
||||
* Denormalize the trigger node's type into an indexed column so the
|
||||
* scheduler can filter by it in SQL instead of decoding every automation's
|
||||
* `nodes` JSON each minute. Recomputed on every save so it cannot drift.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $nodes
|
||||
*/
|
||||
private static function deriveTriggerType(array $nodes): ?string
|
||||
{
|
||||
$triggerNode = collect($nodes)->firstWhere('type', NodeType::Trigger->value);
|
||||
|
||||
return data_get($triggerNode, 'data.trigger_type');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $incoming
|
||||
* @param array<int, array<string, mixed>>|string $original
|
||||
* @return array<int, array<string, mixed>>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public function up(): void
|
|||
$table->foreignUuid('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('status')->default('draft');
|
||||
$table->string('trigger_type')->nullable();
|
||||
$table->json('nodes')->nullable();
|
||||
$table->json('connections')->nullable();
|
||||
$table->timestamp('activated_at')->nullable();
|
||||
|
|
@ -26,6 +27,7 @@ public function up(): void
|
|||
$table->timestamps();
|
||||
|
||||
$table->index(['workspace_id', 'status']);
|
||||
$table->index(['status', 'trigger_type']);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,27 @@
|
|||
expect($automation->nodes[0]['type'])->toBe('trigger');
|
||||
});
|
||||
|
||||
it('derives the trigger_type column from the trigger node on save', function () {
|
||||
$automation = Automation::factory()->withScheduleTrigger()->create();
|
||||
|
||||
expect($automation->trigger_type)->toBe('schedule');
|
||||
|
||||
$automation->update([
|
||||
'nodes' => [
|
||||
['id' => 'trigger_1', 'type' => 'trigger', 'position' => ['x' => 0, 'y' => 0],
|
||||
'data' => ['trigger_type' => 'post_published']],
|
||||
],
|
||||
]);
|
||||
|
||||
expect($automation->fresh()->trigger_type)->toBe('post_published');
|
||||
});
|
||||
|
||||
it('nulls the trigger_type column when there is no trigger node', function () {
|
||||
$automation = Automation::factory()->create(['nodes' => []]);
|
||||
|
||||
expect($automation->trigger_type)->toBeNull();
|
||||
});
|
||||
|
||||
it('relates trigger items, runs and node runs', function () {
|
||||
$automation = Automation::factory()->create();
|
||||
$item = AutomationTriggerItem::factory()->for($automation)->create();
|
||||
|
|
|
|||
|
|
@ -17,3 +17,14 @@
|
|||
expect(AutomationTriggerItem::where('automation_id', $scheduleAutomation->id)->count())->toBe(1);
|
||||
expect(AutomationTriggerItem::where('automation_id', $rssAutomation->id)->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('ignores automations whose trigger is not a schedule', function () {
|
||||
$postAutomation = Automation::factory()->active()->create([
|
||||
'nodes' => [['id' => 't', 'type' => 'trigger', 'position' => ['x' => 0, 'y' => 0],
|
||||
'data' => ['trigger_type' => 'post_published']]],
|
||||
]);
|
||||
|
||||
$this->artisan('automation:fire-schedule')->assertSuccessful();
|
||||
|
||||
expect(AutomationTriggerItem::where('automation_id', $postAutomation->id)->count())->toBe(0);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue