forInstance( $subscription->instance ?? $subscription->customer?->instances()->latest('id')->first() ); } /** * Start a storage run for this instance, or return null when there is * nothing to start. * * Null is the ordinary answer and not a failure: a machine still being * built, one whose VM has gone, one whose service has ended, or one that * already has a run in flight all deliver the allowance from that run * instead — every pipeline that touches storage reads the allowance when it * gets there, which is this state or newer. */ public function forInstance(?Instance $instance): ?ProvisioningRun { if ($instance === null || ! $instance->hasLiveMachine()) { return null; } // The same lock ReapplyInstanceAddress and RestartInstance take, for the // same reason: two bookings can land in one instant — a double click, a // webhook delivered twice, an operator granting a pack while the // customer buys one — and two runs against one machine would resize the // same disk and race each other's occ calls. Nobody waits for it: a run // that lost the race has nothing to add, because the one that won reads // the same allowance. $lock = Cache::lock('instance-storage:'.$instance->uuid, 30); if (! $lock->get()) { return null; } try { return $this->start($instance); } finally { $lock->release(); } } private function start(Instance $instance): ?ProvisioningRun { $order = $instance->order; // Checked against the ORDER, because that is the subject every customer // pipeline shares. A storage run beside an unfinished build would resize // a disk the build is still writing; one beside a plan change would // resize it twice from two different figures. Whatever is in flight // reads the allowance itself when it reaches the quota step, so nothing // is lost by standing aside. if ($this->hasRunInFlight($order)) { return null; } $run = ProvisioningRun::create([ 'subject_type' => Order::class, 'subject_id' => $order->id, 'pipeline' => 'storage', 'status' => ProvisioningRun::STATUS_PENDING, 'current_step' => 0, // Everything the three steps read: instance_id is how CustomerStep // finds the machine, node and vmid are how it reaches inside it. 'context' => [ 'instance_id' => $instance->id, 'host_id' => $instance->host_id, 'node' => $instance->host?->node, 'vmid' => $instance->vmid, 'subdomain' => $instance->subdomain, ], ]); AdvanceRunJob::dispatch($run->uuid); Log::info('Delivering a changed storage allowance.', [ 'instance' => $instance->uuid, 'run' => $run->uuid, ]); return $run; } private function hasRunInFlight(Order $order): bool { return ProvisioningRun::query() ->where('subject_type', Order::class) ->where('subject_id', $order->id) ->whereIn('status', [ ProvisioningRun::STATUS_PENDING, ProvisioningRun::STATUS_RUNNING, ProvisioningRun::STATUS_WAITING, ProvisioningRun::STATUS_PAUSED, ]) ->exists(); } }