fix: withoutOverlapping on schedules, publisher token refresh lock, PublishPost tries:1, TokenExpired allows publish, API validation scoping
This commit is contained in:
parent
9d0a860d87
commit
a9184c9401
16 changed files with 47 additions and 24 deletions
|
|
@ -20,7 +20,7 @@ public function rules(): array
|
|||
{
|
||||
return [
|
||||
'platforms' => ['required', 'array', 'min:1'],
|
||||
'platforms.*.social_account_id' => ['required', 'uuid'],
|
||||
'platforms.*.social_account_id' => ['required', 'uuid', Rule::exists('social_accounts', 'id')->where('workspace_id', $this->workspace->id)],
|
||||
'platforms.*.content_type' => ['required', 'string', Rule::in(array_column(ContentType::cases(), 'value'))],
|
||||
'platforms.*.content' => ['nullable', 'string', 'max:63206'],
|
||||
'scheduled_at' => ['nullable', 'date', 'after:now'],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use App\Enums\Post\Status;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ public function rules(): array
|
|||
'status' => ['required', 'string', Rule::in(array_column(Status::cases(), 'value'))],
|
||||
'synced' => ['required', 'boolean'],
|
||||
'platforms' => ['required', 'array'],
|
||||
'platforms.*.id' => ['required', 'uuid'],
|
||||
'platforms.*.id' => ['required', 'uuid', Rule::exists('post_platforms', 'id')->where('post_id', $this->route('post') instanceof Post ? $this->route('post')->id : $this->route('post'))],
|
||||
'platforms.*.content' => ['nullable', 'string', 'max:63206'],
|
||||
'platforms.*.content_type' => ['required', 'string', Rule::in(array_column(ContentType::cases(), 'value'))],
|
||||
'platforms.*.meta' => ['nullable', 'array'],
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ class PublishPost implements ShouldQueue
|
|||
{
|
||||
use Queueable;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
public int $backoff = 30;
|
||||
public int $tries = 1;
|
||||
|
||||
public function __construct(public Post $post) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
use App\Enums\Notification\Type;
|
||||
use App\Enums\PostPlatform\Status as PostPlatformStatus;
|
||||
use App\Enums\SocialAccount\Platform as SocialPlatform;
|
||||
use App\Enums\SocialAccount\Status;
|
||||
use App\Events\PostPlatformStatusUpdated;
|
||||
use App\Exceptions\Social\SocialPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
|
|
@ -61,7 +62,7 @@ public function handle(): void
|
|||
return;
|
||||
}
|
||||
|
||||
if ($this->postPlatform->socialAccount->isDisconnected()) {
|
||||
if ($this->postPlatform->socialAccount->status === Status::Disconnected) {
|
||||
$this->postPlatform->markAsFailed(__('posts.errors.account_disconnected'));
|
||||
$this->updatePostStatus();
|
||||
$this->broadcastStatus();
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public function publish(PostPlatform $postPlatform): array
|
|||
|
||||
// Refresh token if needed
|
||||
if ($account->is_token_expired || $account->is_token_expiring_soon) {
|
||||
$this->refreshToken($account);
|
||||
$this->refreshTokenWithLock($account, fn () => $this->refreshToken($account));
|
||||
$account->refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
namespace App\Services\Social\Concerns;
|
||||
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
trait HasSocialHttpClient
|
||||
|
|
@ -22,6 +24,25 @@ protected function validateContentLength(PostPlatform $postPlatform): void
|
|||
}
|
||||
}
|
||||
|
||||
protected function refreshTokenWithLock(SocialAccount $account, callable $refreshFn): void
|
||||
{
|
||||
$lock = Cache::lock("token_refresh:{$account->id}", 30);
|
||||
|
||||
if (! $lock->get()) {
|
||||
// Another process is refreshing, wait and reload
|
||||
sleep(2);
|
||||
$account->refresh();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$refreshFn();
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
}
|
||||
|
||||
protected function socialHttp(): PendingRequest
|
||||
{
|
||||
return Http::retry(
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public function publish(PostPlatform $postPlatform): array
|
|||
$account = $postPlatform->socialAccount;
|
||||
|
||||
if ($account->is_token_expired || $account->is_token_expiring_soon) {
|
||||
$this->refreshToken($account);
|
||||
$this->refreshTokenWithLock($account, fn () => $this->refreshToken($account));
|
||||
$account->refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public function publish(PostPlatform $postPlatform): array
|
|||
$this->hasRetried = false;
|
||||
|
||||
if ($this->account->is_token_expired || $this->account->is_token_expiring_soon) {
|
||||
$this->refreshToken($this->account);
|
||||
$this->refreshTokenWithLock($this->account, fn () => $this->refreshToken($this->account));
|
||||
$this->account->refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public function publish(PostPlatform $postPlatform): array
|
|||
$this->hasRetried = false;
|
||||
|
||||
if ($this->account->is_token_expired || $this->account->is_token_expiring_soon) {
|
||||
$this->refreshToken($this->account);
|
||||
$this->refreshTokenWithLock($this->account, fn () => $this->refreshToken($this->account));
|
||||
$this->account->refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public function publish(PostPlatform $postPlatform): array
|
|||
$account = $postPlatform->socialAccount;
|
||||
|
||||
if ($account->is_token_expired || $account->is_token_expiring_soon) {
|
||||
$this->refreshToken($account);
|
||||
$this->refreshTokenWithLock($account, fn () => $this->refreshToken($account));
|
||||
$account->refresh();
|
||||
}
|
||||
|
||||
|
|
@ -383,7 +383,7 @@ private function refreshToken(SocialAccount $account): void
|
|||
public function getBoards(SocialAccount $account): array
|
||||
{
|
||||
if ($account->is_token_expired || $account->is_token_expiring_soon) {
|
||||
$this->refreshToken($account);
|
||||
$this->refreshTokenWithLock($account, fn () => $this->refreshToken($account));
|
||||
$account->refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public function publish(PostPlatform $postPlatform): array
|
|||
$account = $postPlatform->socialAccount;
|
||||
|
||||
if ($account->is_token_expired || $account->is_token_expiring_soon) {
|
||||
$this->refreshToken($account);
|
||||
$this->refreshTokenWithLock($account, fn () => $this->refreshToken($account));
|
||||
$account->refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public function publish(PostPlatform $postPlatform): array
|
|||
$account = $postPlatform->socialAccount;
|
||||
|
||||
if ($account->is_token_expired || $account->is_token_expiring_soon) {
|
||||
$this->refreshToken($account);
|
||||
$this->refreshTokenWithLock($account, fn () => $this->refreshToken($account));
|
||||
$account->refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public function publish(PostPlatform $postPlatform): array
|
|||
|
||||
// Refresh token if expired or expiring soon
|
||||
if ($account->is_token_expired || $account->is_token_expiring_soon) {
|
||||
$this->refreshToken($account);
|
||||
$this->refreshTokenWithLock($account, fn () => $this->refreshToken($account));
|
||||
$account->refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public function publish(PostPlatform $postPlatform): array
|
|||
$account = $postPlatform->socialAccount;
|
||||
|
||||
if ($account->is_token_expired || $account->is_token_expiring_soon) {
|
||||
$this->refreshToken($account);
|
||||
$this->refreshTokenWithLock($account, fn () => $this->refreshToken($account));
|
||||
$account->refresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
use App\Console\Commands\RefreshExpiringTokens;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
||||
Schedule::command(ProcessScheduledPosts::class)->everyMinute();
|
||||
Schedule::command(CheckSocialConnections::class)->daily();
|
||||
Schedule::command(RefreshExpiringTokens::class)->hourly();
|
||||
Schedule::command(RecoverStuckPosts::class)->everyThirtyMinutes();
|
||||
Schedule::command(ProcessScheduledPosts::class)->everyMinute()->withoutOverlapping()->onOneServer();
|
||||
Schedule::command(CheckSocialConnections::class)->daily()->withoutOverlapping()->onOneServer();
|
||||
Schedule::command(RefreshExpiringTokens::class)->hourly()->withoutOverlapping()->onOneServer();
|
||||
Schedule::command(RecoverStuckPosts::class)->everyThirtyMinutes()->withoutOverlapping()->onOneServer();
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@
|
|||
expect($this->postPlatform->error_message)->toBe(__('posts.errors.account_inactive'));
|
||||
});
|
||||
|
||||
test('publish to social platform skips publishing when account token is expired', function () {
|
||||
test('publish to social platform attempts publishing when account token is expired', function () {
|
||||
Event::fake();
|
||||
|
||||
$this->socialAccount->update([
|
||||
|
|
@ -212,15 +212,17 @@
|
|||
]);
|
||||
|
||||
$publisher = Mockery::mock(LinkedInPublisher::class);
|
||||
$publisher->shouldNotReceive('publish');
|
||||
$publisher->shouldReceive('publish')->andReturn([
|
||||
'id' => 'post-123',
|
||||
'url' => 'https://linkedin.com/post/123',
|
||||
]);
|
||||
|
||||
$this->app->instance(LinkedInPublisher::class, $publisher);
|
||||
|
||||
(new PublishToSocialPlatform($this->postPlatform))->handle();
|
||||
|
||||
$this->postPlatform->refresh();
|
||||
expect($this->postPlatform->status)->toBe(PlatformStatus::Failed);
|
||||
expect($this->postPlatform->error_message)->toBe(__('posts.errors.account_disconnected'));
|
||||
expect($this->postPlatform->status)->toBe(PlatformStatus::Published);
|
||||
});
|
||||
|
||||
test('publish to social platform dispatches success notification when all platforms published', function () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue