Skip to content

Commit 3557783

Browse files
Refactor/maintenance character affiliation (#656)
* fix error with eager loading and potential missing corp id * chore: rename CharacterAffiliationService to CacheCharacterAffiliationIdsService and update references in ProcessContactResponse file * chore(CharacterAffiliationJob): refactor character id error handling and upsert logic * feat(character): add RefreshCharacterAffiliationsService and schedule it every five minutes * chore: refactor CharacterAffiliationLifeCycleTest and ContactLifecycleTest to use CacheCharacterAffiliationIdsService instead of CharacterAffiliationService * chore: remove unnecessary whitespace and update code formatting in RefreshCharacterAffiliationsService * chore: fix return type declaration in handleFailedRequest method
1 parent c8dfb37 commit 3557783

9 files changed

Lines changed: 254 additions & 222 deletions

src/EveapiServiceProvider.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
use Seatplus\Eveapi\Observers\CharacterInfoObserver;
5555
use Seatplus\Eveapi\Observers\GroupObserver;
5656
use Seatplus\Eveapi\Observers\TypeObserver;
57+
use Seatplus\Eveapi\Services\Character\RefreshCharacterAffiliationsService;
5758
use Seatplus\Eveapi\Services\Esi\EsiClientSetup;
5859

5960
class EveapiServiceProvider extends ServiceProvider
@@ -236,7 +237,8 @@ private function addSchedules(): void
236237
});
237238

238239
// Run Character Affiliation Job every five minutes to updated outdated affiliations.
239-
$schedule->job(new CharacterAffiliationJob)->everyFiveMinutes();
240+
//$schedule->job(new CharacterAffiliationJob)->everyFiveMinutes();
241+
$schedule->call(new RefreshCharacterAffiliationsService)->everyFiveMinutes();
240242

241243
// Cleanup Batches Table
242244
$schedule->command('queue:prune-batches')->daily();

src/Jobs/Character/CharacterAffiliationJob.php

Lines changed: 75 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@
2626

2727
namespace Seatplus\Eveapi\Jobs\Character;
2828

29+
use Illuminate\Support\Carbon;
2930
use Illuminate\Support\Collection;
3031
use Illuminate\Support\Facades\Cache;
31-
use Illuminate\Support\Facades\Redis;
32+
use Seatplus\EsiClient\DataTransferObjects\EsiResponse;
3233
use Seatplus\EsiClient\Exceptions\RequestFailedException;
3334
use Seatplus\Eveapi\Esi\HasRequestBodyInterface;
3435
use Seatplus\Eveapi\Jobs\Alliances\AllianceInfoJob;
3536
use Seatplus\Eveapi\Jobs\Corporation\CorporationInfoJob;
3637
use Seatplus\Eveapi\Jobs\EsiBase;
3738
use Seatplus\Eveapi\Models\Character\CharacterAffiliation;
38-
use Seatplus\Eveapi\Services\Jobs\CharacterAffiliationService;
3939
use Seatplus\Eveapi\Traits\HasRequestBody;
4040

4141
class CharacterAffiliationJob extends EsiBase implements HasRequestBodyInterface
@@ -44,7 +44,12 @@ class CharacterAffiliationJob extends EsiBase implements HasRequestBodyInterface
4444

4545
private array $manual_ids = [];
4646

47-
public function __construct(int|array|null $character_ids = null)
47+
private Collection $character_affiliations;
48+
49+
/**
50+
* @throws \Throwable
51+
*/
52+
public function __construct(int|array $character_ids)
4853
{
4954
parent::__construct(
5055
method: 'post',
@@ -53,6 +58,11 @@ public function __construct(int|array|null $character_ids = null)
5358
);
5459

5560
$this->setManualIds($character_ids);
61+
62+
// throw error if manual_ids is not larger than 1000
63+
throw_unless(count($this->manual_ids) <= 1000, new \Exception('Character ids must not exceed 1000'));
64+
65+
$this->character_affiliations = collect();
5666
}
5767

5868
public function tags(): array
@@ -63,125 +73,61 @@ public function tags(): array
6373
];
6474
}
6575

66-
/**
67-
* Get the middleware the job should pass through.
68-
*/
69-
public function middleware(): array
70-
{
71-
return [
72-
...parent::middleware(),
73-
];
74-
}
75-
7676
/**
7777
* Execute the job.
7878
*
7979
* @throws \Exception
8080
*/
8181
public function executeJob(): void
8282
{
83-
if ($this->manual_ids) {
84-
$this->updateOrCreateCharacterAffiliations($this->manual_ids);
85-
}
8683

87-
if (! $this->manual_ids) {
88-
Redis::throttle('character_affiliations')
89-
// allow one job to process every 5 minutes
90-
->block(0)->allow(1)->every(5 * 60)
91-
->then(function () {
92-
collect()
93-
->merge($this->getIdsToUpdateFromCache())
94-
->merge($this->getIdsToUpdateFromDatabase())
95-
->chunk(1000)
96-
->each(fn (Collection $chunk) => $this->updateOrCreateCharacterAffiliations($chunk->toArray()));
97-
}, fn () => $this->delete());
98-
}
99-
}
84+
$this->updateOrCreateCharacterAffiliations($this->getManualIds());
10085

101-
private function getIdsToUpdateFromCache(): Collection
102-
{
103-
return CharacterAffiliationService::make()->retrieve()->unique();
86+
CharacterAffiliation::query()->upsert(
87+
$this->character_affiliations->toArray(),
88+
['character_id'],
89+
['corporation_id', 'alliance_id', 'faction_id', 'last_pulled']
90+
);
91+
92+
$this->followUp();
10493
}
10594

106-
private function getIdsToUpdateFromDatabase(): Collection
95+
public function processResponse(EsiResponse $response, Carbon $timestamp): void
10796
{
108-
return CharacterAffiliation::query()
109-
// only those who were not pulled within the last hour
110-
->where('last_pulled', '<=', now()->subHour()->toDateTimeString())
111-
// and don't try doomheimed characters
112-
->where('corporation_id', '<>', 1_000_001)
113-
->pluck('character_id');
97+
collect($response)
98+
->each(fn (object $result) => $this->character_affiliations->push(
99+
[
100+
'character_id' => $result->character_id,
101+
'corporation_id' => $result->corporation_id,
102+
'alliance_id' => data_get($result, 'alliance_id'),
103+
'faction_id' => data_get($result, 'faction_id'),
104+
'last_pulled' => $timestamp,
105+
]
106+
));
114107
}
115108

116109
private function updateOrCreateCharacterAffiliations(array $character_ids): void
117110
{
118111
$this->setRequestBody($character_ids);
119-
120112
$timestamp = now();
121113

122-
$character_affiliations = collect();
123-
124114
// try to get the character affiliations from the esi endpoint
125115
try {
126116
$response = $this->retrieve();
127117

128-
collect($response)
129-
->each(fn (object $result) => $character_affiliations->push(
130-
[
131-
'character_id' => $result->character_id,
132-
'corporation_id' => $result->corporation_id,
133-
'alliance_id' => data_get($result, 'alliance_id'),
134-
'faction_id' => data_get($result, 'faction_id'),
135-
'last_pulled' => $timestamp,
136-
]
137-
));
118+
$this->processResponse($response, $timestamp);
138119
} catch (RequestFailedException $exception) {
139-
// if the request fails, we perform a binary search to find the character ids that are not valid
140-
// if the request fails and the character ids are less than 2, we can assume that the character id is invalid
141-
if (count($character_ids) === 1) {
142-
// dispatch a new character_info job to update the character info if it is member of doomheim
143-
CharacterInfoJob::dispatch($character_ids[0])->onQueue('low');
144-
145-
// cache the invalid character id for 1 day
146-
// first get the cached invalid character ids
147-
$invalid_character_ids = Cache::get('invalid_character_ids', []);
148-
// add the invalid character id to the array
149-
$invalid_character_ids[] = $character_ids[0];
150-
// cache the array
151-
Cache::put('invalid_character_ids', $invalid_character_ids, 60 * 24);
152-
153-
return;
154-
}
155-
156-
// if the request fails and the character ids are more than 2, we perform a binary search to find the invalid character ids
157-
$half = (int) ceil(count($character_ids) / 2);
158-
$first_half = array_slice($character_ids, 0, $half);
159-
$second_half = array_slice($character_ids, $half);
160-
161-
$this->updateOrCreateCharacterAffiliations($first_half);
162-
$this->updateOrCreateCharacterAffiliations($second_half);
120+
$this->handleFailedRequest($character_ids);
163121
}
164-
165-
CharacterAffiliation::upsert(
166-
$character_affiliations->toArray(),
167-
['character_id'],
168-
['corporation_id', 'alliance_id', 'faction_id', 'last_pulled']
169-
);
170-
171-
$this->followUp();
172122
}
173123

174124
public function getManualIds(): array
175125
{
176126
return $this->manual_ids;
177127
}
178128

179-
public function setManualIds(int|array|null $manual_ids): void
129+
public function setManualIds(int|array $manual_ids): void
180130
{
181-
if (is_null($manual_ids)) {
182-
return;
183-
}
184-
185131
$manual_ids = is_array($manual_ids) ? $manual_ids : [$manual_ids];
186132

187133
$this->manual_ids = $manual_ids;
@@ -213,4 +159,44 @@ private function getMissingAlliances(): void
213159
->unique()
214160
->each(fn (int $alliance_id) => AllianceInfoJob::dispatch($alliance_id)->onQueue('high'));
215161
}
162+
163+
private function handleFailedRequest(array $character_ids): void
164+
{
165+
// if the request fails and the character ids are less than 2, we can assume that the character id is invalid
166+
if (count($character_ids) === 1) {
167+
$this->handleSingleIdException($character_ids[0]);
168+
169+
return;
170+
}
171+
172+
// if the request fails, we perform a binary search to find the character ids that are not valid
173+
$this->handleMultipleIdsException($character_ids);
174+
175+
}
176+
177+
private function handleSingleIdException(int $character_id): void
178+
{
179+
180+
// dispatch a new character_info job to update the character info if it is member of doomheim
181+
CharacterInfoJob::dispatch($character_id)->onQueue('low');
182+
183+
// cache the invalid character id for 1 day
184+
// first get the cached invalid character ids
185+
$invalid_character_ids = Cache::get('invalid_character_ids', []);
186+
// add the invalid character id to the array
187+
$invalid_character_ids[] = $character_id;
188+
// cache the array
189+
Cache::put('invalid_character_ids', $invalid_character_ids, 60 * 24);
190+
}
191+
192+
private function handleMultipleIdsException(array $character_ids): void
193+
{
194+
// if the request fails and the character ids are more than 2, we perform a binary search to find the invalid character ids
195+
$half = (int) ceil(count($character_ids) / 2);
196+
$first_half = array_slice($character_ids, 0, $half);
197+
$second_half = array_slice($character_ids, $half);
198+
199+
$this->updateOrCreateCharacterAffiliations($first_half);
200+
$this->updateOrCreateCharacterAffiliations($second_half);
201+
}
216202
}

src/Jobs/Seatplus/UpdateCorporation.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,10 @@ public function handle(): void
7070
if ($this->corporation_id) {
7171
$this->execute($this->corporation_id, 'high');
7272
} else {
73-
RefreshToken::with('corporation', 'character.roles')
73+
RefreshToken::with(['corporation', 'character.roles'])
7474
->cursor()
75-
->map(fn (RefreshToken $token) => $token->corporation->corporation_id)
75+
->map(fn (RefreshToken $token) => $token->corporation?->corporation_id)
76+
->filter()
7677
->unique()
7778
->each(fn (int $corporation_id) => $this->execute($corporation_id));
7879
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace Seatplus\Eveapi\Services\Character;
4+
5+
use Seatplus\Eveapi\Jobs\Character\CharacterAffiliationJob;
6+
use Seatplus\Eveapi\Models\Character\CharacterAffiliation;
7+
use Seatplus\Eveapi\Models\Character\CharacterInfo;
8+
use Seatplus\Eveapi\Services\Jobs\CacheCharacterAffiliationIdsService;
9+
10+
class RefreshCharacterAffiliationsService
11+
{
12+
public function __invoke(): void
13+
{
14+
15+
$character_ids = [
16+
...$this->getIdsToUpdateFromDatabase(),
17+
...$this->getIdsToUpdateFromCache(),
18+
...$this->getMissingIdsFromCharacterInfo(),
19+
];
20+
21+
$this->processAffiliations($character_ids);
22+
}
23+
24+
private function getMissingIdsFromCharacterInfo(): array
25+
{
26+
return CharacterInfo::query()
27+
->whereDoesntHave('character_affiliation')
28+
->pluck('character_id')
29+
->toArray();
30+
}
31+
32+
private function processAffiliations(array $ids): void
33+
{
34+
$unique_ids = array_unique($ids);
35+
$chunks = array_chunk($unique_ids, 1000);
36+
37+
foreach ($chunks as $chunk) {
38+
CharacterAffiliationJob::dispatch($chunk)->onQueue('high');
39+
}
40+
}
41+
42+
private function getIdsToUpdateFromDatabase(): array
43+
{
44+
return CharacterAffiliation::query()
45+
// only those who were not pulled within the last hour
46+
->where('last_pulled', '<=', now()->subHour()->toDateTimeString())
47+
// and don't try doomheimed characters
48+
->where('corporation_id', '<>', 1_000_001)
49+
->pluck('character_id')
50+
->toArray();
51+
}
52+
53+
private function getIdsToUpdateFromCache(): array
54+
{
55+
return CacheCharacterAffiliationIdsService::make()
56+
->retrieve()
57+
->unique()
58+
->toArray();
59+
}
60+
}

src/Services/Contacts/ProcessContactResponse.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
use Illuminate\Support\Collection;
3030
use Seatplus\EsiClient\DataTransferObjects\EsiResponse;
3131
use Seatplus\Eveapi\Models\Contacts\Contact;
32-
use Seatplus\Eveapi\Services\Jobs\CharacterAffiliationService;
32+
use Seatplus\Eveapi\Services\Jobs\CacheCharacterAffiliationIdsService;
3333

3434
class ProcessContactResponse
3535
{
@@ -59,7 +59,7 @@ public function execute(EsiResponse $response): Collection
5959
$contact_model->labels()->createMany($labels_to_save->map(fn (int $label_id) => ['label_id' => $label_id]));
6060
}
6161
})->pipe(function (Collection $response) {
62-
CharacterAffiliationService::make()
62+
CacheCharacterAffiliationIdsService::make()
6363
->queue($response->filter(fn (object $contact) => $contact->contact_type === 'character')->pluck('contact_id')->toArray());
6464

6565
return $response;

src/Services/Jobs/CharacterAffiliationService.php renamed to src/Services/Jobs/CacheCharacterAffiliationIdsService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Illuminate\Support\Collection;
66
use Illuminate\Support\Facades\Cache;
77

8-
class CharacterAffiliationService
8+
class CacheCharacterAffiliationIdsService
99
{
1010
public static function make(): self
1111
{

0 commit comments

Comments
 (0)