177 lines
7.8 KiB
PHP
177 lines
7.8 KiB
PHP
<?php
|
|
|
|
use App\Console\TickProvisioning;
|
|
use App\Models\StripePendingEvent;
|
|
use App\Provisioning\Jobs\CollectInstanceTraffic;
|
|
use App\Provisioning\Jobs\PingHosts;
|
|
use App\Provisioning\Jobs\SyncMonitoringStatus;
|
|
use App\Provisioning\Jobs\SyncVpnPeers;
|
|
use Illuminate\Foundation\Inspiring;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\Schedule;
|
|
|
|
Artisan::command('inspire', function () {
|
|
$this->comment(Inspiring::quote());
|
|
})->purpose('Display an inspiring quote');
|
|
|
|
// Advance every due provisioning run once a minute (runs in the scheduler service).
|
|
Schedule::call(fn () => app(TickProvisioning::class)())
|
|
->everyMinute()
|
|
->name('provisioning-tick')
|
|
->withoutOverlapping();
|
|
|
|
// Refresh the VPN peer state (handshakes, traffic) so the console does not go
|
|
// stale while nobody has it open. The job itself runs on the provisioning
|
|
// queue — that worker owns wg0.
|
|
Schedule::job(new SyncVpnPeers)
|
|
->everyMinute()
|
|
->name('vpn-sync');
|
|
|
|
// Sample network counters and enforce the monthly allowance. Runs on the
|
|
// provisioning queue, which is where the Proxmox credentials are usable.
|
|
// Turns the monitoring column from a claim into a measurement. Without it the
|
|
// console and the public status page report every instance healthy forever.
|
|
Schedule::job(new SyncMonitoringStatus)
|
|
->everyFiveMinutes()
|
|
->withoutOverlapping();
|
|
|
|
Schedule::job(new CollectInstanceTraffic)
|
|
->everyFifteenMinutes()
|
|
->name('traffic-collect');
|
|
|
|
// Discard Stripe events that were held for a contract which never appeared.
|
|
// Most of them never will: Stripe also delivers events for subscriptions
|
|
// belonging to other environments pointed at this endpoint, and for objects
|
|
// made by hand in the dashboard. A week is far longer than the moment a
|
|
// checkout takes to become a contract, and short enough that the table stays
|
|
// a holding area rather than a second copy of Stripe's event log.
|
|
Schedule::call(fn () => StripePendingEvent::query()
|
|
->where('created_at', '<', now()->subWeek())
|
|
->delete())
|
|
->daily()
|
|
->name('stripe-pending-prune');
|
|
|
|
// Copies of invoices that never reached the archive — a NAS that was rebooting
|
|
// when the invoice was issued, a mount that had gone away. Copy-on-issue is
|
|
// right and is not enough: nobody opens an archive to check whether last
|
|
// Tuesday is in it, so something has to go and look.
|
|
//
|
|
// Hourly rather than by the minute: the failure it repairs lasts as long as an
|
|
// outage lasts, and a retry storm against a mount that is down helps nobody.
|
|
// The support mailbox. Polling, because IMAP is a mailbox you look in — a mail
|
|
// server has no way to tell us something arrived — so the interval IS the
|
|
// answer to "how long until I see it". Two minutes is short enough that an
|
|
// operator watching the inbox does not reload it by hand, and long enough that
|
|
// a mail server is not being asked once a second for nothing.
|
|
//
|
|
// withoutOverlapping: a slow or unreachable mailbox must not stack runs on top
|
|
// of each other until a worker is doing nothing but timing out.
|
|
Schedule::command('clupilot:fetch-mail')
|
|
->everyTwoMinutes()
|
|
->withoutOverlapping();
|
|
|
|
Schedule::command('clupilot:archive-invoices')
|
|
->hourly()
|
|
->withoutOverlapping();
|
|
|
|
// Plan changes that landed here and never reached Stripe. The change is applied
|
|
// to the contract and to the machine before Stripe is told, and it is not rolled
|
|
// back when Stripe is away — so what is left is a customer being billed for a
|
|
// package they are no longer on, and nobody would ever go and look.
|
|
//
|
|
// Hourly, like the archive sweep and for the same reason: the failure it repairs
|
|
// lasts as long as an outage lasts, and hammering an API that is down helps
|
|
// nobody.
|
|
Schedule::command('clupilot:sync-stripe-subscriptions')
|
|
->hourly()
|
|
->withoutOverlapping();
|
|
|
|
// Empty a handover directory of folders past their keep-by. Only where a
|
|
// destination asks for it, and only folders whose name is a date this
|
|
// application wrote — see the command for why it leaves everything else alone.
|
|
Schedule::command('clupilot:prune-exports')
|
|
->dailyAt('03:20')
|
|
->withoutOverlapping();
|
|
|
|
// One sample of every public status component, into today's row.
|
|
//
|
|
// The ninety-day bar on the status page is the one figure a reader checks
|
|
// against their own memory, so it has to come from something recorded at the
|
|
// time — there is no way to reconstruct it later. Beside the monitoring sync it
|
|
// reads from, at the same cadence.
|
|
Schedule::command('clupilot:sample-status')
|
|
->everyFiveMinutes()
|
|
->withoutOverlapping();
|
|
|
|
// Re-read the DNS proof for every custom domain.
|
|
//
|
|
// Not a one-off: a token checked once can be taken straight back out, and a
|
|
// domain that later lapses and is registered by somebody else keeps resolving
|
|
// to the same instance. Nightly, and at an hour where withdrawing a domain
|
|
// inconveniences nobody — a withdrawal takes a working Nextcloud off its own
|
|
// address, which is not a thing to do at eleven in the morning.
|
|
Schedule::command('clupilot:verify-domains')
|
|
->dailyAt('03:40')
|
|
->withoutOverlapping();
|
|
|
|
// Take an available release inside the configured window, if the owner has
|
|
// switched that on. Every five minutes rather than once at the start of the
|
|
// window: a scheduler that was down for those sixty seconds would otherwise
|
|
// skip the whole night.
|
|
Schedule::command('clupilot:auto-update')
|
|
->everyFiveMinutes()
|
|
->withoutOverlapping();
|
|
|
|
// Ask every host whether it is still there.
|
|
//
|
|
// hosts.last_seen_at drove the console's health dot and was written exactly
|
|
// once, at onboarding — so every host read "offline" half an hour later,
|
|
// permanently. Nothing was measuring host reachability at all, which is also
|
|
// why it could not appear on the status page.
|
|
Schedule::job(new PingHosts)
|
|
->everyMinute()
|
|
->name('host-ping');
|
|
|
|
// Carry out the downgrades whose term has run out.
|
|
//
|
|
// An upgrade lands the moment it is paid for; a downgrade waits, because
|
|
// somebody on a yearly contract bought a year. Nothing was waiting with it — the
|
|
// order sat in the cart forever and the customer stayed on the bigger package.
|
|
//
|
|
// Every quarter of an hour rather than nightly: a term ends at the second it
|
|
// ends, and a customer who has asked to pay less should not spend another
|
|
// afternoon on the old price. Nothing is taken away that they did not ask to
|
|
// give up, so there is no bad hour for it.
|
|
Schedule::command('clupilot:apply-due-plan-changes')
|
|
->everyFifteenMinutes()
|
|
->withoutOverlapping();
|
|
|
|
// End the modules whose cancellation date has arrived.
|
|
//
|
|
// The same rule the downgrade above follows: a module cancelled on the tenth is
|
|
// kept until the term it was paid for runs out. Stripe stopped billing it at the
|
|
// moment of cancellation — its item is only ever about the NEXT cycle — so what
|
|
// waits for this sweep is the module itself: the entitlement, the space on the
|
|
// disk, and the entry in the register saying it ended.
|
|
//
|
|
// Beside the downgrades and at the same cadence, because it is the same
|
|
// appointment being kept.
|
|
Schedule::command('clupilot:end-cancelled-addons')
|
|
->everyFifteenMinutes()
|
|
->withoutOverlapping();
|
|
|
|
// Keep the appointment a cancellation made.
|
|
//
|
|
// ConfirmCancelPackage writes a date and nothing ever went back to it, which is
|
|
// why TraefikWriter::remove() had no caller in the whole application: a
|
|
// cancelled instance kept its router, its certificate and its route to a guest
|
|
// address the host is free to reassign. Only instances whose paid term has
|
|
// actually run out are touched — a cancellation that is merely SCHEDULED is a
|
|
// paying customer, and this command is written around that distinction.
|
|
//
|
|
// Hourly: a term ends at a moment, and neither giving away a day of service nor
|
|
// hammering a DNS provider by the minute is a decision worth making by accident.
|
|
Schedule::command('clupilot:end-due-services')
|
|
->hourly()
|
|
->withoutOverlapping();
|