Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: Adds basic scheduler #16

Merged
merged 12 commits into from
May 3, 2024
32 changes: 32 additions & 0 deletions app/Console/ScheduledCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace App\Console;

use Tempest\Console\ConsoleCommand;
use Tempest\Console\ConsoleOutput;
use Tempest\Console\Scheduler\Every;
use Tempest\Console\Scheduler\Schedule;

final class ScheduledCommand
{
public function __construct(
protected ConsoleOutput $output,
) {

}

#[Schedule(Every::Second)]
#[ConsoleCommand('scheduled')]
public function command(): void
{
$this->output->writeln('A command got scheduled');
}

#[Schedule(Every::Second)]
public function method(): void
{
$this->output->writeln('A method got scheduled');
}
}
22 changes: 22 additions & 0 deletions src/Commands/SchedulerRunCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Commands;

use Tempest\Console\ConsoleCommand;
use Tempest\Console\Scheduler\Scheduler;

final class SchedulerRunCommand
{
public function __construct(
private Scheduler $scheduler,
) {
}

#[ConsoleCommand('scheduler:run')]
brendt marked this conversation as resolved.
Show resolved Hide resolved
public function __invoke(): void
{
$this->scheduler->run();
}
}
35 changes: 35 additions & 0 deletions src/Commands/SchedulerRunInvocationCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Commands;

use ReflectionMethod;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\ConsoleOutput;
use Tempest\Container\Container;

final class SchedulerRunInvocationCommand
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should pick a proper name for "something that's run in the background", especially if we plan to add advanced monitoring tools later (horizon-like).

  • Task
  • BackgroundTask
  • Process
  • BackgroundProcess

Those are the names I can come up with. I like Task the most, but I'm not sure whether it's too ambiguous.

{
public const string NAME = 'scheduler:invoke';

public function __construct(
private Container $container,
private ConsoleOutput $output,
) {
}

#[ConsoleCommand(self::NAME)]
public function __invoke(string $invocation): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there's a better way to invoke the task. I'll play around with it once it's merged

{
$this->output->info("Invoking $invocation");

Check failure on line 25 in src/Commands/SchedulerRunInvocationCommand.php

View workflow job for this annotation

GitHub Actions / Perform Static Analysis

Call to an undefined method Tempest\Console\ConsoleOutput::info().

[$class, $method] = explode('::', $invocation);

$reflectionMethod = new ReflectionMethod($class, $method);

$reflectionMethod->invoke(
$this->container->get($class),
);
}
}
7 changes: 7 additions & 0 deletions src/Config/scheduler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

use Tempest\Console\Scheduler\SchedulerConfig;

return new SchedulerConfig();
5 changes: 3 additions & 2 deletions src/ConsoleCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

use Attribute;
use ReflectionMethod;
use Tempest\Console\Scheduler\Invocation;

#[Attribute]
final class ConsoleCommand
final class ConsoleCommand implements Invocation
{
public ReflectionMethod $handler;

Expand Down Expand Up @@ -64,7 +65,7 @@ public function __unserialize(array $data): void
}

/**
* @return \Tempest\Console\ConsoleArgumentDefinition[]
* @return ConsoleArgumentDefinition[]
*/
public function getArgumentDefinitions(): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/ConsoleConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ final class ConsoleConfig
public function __construct(
public string $name = 'Tempest',

/** @var \Tempest\Console\ConsoleCommand[] $commands */
/** @var ConsoleCommand[] $commands */
public array $commands = [],
) {
}
Expand Down
64 changes: 64 additions & 0 deletions src/Discovery/ScheduleDisovery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Discovery;

use ReflectionClass;
use ReflectionMethod;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\Scheduler\Schedule;
use Tempest\Console\Scheduler\SchedulerConfig;
use Tempest\Container\Container;
use Tempest\Discovery\Discovery;
use Tempest\Support\Reflection\Attributes;

final class ScheduleDisovery implements Discovery
{
private const CACHE_PATH = __DIR__ . '/schedule-discovery.cache.php';

public function __construct(private SchedulerConfig $schedulerConfig)
{
}

public function discover(ReflectionClass $class): void
brendt marked this conversation as resolved.
Show resolved Hide resolved
{
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$schedule = Attributes::find(Schedule::class)->in($method)->first();

if ($schedule === null) {
continue;
}

$command = Attributes::find(ConsoleCommand::class)->in($method)->first();

if ($command) {
$this->schedulerConfig->addCommandInvocation($method, $command, $schedule);
} else {
$this->schedulerConfig->addHandlerInvocation($method, $schedule);
}
}
}

public function hasCache(): bool
{
return file_exists(self::CACHE_PATH);
}

public function storeCache(): void
{
file_put_contents(self::CACHE_PATH, serialize($this->schedulerConfig->scheduledInvocations));
}

public function restoreCache(Container $container): void
{
$scheduledInvocations = unserialize(file_get_contents(self::CACHE_PATH));

$this->schedulerConfig->scheduledInvocations = $scheduledInvocations;
}

public function destroyCache(): void
{
@unlink(self::CACHE_PATH);
}
}
31 changes: 31 additions & 0 deletions src/Inititalizers/InvocationExecutorInitializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Inititalizers;

use Tempest\Application;
use Tempest\Console\ConsoleApplication;
use Tempest\Console\Scheduler\GenericInvocationExecutor;
use Tempest\Console\Scheduler\NullInvocationExecutor;
use Tempest\Console\Scheduler\ScheduledInvocationExecutor;
use Tempest\Container\Container;
use Tempest\Container\Initializer;
use Tempest\Container\Singleton;

#[Singleton]
final readonly class InvocationExecutorInitializer implements Initializer
{
public function initialize(Container $container): ScheduledInvocationExecutor
{
$app = $container->get(Application::class);

if (! $app instanceof ConsoleApplication) {
$executor = new NullInvocationExecutor();
} else {
$executor = new GenericInvocationExecutor();
}

return $executor;
}
}
36 changes: 36 additions & 0 deletions src/Inititalizers/SchedulerInitializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Inititalizers;

use Tempest\Application;
use Tempest\Console\ConsoleApplication;
use Tempest\Console\Scheduler\GenericScheduler;
use Tempest\Console\Scheduler\NullScheduler;
use Tempest\Console\Scheduler\ScheduledInvocationExecutor;
use Tempest\Console\Scheduler\Scheduler;
use Tempest\Console\Scheduler\SchedulerConfig;
use Tempest\Container\Container;
use Tempest\Container\Initializer;
use Tempest\Container\Singleton;

#[Singleton]
final readonly class SchedulerInitializer implements Initializer
{
public function initialize(Container $container): Scheduler
{
$app = $container->get(Application::class);

if (! $app instanceof ConsoleApplication) {
$consoleInput = new NullScheduler();
brendt marked this conversation as resolved.
Show resolved Hide resolved
} else {
$consoleInput = new GenericScheduler(
$container->get(SchedulerConfig::class),
$container->get(ScheduledInvocationExecutor::class),
);
}

return $consoleInput;
}
}
35 changes: 35 additions & 0 deletions src/Scheduler/Every.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Scheduler;

enum Every
{
case Second;
brendt marked this conversation as resolved.
Show resolved Hide resolved
case Minute;
case QuarterHour;
case HalfHour;
case Hour;
case HalfDay;
case Day;
case Week;
case Month;
case Year;

public function toInterval(): Interval
{
return match ($this) {
self::Second => new Interval(seconds: 1),
self::Minute => new Interval(minutes: 1),
self::QuarterHour => new Interval(minutes: 15),
self::HalfHour => new Interval(minutes: 30),
self::Hour => new Interval(hours: 1),
self::HalfDay => new Interval(hours: 12),
self::Day => new Interval(days: 1),
self::Week => new Interval(weeks: 1),
self::Month => new Interval(months: 1),
self::Year => new Interval(years: 1),
};
}
}
13 changes: 13 additions & 0 deletions src/Scheduler/GenericInvocationExecutor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Tempest\Console\Scheduler;

final class GenericInvocationExecutor implements ScheduledInvocationExecutor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I want to rework the whole invocation structure. Instead of going through the shell again, we can build the command, and then pass it to the ConsoleApplication without having to boot tempest once more.

This is something I'll refactor after merge though, as it might involve some bigger changes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: maybe it won't be possible, because we want to run stuff in the background. Ok, I'll just play around with it and see where it leads me

{
public function execute(string $compiledCommand): void
{
shell_exec($compiledCommand);
}
}
Loading
Loading