Skip to content

Commit f103cb4

Browse files
committed
Merge branch '10.x'
2 parents 955e60e + 0672e3f commit f103cb4

File tree

10 files changed

+296
-73
lines changed

10 files changed

+296
-73
lines changed

src/Illuminate/Console/CacheCommandMutex.php

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
namespace Illuminate\Console;
44

55
use Carbon\CarbonInterval;
6+
use Illuminate\Cache\DynamoDbStore;
67
use Illuminate\Contracts\Cache\Factory as Cache;
8+
use Illuminate\Contracts\Cache\LockProvider;
9+
use Illuminate\Support\InteractsWithTime;
710

811
class CacheCommandMutex implements CommandMutex
912
{
13+
use InteractsWithTime;
14+
1015
/**
1116
* The cache factory implementation.
1217
*
@@ -39,13 +44,20 @@ public function __construct(Cache $cache)
3944
*/
4045
public function create($command)
4146
{
42-
return $this->cache->store($this->store)->add(
43-
$this->commandMutexName($command),
44-
true,
45-
method_exists($command, 'isolationLockExpiresAt')
46-
? $command->isolationLockExpiresAt()
47-
: CarbonInterval::hour(),
48-
);
47+
$store = $this->cache->store($this->store);
48+
49+
$expiresAt = method_exists($command, 'isolationLockExpiresAt')
50+
? $command->isolationLockExpiresAt()
51+
: CarbonInterval::hour();
52+
53+
if ($this->shouldUseLocks($store->getStore())) {
54+
return $store->getStore()->lock(
55+
$this->commandMutexName($command),
56+
$this->secondsUntil($expiresAt)
57+
)->get();
58+
}
59+
60+
return $store->add($this->commandMutexName($command), true, $expiresAt);
4961
}
5062

5163
/**
@@ -56,9 +68,19 @@ public function create($command)
5668
*/
5769
public function exists($command)
5870
{
59-
return $this->cache->store($this->store)->has(
60-
$this->commandMutexName($command)
61-
);
71+
$store = $this->cache->store($this->store);
72+
73+
if ($this->shouldUseLocks($store->getStore())) {
74+
$lock = $store->getStore()->lock($this->commandMutexName($command));
75+
76+
return tap(! $lock->get(), function ($exists) use ($lock) {
77+
if ($exists) {
78+
$lock->release();
79+
}
80+
});
81+
}
82+
83+
return $this->cache->store($this->store)->has($this->commandMutexName($command));
6284
}
6385

6486
/**
@@ -69,9 +91,13 @@ public function exists($command)
6991
*/
7092
public function forget($command)
7193
{
72-
return $this->cache->store($this->store)->forget(
73-
$this->commandMutexName($command)
74-
);
94+
$store = $this->cache->store($this->store);
95+
96+
if ($this->shouldUseLocks($store->getStore())) {
97+
return $store->getStore()->lock($this->commandMutexName($command))->forceRelease();
98+
}
99+
100+
return $this->cache->store($this->store)->forget($this->commandMutexName($command));
75101
}
76102

77103
/**
@@ -95,4 +121,15 @@ public function useStore($store)
95121

96122
return $this;
97123
}
124+
125+
/**
126+
* Determine if the given store should use locks for command mutexes.
127+
*
128+
* @param \Illuminate\Contracts\Cache\Store $store
129+
* @return bool
130+
*/
131+
protected function shouldUseLocks($store)
132+
{
133+
return $store instanceof LockProvider && ! $store instanceof DynamoDbStore;
134+
}
98135
}

src/Illuminate/Console/Scheduling/ScheduleListCommand.php

Lines changed: 119 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -62,87 +62,130 @@ public function handle(Schedule $schedule)
6262

6363
$expressionSpacing = $this->getCronExpressionSpacing($events);
6464

65+
$repeatExpressionSpacing = $this->getRepeatExpressionSpacing($events);
66+
6567
$timezone = new DateTimeZone($this->option('timezone') ?? config('app.timezone'));
6668

6769
$events = $this->sortEvents($events, $timezone);
6870

69-
$events = $events->map(function ($event) use ($terminalWidth, $expressionSpacing, $timezone) {
70-
$expression = $this->formatCronExpression($event->expression, $expressionSpacing);
71+
$events = $events->map(function ($event) use ($terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone) {
72+
return $this->listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone);
73+
});
7174

72-
$command = $event->command ?? '';
75+
$this->line(
76+
$events->flatten()->filter()->prepend('')->push('')->toArray()
77+
);
78+
}
7379

74-
$description = $event->description ?? '';
80+
/**
81+
* Get the spacing to be used on each event row.
82+
*
83+
* @param \Illuminate\Support\Collection $events
84+
* @return array<int, int>
85+
*/
86+
private function getCronExpressionSpacing($events)
87+
{
88+
$rows = $events->map(fn ($event) => array_map('mb_strlen', preg_split("/\s+/", $event->expression)));
7589

76-
if (! $this->output->isVerbose()) {
77-
$command = str_replace([Application::phpBinary(), Application::artisanBinary()], [
78-
'php',
79-
preg_replace("#['\"]#", '', Application::artisanBinary()),
80-
], $command);
81-
}
90+
return collect($rows[0] ?? [])->keys()->map(fn ($key) => $rows->max($key))->all();
91+
}
8292

83-
if ($event instanceof CallbackEvent) {
84-
$command = $event->getSummaryForDisplay();
93+
/**
94+
* Get the spacing to be used on each event row.
95+
*
96+
* @param \Illuminate\Support\Collection $events
97+
* @return int
98+
*/
99+
private function getRepeatExpressionSpacing($events)
100+
{
101+
return $events->map(fn ($event) => mb_strlen($this->getRepeatExpression($event)))->max();
102+
}
85103

86-
if (in_array($command, ['Closure', 'Callback'])) {
87-
$command = 'Closure at: '.$this->getClosureLocation($event);
88-
}
89-
}
104+
/**
105+
* List the given even in the console.
106+
*
107+
* @param \Illuminate\Console\Scheduling\Event
108+
* @param int $terminalWidth
109+
* @param array $expressionSpacing
110+
* @param int $repeatExpressionSpacing
111+
* @param array $repeatExpressionSpacing
112+
* @param \DateTimeZone $timezone
113+
* @return \Illuminate\Support\DateTimeZone
114+
*/
115+
private function listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone)
116+
{
117+
$expression = $this->formatCronExpression($event->expression, $expressionSpacing);
90118

91-
$command = mb_strlen($command) > 1 ? "{$command} " : '';
119+
$repeatExpression = str_pad($this->getRepeatExpression($event), $repeatExpressionSpacing);
92120

93-
$nextDueDateLabel = 'Next Due:';
121+
$command = $event->command ?? '';
94122

95-
$nextDueDate = $this->getNextDueDateForEvent($event, $timezone);
123+
$description = $event->description ?? '';
96124

97-
$nextDueDate = $this->output->isVerbose()
98-
? $nextDueDate->format('Y-m-d H:i:s P')
99-
: $nextDueDate->diffForHumans();
125+
if (! $this->output->isVerbose()) {
126+
$command = str_replace([Application::phpBinary(), Application::artisanBinary()], [
127+
'php',
128+
preg_replace("#['\"]#", '', Application::artisanBinary()),
129+
], $command);
130+
}
100131

101-
$hasMutex = $event->mutex->exists($event) ? 'Has Mutex › ' : '';
132+
if ($event instanceof CallbackEvent) {
133+
$command = $event->getSummaryForDisplay();
102134

103-
$dots = str_repeat('.', max(
104-
$terminalWidth - mb_strlen($expression.$command.$nextDueDateLabel.$nextDueDate.$hasMutex) - 8, 0
105-
));
135+
if (in_array($command, ['Closure', 'Callback'])) {
136+
$command = 'Closure at: '.$this->getClosureLocation($event);
137+
}
138+
}
106139

107-
// Highlight the parameters...
108-
$command = preg_replace("#(php artisan [\w\-:]+) (.+)#", '$1 <fg=yellow;options=bold>$2</>', $command);
140+
$command = mb_strlen($command) > 1 ? "{$command} " : '';
109141

110-
return [sprintf(
111-
' <fg=yellow>%s</> %s<fg=#6C7280>%s %s%s %s</>',
112-
$expression,
113-
$command,
114-
$dots,
115-
$hasMutex,
116-
$nextDueDateLabel,
117-
$nextDueDate
118-
), $this->output->isVerbose() && mb_strlen($description) > 1 ? sprintf(
119-
' <fg=#6C7280>%s%s %s</>',
120-
str_repeat(' ', mb_strlen($expression) + 2),
121-
'',
122-
$description
123-
) : ''];
124-
});
142+
$nextDueDateLabel = 'Next Due:';
125143

126-
$this->line(
127-
$events->flatten()->filter()->prepend('')->push('')->toArray()
128-
);
144+
$nextDueDate = $this->getNextDueDateForEvent($event, $timezone);
145+
146+
$nextDueDate = $this->output->isVerbose()
147+
? $nextDueDate->format('Y-m-d H:i:s P')
148+
: $nextDueDate->diffForHumans();
149+
150+
$hasMutex = $event->mutex->exists($event) ? 'Has Mutex › ' : '';
151+
152+
$dots = str_repeat('.', max(
153+
$terminalWidth - mb_strlen($expression.$repeatExpression.$command.$nextDueDateLabel.$nextDueDate.$hasMutex) - 8, 0
154+
));
155+
156+
// Highlight the parameters...
157+
$command = preg_replace("#(php artisan [\w\-:]+) (.+)#", '$1 <fg=yellow;options=bold>$2</>', $command);
158+
159+
return [sprintf(
160+
' <fg=yellow>%s</> <fg=#6C7280>%s</> %s<fg=#6C7280>%s %s%s %s</>',
161+
$expression,
162+
$repeatExpression,
163+
$command,
164+
$dots,
165+
$hasMutex,
166+
$nextDueDateLabel,
167+
$nextDueDate
168+
), $this->output->isVerbose() && mb_strlen($description) > 1 ? sprintf(
169+
' <fg=#6C7280>%s%s %s</>',
170+
str_repeat(' ', mb_strlen($expression) + 2),
171+
'',
172+
$description
173+
) : ''];
129174
}
130175

131176
/**
132-
* Gets the spacing to be used on each event row.
177+
* Get the repeat expression for an event.
133178
*
134-
* @param \Illuminate\Support\Collection $events
135-
* @return array<int, int>
179+
* @param \Illuminate\Console\Scheduling\Event $event
180+
* @return string
136181
*/
137-
private function getCronExpressionSpacing($events)
182+
private function getRepeatExpression($event)
138183
{
139-
$rows = $events->map(fn ($event) => array_map('mb_strlen', preg_split("/\s+/", $event->expression)));
140-
141-
return collect($rows[0] ?? [])->keys()->map(fn ($key) => $rows->max($key))->all();
184+
return $event->isRepeatable() ? "{$event->repeatSeconds}s " : '';
142185
}
143186

144187
/**
145-
* Sorts the events by due date if option set.
188+
* Sort the events by due date if option set.
146189
*
147190
* @param \Illuminate\Support\Collection $events
148191
* @param \DateTimeZone $timezone
@@ -164,15 +207,35 @@ private function sortEvents(\Illuminate\Support\Collection $events, DateTimeZone
164207
*/
165208
private function getNextDueDateForEvent($event, DateTimeZone $timezone)
166209
{
167-
return Carbon::instance(
210+
$nextDueDate = Carbon::instance(
168211
(new CronExpression($event->expression))
169212
->getNextRunDate(Carbon::now()->setTimezone($event->timezone))
170213
->setTimezone($timezone)
171214
);
215+
216+
if (! $event->isRepeatable()) {
217+
return $nextDueDate;
218+
}
219+
220+
$previousDueDate = Carbon::instance(
221+
(new CronExpression($event->expression))
222+
->getPreviousRunDate(Carbon::now()->setTimezone($event->timezone), allowCurrentDate: true)
223+
->setTimezone($timezone)
224+
);
225+
226+
$now = Carbon::now()->setTimezone($event->timezone);
227+
228+
if (! $now->copy()->startOfMinute()->eq($previousDueDate)) {
229+
return $nextDueDate;
230+
}
231+
232+
return $now
233+
->endOfSecond()
234+
->ceilSeconds($event->repeatSeconds);
172235
}
173236

174237
/**
175-
* Formats the cron expression based on the spacing provided.
238+
* Format the cron expression based on the spacing provided.
176239
*
177240
* @param string $expression
178241
* @param array<int, int> $spacing

src/Illuminate/Database/DetectsLostConnections.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ protected function causedByLostConnection(Throwable $e)
6565
'SSL: Handshake timed out',
6666
'SQLSTATE[08006] [7] SSL error: sslv3 alert unexpected message',
6767
'SQLSTATE[08006] [7] unrecognized SSL error code:',
68+
'SQLSTATE[HY000] [2002] No connection could be made because the target machine actively refused it',
6869
]);
6970
}
7071
}

src/Illuminate/Database/Eloquent/Model.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1826,7 +1826,7 @@ public static function resolveConnection($connection = null)
18261826
/**
18271827
* Get the connection resolver instance.
18281828
*
1829-
* @return \Illuminate\Database\ConnectionResolverInterface
1829+
* @return \Illuminate\Database\ConnectionResolverInterface|null
18301830
*/
18311831
public static function getConnectionResolver()
18321832
{

src/Illuminate/Foundation/Console/Kernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ protected function getArtisan()
484484

485485
if ($this->symfonyDispatcher instanceof EventDispatcher) {
486486
$this->artisan->setDispatcher($this->symfonyDispatcher);
487+
$this->artisan->setSignalsToDispatchEvent();
487488
}
488489
}
489490

src/Illuminate/Mail/Mailable.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,6 +1330,8 @@ private function formatAssertionRecipient($address, $name = null)
13301330
*/
13311331
public function assertHasSubject($subject)
13321332
{
1333+
$this->renderForAssertions();
1334+
13331335
PHPUnit::assertTrue(
13341336
$this->hasSubject($subject),
13351337
"Did not see expected text [{$subject}] in email subject."

src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ protected function handleRequest($request, Closure $next, array $limits)
8686
protected function tooManyAttempts($key, $maxAttempts, $decayMinutes)
8787
{
8888
$limiter = new DurationLimiter(
89-
$this->redis, $key, $maxAttempts, $decayMinutes * 60
89+
$this->getRedisConnection(), $key, $maxAttempts, $decayMinutes * 60
9090
);
9191

9292
return tap(! $limiter->acquire(), function () use ($key, $limiter) {
@@ -119,4 +119,14 @@ protected function getTimeUntilNextRetry($key)
119119
{
120120
return $this->decaysAt[$key] - $this->currentTime();
121121
}
122+
123+
/**
124+
* Get the Redis connection that should be used for throttling.
125+
*
126+
* @return \Illuminate\Redis\Connections\Connection
127+
*/
128+
protected function getRedisConnection()
129+
{
130+
return $this->redis->connection();
131+
}
122132
}

0 commit comments

Comments
 (0)