Add support for executing a scheduled task right now
This commit is contained in:
parent
f33d0b1d72
commit
c1ee0ac4f8
13 changed files with 158 additions and 157 deletions
|
@ -15,16 +15,6 @@ interface ScheduleRepositoryInterface extends RepositoryInterface
|
|||
*/
|
||||
public function findServerSchedules(int $server): Collection;
|
||||
|
||||
/**
|
||||
* Load the tasks relationship onto the Schedule module if they are not
|
||||
* already present.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Schedule $schedule
|
||||
* @param bool $refresh
|
||||
* @return \Pterodactyl\Models\Schedule
|
||||
*/
|
||||
public function loadTasks(Schedule $schedule, bool $refresh = false): Schedule;
|
||||
|
||||
/**
|
||||
* Return a schedule model with all of the associated tasks as a relationship.
|
||||
*
|
||||
|
|
|
@ -10,15 +10,19 @@ use Pterodactyl\Models\Server;
|
|||
use Pterodactyl\Models\Schedule;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Helpers\Utilities;
|
||||
use Pterodactyl\Jobs\Schedule\RunTaskJob;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
|
||||
use Pterodactyl\Services\Schedules\ProcessScheduleService;
|
||||
use Pterodactyl\Transformers\Api\Client\ScheduleTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\UpdateScheduleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\TriggerScheduleRequest;
|
||||
|
||||
class ScheduleController extends ClientApiController
|
||||
{
|
||||
|
@ -27,16 +31,23 @@ class ScheduleController extends ClientApiController
|
|||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Schedules\ProcessScheduleService
|
||||
*/
|
||||
private $service;
|
||||
|
||||
/**
|
||||
* ScheduleController constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\ScheduleRepository $repository
|
||||
* @param \Pterodactyl\Services\Schedules\ProcessScheduleService $service
|
||||
*/
|
||||
public function __construct(ScheduleRepository $repository)
|
||||
public function __construct(ScheduleRepository $repository, ProcessScheduleService $service)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->repository = $repository;
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -147,6 +158,30 @@ class ScheduleController extends ClientApiController
|
|||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a given schedule immediately rather than waiting on it's normally scheduled time
|
||||
* to pass. This does not care about the schedule state.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\TriggerScheduleRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param \Pterodactyl\Models\Schedule $schedule
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function execute(TriggerScheduleRequest $request, Server $server, Schedule $schedule)
|
||||
{
|
||||
if (!$schedule->is_active) {
|
||||
throw new BadRequestHttpException(
|
||||
'Cannot trigger schedule exection for a schedule that is not currently active.'
|
||||
);
|
||||
}
|
||||
|
||||
$this->service->handle($schedule, true);
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_ACCEPTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a schedule and it's associated tasks.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Schedules;
|
||||
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class TriggerScheduleRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function permission(): string
|
||||
{
|
||||
return Permission::ACTION_SCHEDULE_UPDATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -42,15 +42,13 @@ class RunTaskJob extends Job implements ShouldQueue
|
|||
* @param \Pterodactyl\Repositories\Wings\DaemonCommandRepository $commandRepository
|
||||
* @param \Pterodactyl\Services\Backups\InitiateBackupService $backupService
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\TaskRepository $taskRepository
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handle(
|
||||
DaemonCommandRepository $commandRepository,
|
||||
InitiateBackupService $backupService,
|
||||
DaemonPowerRepository $powerRepository,
|
||||
TaskRepository $taskRepository
|
||||
DaemonPowerRepository $powerRepository
|
||||
) {
|
||||
// Do not process a task that is not set to active.
|
||||
if (! $this->task->schedule->is_active) {
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
use Cron\CronExpression;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Container\Container;
|
||||
use Pterodactyl\Contracts\Extensions\HashidsInterface;
|
||||
|
||||
|
@ -114,6 +116,20 @@ class Schedule extends Model
|
|||
'next_run_at' => 'nullable|date',
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the schedule's execution crontab entry as a string.
|
||||
*
|
||||
* @return \Carbon\CarbonImmutable
|
||||
*/
|
||||
public function getNextRunDate()
|
||||
{
|
||||
$formatted = sprintf('%s %s %s * %s', $this->cron_minute, $this->cron_hour, $this->cron_day_of_month, $this->cron_day_of_week);
|
||||
|
||||
return CarbonImmutable::createFromTimestamp(
|
||||
CronExpression::factory($formatted)->getNextRunDate()->getTimestamp()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a hashid encoded string to represent the ID of the schedule.
|
||||
*
|
||||
|
|
|
@ -31,23 +31,6 @@ class ScheduleRepository extends EloquentRepository implements ScheduleRepositor
|
|||
return $this->getBuilder()->withCount('tasks')->where('server_id', '=', $server)->get($this->getColumns());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the tasks relationship onto the Schedule module if they are not
|
||||
* already present.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Schedule $schedule
|
||||
* @param bool $refresh
|
||||
* @return \Pterodactyl\Models\Schedule
|
||||
*/
|
||||
public function loadTasks(Schedule $schedule, bool $refresh = false): Schedule
|
||||
{
|
||||
if (! $schedule->relationLoaded('tasks') || $refresh) {
|
||||
$schedule->load('tasks');
|
||||
}
|
||||
|
||||
return $schedule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a schedule model with all of the associated tasks as a relationship.
|
||||
*
|
||||
|
|
|
@ -2,12 +2,10 @@
|
|||
|
||||
namespace Pterodactyl\Services\Schedules;
|
||||
|
||||
use Cron\CronExpression;
|
||||
use Pterodactyl\Models\Schedule;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Pterodactyl\Jobs\Schedule\RunTaskJob;
|
||||
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
|
||||
class ProcessScheduleService
|
||||
{
|
||||
|
@ -17,62 +15,45 @@ class ProcessScheduleService
|
|||
private $dispatcher;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
|
||||
* @var \Illuminate\Database\ConnectionInterface
|
||||
*/
|
||||
private $scheduleRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface
|
||||
*/
|
||||
private $taskRepository;
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* ProcessScheduleService constructor.
|
||||
*
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Illuminate\Contracts\Bus\Dispatcher $dispatcher
|
||||
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $scheduleRepository
|
||||
* @param \Pterodactyl\Contracts\Repository\TaskRepositoryInterface $taskRepository
|
||||
*/
|
||||
public function __construct(
|
||||
Dispatcher $dispatcher,
|
||||
ScheduleRepositoryInterface $scheduleRepository,
|
||||
TaskRepositoryInterface $taskRepository
|
||||
) {
|
||||
public function __construct(ConnectionInterface $connection, Dispatcher $dispatcher)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->scheduleRepository = $scheduleRepository;
|
||||
$this->taskRepository = $taskRepository;
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a schedule and push the first task onto the queue worker.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Schedule $schedule
|
||||
* @param bool $now
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handle(Schedule $schedule)
|
||||
public function handle(Schedule $schedule, bool $now = false)
|
||||
{
|
||||
$this->scheduleRepository->loadTasks($schedule);
|
||||
|
||||
/** @var \Pterodactyl\Models\Task $task */
|
||||
$task = $schedule->getRelation('tasks')->where('sequence_id', 1)->first();
|
||||
$task = $schedule->tasks()->where('sequence_id', 1)->firstOrFail();
|
||||
|
||||
$formattedCron = sprintf('%s %s %s * %s',
|
||||
$schedule->cron_minute,
|
||||
$schedule->cron_hour,
|
||||
$schedule->cron_day_of_month,
|
||||
$schedule->cron_day_of_week
|
||||
);
|
||||
$this->connection->transaction(function () use ($schedule, $task) {
|
||||
$schedule->forceFill([
|
||||
'is_processing' => true,
|
||||
'next_run_at' => $schedule->getNextRunDate(),
|
||||
])->saveOrFail();
|
||||
|
||||
$this->scheduleRepository->update($schedule->id, [
|
||||
'is_processing' => true,
|
||||
'next_run_at' => CronExpression::factory($formattedCron)->getNextRunDate(),
|
||||
]);
|
||||
$task->update(['is_queued' => true]);
|
||||
});
|
||||
|
||||
$this->taskRepository->update($task->id, ['is_queued' => true]);
|
||||
|
||||
$this->dispatcher->dispatch(
|
||||
$this->dispatcher->{$now ? 'dispatchNow' : 'dispatch'}(
|
||||
(new RunTaskJob($task))->delay($task->time_offset)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
export default async (server: string, schedule: number): Promise<void> =>
|
||||
await http.post(`/api/client/servers/${server}/schedules/${schedule}/execute`);
|
|
@ -0,0 +1,48 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import triggerScheduleExecution from '@/api/server/schedules/triggerScheduleExecution';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { Schedule } from '@/api/server/schedules/getServerSchedules';
|
||||
|
||||
const RunScheduleButton = ({ schedule }: { schedule: Schedule }) => {
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
|
||||
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
||||
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
|
||||
|
||||
const onTriggerExecute = useCallback(() => {
|
||||
clearFlashes('schedule');
|
||||
setLoading(true);
|
||||
triggerScheduleExecution(id, schedule.id)
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
appendSchedule({ ...schedule, isProcessing: true });
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ error, key: 'schedules' });
|
||||
})
|
||||
.then(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SpinnerOverlay visible={loading} size={'large'}/>
|
||||
<Button
|
||||
isSecondary
|
||||
color={'grey'}
|
||||
css={tw`flex-1 sm:flex-none border-transparent`}
|
||||
disabled={schedule.isProcessing}
|
||||
onClick={onTriggerExecute}
|
||||
>
|
||||
Run Now
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunScheduleButton;
|
|
@ -4,7 +4,6 @@ import { Schedule } from '@/api/server/schedules/getServerSchedules';
|
|||
import getServerSchedule from '@/api/server/schedules/getServerSchedule';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import EditScheduleModal from '@/components/server/schedules/EditScheduleModal';
|
||||
import NewTaskButton from '@/components/server/schedules/NewTaskButton';
|
||||
import DeleteScheduleButton from '@/components/server/schedules/DeleteScheduleButton';
|
||||
|
@ -18,6 +17,7 @@ import ScheduleTaskRow from '@/components/server/schedules/ScheduleTaskRow';
|
|||
import isEqual from 'react-fast-compare';
|
||||
import { format } from 'date-fns';
|
||||
import ScheduleCronRow from '@/components/server/schedules/ScheduleCronRow';
|
||||
import RunScheduleButton from '@/components/server/schedules/RunScheduleButton';
|
||||
|
||||
interface Params {
|
||||
id: string;
|
||||
|
@ -49,7 +49,7 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
|
|||
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
|
||||
const { clearFlashes, addError } = useFlash();
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const [ isLoading, setIsLoading ] = useState(true);
|
||||
const [ showEditModal, setShowEditModal ] = useState(false);
|
||||
|
||||
|
@ -68,7 +68,7 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
|
|||
.then(schedule => appendSchedule(schedule))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
addError({ message: httpErrorToHuman(error), key: 'schedules' });
|
||||
clearAndAddHttpError({ error, key: 'schedules' });
|
||||
})
|
||||
.then(() => setIsLoading(false));
|
||||
}, [ match ]);
|
||||
|
@ -146,6 +146,11 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
|
|||
onDeleted={() => history.push(`/server/${id}/schedules`)}
|
||||
/>
|
||||
</Can>
|
||||
{schedule.isActive &&
|
||||
<Can action={'schedule.update'}>
|
||||
<RunScheduleButton schedule={schedule}/>
|
||||
</Can>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -72,6 +72,7 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ
|
|||
Route::post('/', 'Servers\ScheduleController@store');
|
||||
Route::get('/{schedule}', 'Servers\ScheduleController@view');
|
||||
Route::post('/{schedule}', 'Servers\ScheduleController@update');
|
||||
Route::post('/{schedule}/execute', 'Servers\ScheduleController@execute');
|
||||
Route::delete('/{schedule}', 'Servers\ScheduleController@delete');
|
||||
|
||||
Route::post('/{schedule}/tasks', 'Servers\ScheduleTaskController@store');
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Schedules;
|
||||
|
||||
use Mockery as m;
|
||||
use Tests\TestCase;
|
||||
use Cron\CronExpression;
|
||||
use Pterodactyl\Models\Task;
|
||||
use Pterodactyl\Models\Schedule;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Pterodactyl\Jobs\Schedule\RunTaskJob;
|
||||
use Pterodactyl\Services\Schedules\ProcessScheduleService;
|
||||
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
|
||||
|
||||
class ProcessScheduleServiceTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Bus\Dispatcher|\Mockery\Mock
|
||||
*/
|
||||
private $dispatcher;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface|\Mockery\Mock
|
||||
*/
|
||||
private $scheduleRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface|\Mockery\Mock
|
||||
*/
|
||||
private $taskRepository;
|
||||
|
||||
/**
|
||||
* Setup tests.
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->dispatcher = m::mock(Dispatcher::class);
|
||||
$this->scheduleRepository = m::mock(ScheduleRepositoryInterface::class);
|
||||
$this->taskRepository = m::mock(TaskRepositoryInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a schedule can be updated and first task set to run.
|
||||
*/
|
||||
public function testScheduleIsUpdatedAndRun()
|
||||
{
|
||||
$model = factory(Schedule::class)->make(['id' => 123]);
|
||||
$model->setRelation('tasks', collect([$task = factory(Task::class)->make([
|
||||
'sequence_id' => 1,
|
||||
])]));
|
||||
|
||||
$this->scheduleRepository->shouldReceive('loadTasks')->with($model)->once()->andReturn($model);
|
||||
|
||||
$formatted = sprintf('%s %s %s * %s', $model->cron_minute, $model->cron_hour, $model->cron_day_of_month, $model->cron_day_of_week);
|
||||
$this->scheduleRepository->shouldReceive('update')->with($model->id, [
|
||||
'is_processing' => true,
|
||||
'next_run_at' => CronExpression::factory($formatted)->getNextRunDate(),
|
||||
]);
|
||||
|
||||
$this->taskRepository->shouldReceive('update')->with($task->id, ['is_queued' => true])->once();
|
||||
|
||||
$this->dispatcher->shouldReceive('dispatch')->with(m::on(function ($class) use ($model, $task) {
|
||||
$this->assertInstanceOf(RunTaskJob::class, $class);
|
||||
$this->assertSame($task->time_offset, $class->delay);
|
||||
$this->assertSame($task->id, $class->task->id);
|
||||
|
||||
return true;
|
||||
}))->once();
|
||||
|
||||
$this->getService()->handle($model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of the service for testing purposes.
|
||||
*
|
||||
* @return \Pterodactyl\Services\Schedules\ProcessScheduleService
|
||||
*/
|
||||
private function getService(): ProcessScheduleService
|
||||
{
|
||||
return new ProcessScheduleService($this->dispatcher, $this->scheduleRepository, $this->taskRepository);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue