From dc3f9f2e519f74fbe2ed47223a4ac895a79617b5 Mon Sep 17 00:00:00 2001 From: Bohdan Shulha Date: Fri, 21 Jun 2024 15:14:33 +0200 Subject: [PATCH] feat: #11 allow to retry tasks --- app/Casts/TaskMetaCast.php | 10 ++++- app/Casts/TaskPayloadCast.php | 10 ++++- app/Casts/TaskResultCast.php | 10 ++++- app/Http/Controllers/Controller.php | 11 ++++- app/Http/Controllers/NodeController.php | 9 ++-- .../Controllers/NodeTaskGroupController.php | 27 ++++++++++++ app/Models/NodeTask.php | 18 ++++---- app/Models/NodeTaskGroup.php | 42 +++++++++++++++++++ app/Policies/NodeTaskGroupPolicy.php | 17 ++++++++ app/Traits/HasTaskStatus.php | 5 +++ .../js/Components/NodeTasks/TaskGroup.vue | 16 ++++++- .../js/Components/NodeTasks/TaskResult.vue | 3 ++ .../js/Pages/Nodes/Partials/SwarmDetauls.vue | 20 +++++++++ resources/js/Pages/Nodes/Show.vue | 16 +++---- routes/web.php | 3 ++ 15 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 app/Http/Controllers/NodeTaskGroupController.php create mode 100644 app/Policies/NodeTaskGroupPolicy.php create mode 100644 resources/js/Pages/Nodes/Partials/SwarmDetauls.vue diff --git a/app/Casts/TaskMetaCast.php b/app/Casts/TaskMetaCast.php index cd78877..b256a5e 100644 --- a/app/Casts/TaskMetaCast.php +++ b/app/Casts/TaskMetaCast.php @@ -29,7 +29,11 @@ public function get(Model $model, string $key, mixed $value, array $attributes): throw new InvalidArgumentException('Model must be an instance of NodeTask'); } - return self::META_BY_TYPE[$model->type]::from($value); +// if (!isset($attributes['type'])) { +// return null; +// } + + return self::META_BY_TYPE[$attributes['type']]::from($value); } /** @@ -43,6 +47,10 @@ public function set(Model $model, string $key, mixed $value, array $attributes): throw new InvalidArgumentException('Model must be an instance of NodeTask'); } + if (is_string($value)) { + return $value; + } + return $value->toJson(); } } diff --git a/app/Casts/TaskPayloadCast.php b/app/Casts/TaskPayloadCast.php index 86e7086..58ea61d 100644 --- a/app/Casts/TaskPayloadCast.php +++ b/app/Casts/TaskPayloadCast.php @@ -32,7 +32,11 @@ public function get(Model $model, string $key, mixed $value, array $attributes): throw new InvalidArgumentException('Model must be an instance of NodeTask'); } - return self::PAYLOAD_BY_TYPE[$model->type]::from($value); +// if (!isset($attributes['type'])) { +// return null; +// } +//dd($model->type, $attributes); + return self::PAYLOAD_BY_TYPE[$attributes['type']]::from($value); } /** @@ -46,6 +50,10 @@ public function set(Model $model, string $key, mixed $value, array $attributes): throw new InvalidArgumentException('Model must be an instance of NodeTask'); } + if (is_string($value)) { + return $value; + } + return $value->toJson(); } } diff --git a/app/Casts/TaskResultCast.php b/app/Casts/TaskResultCast.php index 19760e1..af0216b 100644 --- a/app/Casts/TaskResultCast.php +++ b/app/Casts/TaskResultCast.php @@ -32,8 +32,12 @@ public function get(Model $model, string $key, mixed $value, array $attributes): return ErrorResult::from($value); } +// if (!isset($attributes['type'])) { +// return null; +// } + if ($value != null) { - return self::RESULT_BY_TYPE[$model->type]::from($value); + return self::RESULT_BY_TYPE[$attributes['type']]::from($value); } return null; @@ -50,6 +54,10 @@ public function set(Model $model, string $key, mixed $value, array $attributes): throw new InvalidArgumentException('Model must be an instance of NodeTask'); } + if (is_string($value)) { + return $value; + } + return $value->toJson(); } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 8677cd5..92cfc68 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -2,7 +2,16 @@ namespace App\Http\Controllers; +use App\Models\User; + abstract class Controller { - // + protected function authorizeOr403(string $ability, ...$arguments) + { + $user = auth()->user(); + + if ($user->cannot($ability, ...$arguments)) { + abort(403); + } + } } diff --git a/app/Http/Controllers/NodeController.php b/app/Http/Controllers/NodeController.php index c63ee8a..6045067 100644 --- a/app/Http/Controllers/NodeController.php +++ b/app/Http/Controllers/NodeController.php @@ -42,9 +42,12 @@ public function store(StoreNodeRequest $request) */ public function show(Node $node) { - $initTaskGroup = $node->tasks()->inProgress()->ofType(InitSwarmTaskPayload::class)->first()?->taskGroup->with('tasks')->first(); - if (!$initTaskGroup) { - $initTaskGroup = $node->tasks()->unsuccessful()->ofType(InitSwarmTaskPayload::class)->first()?->taskGroup->with('tasks')->first(); + $initTaskGroup = null; + if (is_null($node->swarm_id)) { + $initTaskGroup = $node->tasks()->inProgress()->ofType(InitSwarmTaskPayload::class)->first()?->taskGroup->load(['tasks', 'invoker']); + if (!$initTaskGroup) { + $initTaskGroup = $node->tasks()->unsuccessful()->ofType(InitSwarmTaskPayload::class)->latest('id')->first()?->taskGroup->load(['tasks', 'invoker']); + } } return Inertia::render('Nodes/Show', ['node' => $node, 'initTaskGroup' => $initTaskGroup]); diff --git a/app/Http/Controllers/NodeTaskGroupController.php b/app/Http/Controllers/NodeTaskGroupController.php new file mode 100644 index 0000000..a3e93d5 --- /dev/null +++ b/app/Http/Controllers/NodeTaskGroupController.php @@ -0,0 +1,27 @@ +authorizeOr403('retry', $taskGroup); + + $attrs = $request->validate([ + 'node_id' => 'required|exists:nodes,id', + ]); + + $node = Node::whereId($attrs['node_id'])->first(); + + $this->authorizeOr403('view', $node); + + $taskGroup->retry($node); + } +} \ No newline at end of file diff --git a/app/Models/NodeTask.php b/app/Models/NodeTask.php index 9371d02..60a19bd 100644 --- a/app/Models/NodeTask.php +++ b/app/Models/NodeTask.php @@ -52,15 +52,15 @@ class NodeTask extends Model protected static function booted() { - self::creating(function (NodeTask $nodeTask) { - $payload = $nodeTask->payload; - - if (!($payload instanceof AbstractTaskPayload)) { - throw new IllegalArgumentException('Payload must be an instance of AbstractTaskPayload'); - } - - $nodeTask->type = TaskPayloadCast::TYPE_BY_PAYLOAD[get_class($payload)]; - }); +// self::creating(function (NodeTask $nodeTask) { +// $payload = $nodeTask->payload; +// +// if (!($payload instanceof AbstractTaskPayload)) { +// throw new IllegalArgumentException('Payload must be an instance of AbstractTaskPayload'); +// } +// +// $nodeTask->type = TaskPayloadCast::TYPE_BY_PAYLOAD[get_class($payload)]; +// }); } public function taskGroup(): BelongsTo diff --git a/app/Models/NodeTaskGroup.php b/app/Models/NodeTaskGroup.php index a2cec46..9f21c36 100644 --- a/app/Models/NodeTaskGroup.php +++ b/app/Models/NodeTaskGroup.php @@ -10,6 +10,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Symfony\Component\VarDumper\VarDumper; +use function Psy\debug; class NodeTaskGroup extends Model { @@ -43,6 +45,11 @@ public function node(): BelongsTo return $this->belongsTo(Node::class); } + public function invoker(): BelongsTo + { + return $this->belongsTo(User::class, 'invoker_id'); + } + public function start(Node $node): void { $this->status = TaskStatus::Running; @@ -50,4 +57,39 @@ public function start(Node $node): void $this->started_at = now(); $this->save(); } + + public function retry(Node|null $node): void + { + $nodeId = is_null($node) ? null : $node->id; + + $taskGroup = new NodeTaskGroup(); + $taskGroup->node_id = $nodeId; + $taskGroup->forceFill(collect($this->attributes)->only([ + 'swarm_id', + 'invoker_id', + ])->toArray()); + $taskGroup->save(); + + + $taskGroup->tasks()->saveMany($this->tasks->map(function (NodeTask $task) use ($node) { + $dataAttrs = $task->is_completed + ? [ + 'status', + 'started_at', + 'ended_at', + 'result' + ] + : [ + ]; + + $attrs = collect($task->attributes); + + $attributes = $attrs + ->only($dataAttrs) + ->merge($attrs->only(['type', 'meta', 'payload'])) + ->toArray(); + + return (new NodeTask($attributes))->forceFill($attributes); + })); + } } diff --git a/app/Policies/NodeTaskGroupPolicy.php b/app/Policies/NodeTaskGroupPolicy.php new file mode 100644 index 0000000..29de7cd --- /dev/null +++ b/app/Policies/NodeTaskGroupPolicy.php @@ -0,0 +1,17 @@ +belongsToTeam($taskGroup->node->team); + } +} \ No newline at end of file diff --git a/app/Traits/HasTaskStatus.php b/app/Traits/HasTaskStatus.php index 1508de0..1db8d3a 100644 --- a/app/Traits/HasTaskStatus.php +++ b/app/Traits/HasTaskStatus.php @@ -47,6 +47,11 @@ public function getIsRunningAttribute() : bool return $this->status === TaskStatus::Running; } + public function getIsCompletedAttribute() : bool + { + return $this->status === TaskStatus::Completed; + } + public function scopeOfType(Builder $query, string $typeClass): Builder { return $query->where('type', TaskPayloadCast::TYPE_BY_PAYLOAD[$typeClass]); diff --git a/resources/js/Components/NodeTasks/TaskGroup.vue b/resources/js/Components/NodeTasks/TaskGroup.vue index 9cbf3b6..c92edf0 100644 --- a/resources/js/Components/NodeTasks/TaskGroup.vue +++ b/resources/js/Components/NodeTasks/TaskGroup.vue @@ -1,15 +1,29 @@ \ No newline at end of file diff --git a/resources/js/Components/NodeTasks/TaskResult.vue b/resources/js/Components/NodeTasks/TaskResult.vue index 678fcc4..9c9ef7b 100644 --- a/resources/js/Components/NodeTasks/TaskResult.vue +++ b/resources/js/Components/NodeTasks/TaskResult.vue @@ -63,6 +63,8 @@ const classes = computed(() => { + #{{ task.id }} + +
diff --git a/resources/js/Pages/Nodes/Partials/SwarmDetauls.vue b/resources/js/Pages/Nodes/Partials/SwarmDetauls.vue new file mode 100644 index 0000000..f229f71 --- /dev/null +++ b/resources/js/Pages/Nodes/Partials/SwarmDetauls.vue @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/resources/js/Pages/Nodes/Show.vue b/resources/js/Pages/Nodes/Show.vue index 10380db..a420f56 100644 --- a/resources/js/Pages/Nodes/Show.vue +++ b/resources/js/Pages/Nodes/Show.vue @@ -1,25 +1,16 @@ diff --git a/routes/web.php b/routes/web.php index d8b22a8..85aafa2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ name('swarm-tasks.init-cluster'); + Route::post('/node-task-groups/{taskGroup}/retry', [NodeTaskGroupController::class, 'retry'])->name('node-task-groups.retry'); + Route::resource("nodes", NodeController::class); });