Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions IRRDB-QUEUE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Optional dedicated IRRDB queue

Manual IRRDB updates can run outside the HTTP request without changing the application's default queue connection. Set `IRRDB_QUEUE_CONNECTION=irrdb`, run database migrations, and rebuild cached configuration. Supervise a worker as the web application user:

```
php artisan queue:work irrdb --queue=irrdb --sleep=2 --tries=1 --timeout=1500 --memory=768 --max-time=3600
```

The dedicated connection uses the `irrdb_jobs` table and a retry interval of 1800 seconds. Keep worker timeout below the retry interval. Restart workers after deploying changes. A persistent cache supporting atomic locks is required; configure the same cache for web, worker, and scheduled IRRDB imports.

Leaving the environment variable unset preserves existing queue selection. Pagination, batched writes, and writer serialization apply independently. Listing search matches the start of a prefix or ASN. Change previews are limited to 50 entries with exact totals retained. Failed imports roll back their transaction and leave previously committed records available.

The worker must be supervised and monitored: jobs cannot complete while it is stopped. This feature does not reload route-server configurations or restart BGP sessions.
23 changes: 18 additions & 5 deletions app/Http/Controllers/Irrdb/IrrdbController.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,24 @@ public function list( IrrdbRequest $r, Customer $cust, string $type, int $protoc
/** @var User $us */
$us = Auth::getUser();

$irrdbList = IrrdbAggregator::forCustomerAndProtocol( $cust->id, $protocol, $type );
$model = $type === 'asn' ? \IXP\Models\IrrdbAsn::class : \IXP\Models\IrrdbPrefix::class;
$search = trim( substr( (string) $r->query( 'search', '' ), 0, 100 ) );
$query = $model::where( 'customer_id', $cust->id )->where( 'protocol', $protocol );
if( $search !== '' ) {
// Prefix match keeps the search bounded and treats SQL wildcards literally.
$query->whereRaw( "`" . $type . "` LIKE ? ESCAPE '!'", [ str_replace( ['!', '%', '_'], ['!!', '!%', '!_'], $search ) . '%' ] );
}
$irrdbList = $query->orderBy( 'id' )->paginate(100)->withQueryString();

// are we busting the cache?
if( $r->reset_cache === "1" && $us->isSuperUser() ) {
Cache::forget('updated-irrdb-' . $type . '-' . $cust->id );
Cache::forget('updated-irrdb-' . $type . '-' . $protocol . '-' . $cust->id );
}

return view( 'irrdb/list' )->with([
'irrdbList' => $irrdbList,
'search' => $search,
'failedIrrdb' => Cache::get( 'failed-irrdb-' . $type . '-' . $protocol . '-' . $cust->id, false ),
'type' => $type,
'customer' => $cust,
'protocol' => $protocol,
Expand Down Expand Up @@ -135,10 +144,14 @@ public function update( IrrdbRequest $r, Customer $cust, string $type, int $prot
// get the status of the irrdb update function
$updatedIrrdb = Cache::get( 'updated-irrdb-' . $type . '-' . $protocol . '-' . $cust->id, false );

if( $updatedIrrdb === false ) {
if( $updatedIrrdb === false && Cache::add( 'updating-irrdb-' . $type . '-' . $protocol . '-' . $cust->id, true, 1800 ) ) {
// no cached result so schedule a job to gather them:
Cache::put( 'updating-irrdb-' . $type . '-' . $protocol . '-' . $cust->id, true, 3600 );
UpdateIrrdb::dispatch( $cust, $type, $protocol );
try {
UpdateIrrdb::dispatch( $cust, $type, $protocol );
} catch( \Throwable $e ) {
Cache::forget( 'updating-irrdb-' . $type . '-' . $protocol . '-' . $cust->id );
throw $e;
}
}

return redirect( route( "irrdb@list", [ "cust" => $cust->id, "type" => $type , "protocol" => $protocol ] ) );
Expand Down
52 changes: 41 additions & 11 deletions app/Jobs/UpdateIrrdb.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,23 @@ class UpdateIrrdb extends Job implements ShouldQueue
* @return void
*/
public function __construct( public readonly CustomerModel $customer, public readonly string $type, public readonly int $protocol )
{}
{
/** @var string|null $connection Optional deployment-specific queue connection. */
$connection = config( 'irrdb-performance.connection' );
if( $connection !== null && $connection !== '' ) {
$this->onConnection( $connection )->onQueue( 'irrdb' );
}
}

public int $timeout = 1500;
public int $tries = 1;

public function failed( ?\Throwable $exception ): void
{
$key = $this->type . '-' . $this->protocol . '-' . $this->customer->id;
Cache::forget( 'updating-irrdb-' . $key );
Cache::put( 'failed-irrdb-' . $key, true, 3600 );
}

/**
* Execute the job.
Expand All @@ -87,14 +103,28 @@ public function handle(IrrQuerier $irrdb): void
throw new GeneralException('A persistent cache is required to fetch filtered prefixes' );
}

Cache::put( 'updating-irrdb-' . $this->type . '-' . $this->protocol . '-' . $this->customer->id, true, 3600 );

$updater = $this->type === "asn" ? new UpdateAsnDb( $irrdb, $this->customer ) : new UpdatePrefixDb( $irrdb, $this->customer );

$result = $updater->update();
$result[ "found_at" ] = now();

Cache::put( 'updated-irrdb-' . $this->type . '-' . $this->protocol . '-' . $this->customer->id, $result , 900 );
Cache::put( 'updating-irrdb-' . $this->type . '-' . $this->protocol . '-' . $this->customer->id, false, 3600 );
$key = $this->type . '-' . $this->protocol . '-' . $this->customer->id;
Cache::put( 'updating-irrdb-' . $key, true, 1800 );
Cache::forget( 'failed-irrdb-' . $key );
try {
$updater = $this->type === 'asn'
? new UpdateAsnDb( $irrdb, $this->customer, [ $this->protocol ] )
: new UpdatePrefixDb( $irrdb, $this->customer, [ $this->protocol ] );
$result = $updater->update();
$result['found_at'] = now();
foreach( [4, 6] as $family ) {
foreach( ['new', 'stale'] as $change ) {
$entries = $result['v' . $family][$change];
$result['v' . $family][$change . '_count'] = count( $entries );
$result['v' . $family][$change] = array_slice( $entries, 0, 50 );
}
}
Cache::put( 'updated-irrdb-' . $key, $result, 900 );
} catch( \Throwable $e ) {
$this->failed( $e );
throw $e;
} finally {
Cache::forget( 'updating-irrdb-' . $key );
}
}
}
}
7 changes: 7 additions & 0 deletions app/Tasks/Irrdb/UpdateAsnDb.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ class UpdateAsnDb extends UpdateDb
* @throws \IXP\Exceptions\GeneralException
*/
public function update(): array
{
return \Illuminate\Support\Facades\Cache::lock(
'irrdb-writer-asn-' . $this->customer()->id, 1800
)->block( 30, fn() => $this->performUpdate() );
}

protected function performUpdate(): array
{
foreach( $this->protocols() as $protocol ) {
if( $this->customer()->irrdbConfig && $this->customer()->routeServerClient( $protocol ) && $this->customer()->irrdbFiltered() ) {
Expand Down
33 changes: 18 additions & 15 deletions app/Tasks/Irrdb/UpdateDb.php
Original file line number Diff line number Diff line change
Expand Up @@ -272,22 +272,25 @@ protected function updateDb( array $fromIrrdb, int $protocol, string $type = 'pr
try {
$now = now()->format( 'Y-m-d H:i:s' );

foreach( $fromIrrdb as $p ) {
Log::debug( "INSERT [{$type}]: {$this->customer()->shortname} IPv{$protocol} {$p}" );
$model::create(
[
'customer_id' => $this->customer()->id,
$type => $p,
'protocol' => $protocol,
'last_seen' => $now,
'first_seen' => $now,
]
);
foreach( array_chunk( $fromIrrdb, 1000 ) as $chunk ) {
$rows = [];
foreach( $chunk as $entry ) {
$rows[] = [
'customer_id' => $this->customer()->id,
$type => $entry,
'protocol' => $protocol,
'first_seen' => $now,
'last_seen' => $now,
'created_at' => $now,
'updated_at' => $now,
];
}
$model::insert( $rows );
}

foreach( $fromDb as $i => $p ) {
Log::debug( "DELETE [{$type}]: {$this->customer()->shortname} IPv{$protocol} ID:{$p['id']} {$p[$type]}" );
$model::where( 'id', $p['id'] )->delete();
foreach( array_chunk( array_column( $fromDb, 'id' ), 1000 ) as $ids ) {
$model::where( 'customer_id', $this->customer()->id )
->where( 'protocol', $protocol )->whereIn( 'id', $ids )->delete();
}

$model::where( 'customer_id', $this->customer()->id )
Expand All @@ -304,7 +307,7 @@ protected function updateDb( array $fromIrrdb, int $protocol, string $type = 'pr
IrrdbAggregator::prefixesForRouterConfiguration( $this->customer(), $protocol, true );
}

} catch( Exception $e ) {
} catch( \Throwable $e ) {
DB::rollBack();
$this->result['dbTime'] += $this->timeElapsed();
throw $e;
Expand Down
7 changes: 7 additions & 0 deletions app/Tasks/Irrdb/UpdatePrefixDb.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ class UpdatePrefixDb extends UpdateDb
* @throws \IXP\Exceptions\GeneralException
*/
public function update(): array
{
return \Illuminate\Support\Facades\Cache::lock(
'irrdb-writer-prefix-' . $this->customer()->id, 1800
)->block( 30, fn() => $this->performUpdate() );
}

protected function performUpdate(): array
{
foreach( $this->protocols() as $protocol ) {
if( $this->customer()->irrdbConfig && $this->customer()->routeServerClient( $protocol ) && $this->customer()->irrdbFiltered() ) {
Expand Down
2 changes: 2 additions & 0 deletions config/irrdb-performance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?php
return ['connection' => env('IRRDB_QUEUE_CONNECTION', null)];
7 changes: 7 additions & 0 deletions config/queue.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@

'connections' => [

'irrdb' => [
'driver' => 'database',
'table' => 'irrdb_jobs',
'queue' => 'irrdb',
'retry_after' => 1800,
],

'sync' => [
'driver' => 'sync',
],
Expand Down
18 changes: 18 additions & 0 deletions database/migrations/2026_11_07_213600_create_irrdb_jobs_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('irrdb_jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
public function down(): void { Schema::dropIfExists('irrdb_jobs'); }
};
37 changes: 27 additions & 10 deletions resources/views/irrdb/list.foil.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</a>
/
<?php endif; ?>
IRRDB <?= $t->type === "asn" ? 'ASNs' : 'Prefixes' ?> IPv<?= $t->ee( $t->protocol ) ?> Entries (<?= count( $t->irrdbList ) ?>)
IRRDB <?= $t->type === "asn" ? 'ASNs' : 'Prefixes' ?> IPv<?= $t->ee( $t->protocol ) ?> Entries (<?= $t->irrdbList->total() ?>)
<?php $this->append() ?>


Expand Down Expand Up @@ -41,7 +41,7 @@
<div class="row">
<div class="col-md-12">
<?= $t->alerts() ?>
<?php if( $t->updatingIrrdb || $t->updatedIrrdb ): ?>
<?php if( $t->updatingIrrdb || ($t->updatedIrrdb && !$t->failedIrrdb) ): ?>
<div class="alert alert-info" role="alert">
<?php if( $t->updatingIrrdb ): ?>
<p>
Expand All @@ -55,32 +55,41 @@
The IRRDB update process has completed. <?= $t->ee( $t->updatedIrrdb[ "msg" ] ) ?>
<br/><br/>
There are a total of <?= $t->ee( $t->updatedIrrdb[ "v" . $t->protocol ][ "count" ] ) ?> IPv<?= $t->ee( $t->protocol ) ?> <?= $t->type === "asn" ? 'ASNs' : 'prefixes' ?>
of which <b><?= count( $t->updatedIrrdb[ "v" . $t->protocol ][ "new" ] ) ?> are new and have been added;
and <?= count( $t->updatedIrrdb[ "v" . $t->protocol ][ "stale" ] ) ?> are stale and have been removed</b>.
of which <b><?= ( $t->updatedIrrdb[ "v" . $t->protocol ][ "new_count" ] ?? count( $t->updatedIrrdb[ "v" . $t->protocol ][ "new" ] ) ) ?> are new and have been added;
and <?= ( $t->updatedIrrdb[ "v" . $t->protocol ][ "stale_count" ] ?? count( $t->updatedIrrdb[ "v" . $t->protocol ][ "stale" ] ) ) ?> are stale and have been removed</b>.

<?php if( count( $t->updatedIrrdb[ "v" . $t->protocol ][ "new" ] ) > 0): ?>
<?php if( ( $t->updatedIrrdb[ "v" . $t->protocol ][ "new_count" ] ?? count( $t->updatedIrrdb[ "v" . $t->protocol ][ "new" ] ) ) > 0): ?>
<br><br>
<h6>New/Added IPv<?= $t->ee( $t->protocol ) ?> <?= $t->type === "asn" ? 'ASNs' : 'Prefixes' ?></h6>
<ul>
<?php foreach( $t->updatedIrrdb[ "v" . $t->protocol ][ "new" ] as $new ): ?>
<?php foreach( array_slice($t->updatedIrrdb[ "v" . $t->protocol ][ "new" ], 0, 50) as $new ): ?>
<li> <?= $t->ee( $new ) ?> </li>
<?php endforeach; ?>
</ul>
<?php endif; ?>

<?php if( count( $t->updatedIrrdb[ "v" . $t->protocol ][ "stale" ] ) > 0): ?>
<?php if( ( $t->updatedIrrdb[ "v" . $t->protocol ][ "stale_count" ] ?? count( $t->updatedIrrdb[ "v" . $t->protocol ][ "stale" ] ) ) > 0): ?>
<br><br>
<h6>Stale/Removed IPv<?= $t->ee( $t->protocol ) ?> <?= $t->type === "asn" ? 'ASNs' : 'Prefixes' ?></h6>
<ul>
<?php foreach( $t->updatedIrrdb[ "v" . $t->protocol ][ "stale" ] as $stale ): ?>
<?php foreach( array_slice($t->updatedIrrdb[ "v" . $t->protocol ][ "stale" ], 0, 50) as $stale ): ?>
<li> <?= $t->ee( $stale[$t->type] ) ?> </li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php endif; ?>
</div>
<?php endif; ?>
<table id='irrdb-list' class="table collapse table-striped table-responsive-ixp-with-header" width="100%">
<?php if( $t->failedIrrdb ): ?>
<div class="alert alert-danger">The last IRRDB update failed. Previously committed entries remain available. Please contact the NOC or retry the update.</div>
<?php endif; ?>
<form method="get" class="form-inline mb-3">
<label for="irrdb-search" class="mr-2">Prefix / ASN starts with</label>
<input id="irrdb-search" name="search" maxlength="100" class="form-control mr-2" value="<?= $t->ee($t->search) ?>">
<button class="btn btn-secondary" type="submit">Search</button>
</form>
<p>Showing <?= $t->irrdbList->firstItem() ?? 0 ?>–<?= $t->irrdbList->lastItem() ?? 0 ?> of <?= $t->irrdbList->total() ?> entries. Update details show at most 50 added and 50 removed entries.</p>
<table id='irrdb-list' class="table collapse table-striped table-responsive-ixp-with-header" width="100%">
<thead class="thead-dark">
<tr>
<th>
Expand Down Expand Up @@ -108,9 +117,14 @@
</td>
</tr>
<?php endforeach;?>
<tbody>
</tbody>
</table>

<nav aria-label="IRRDB result pages" class="my-3">
<?php if( $t->irrdbList->previousPageUrl() ): ?><a class="btn btn-secondary" href="<?= $t->ee($t->irrdbList->previousPageUrl()) ?>">Previous</a><?php endif; ?>
<span class="mx-2">Page <?= $t->irrdbList->currentPage() ?> of <?= $t->irrdbList->lastPage() ?></span>
<?php if( $t->irrdbList->nextPageUrl() ): ?><a class="btn btn-secondary" href="<?= $t->ee($t->irrdbList->nextPageUrl()) ?>">Next</a><?php endif; ?>
</nav>
<?php if( $t->updatedIrrdb ): ?>
<div class="alert alert-info tw:mt-16">
<p>
Expand Down Expand Up @@ -144,6 +158,9 @@
stateDuration : DATATABLE_STATE_DURATION,
responsive: true,
paging: false,
searching: false,
ordering: false,
info: false,
columnDefs: [
{ responsivePriority: 1, targets: 0 },
{ responsivePriority: 2, targets: -1 }
Expand Down
14 changes: 6 additions & 8 deletions tests/Jobs/UpdateIrrdbTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,14 @@ public function testAsnJob()
$this->assertEquals(0, IrrdbAsn::where('customer_id', $this->customer->id)->where('protocol', 6)->count());

$bgp = Mockery::mock(IrrQuerier::class);
$bgp->expects('setWhois')->with($this->irrdbConfig->host)->times(2);
$bgp->expects('setSources')->with($this->irrdbConfig->source)->times(2);
$bgp->expects('setWhois')->with($this->irrdbConfig->host)->once();
$bgp->expects('setSources')->with($this->irrdbConfig->source)->once();
$bgp->expects('getAsnList')->with('as1213', 4)->andReturn($asns);
$bgp->expects('getAsnList')->with('as1213', 6)->andReturn($asns);
$job = new UpdateIrrdb($this->customer, 'asn', 4);
$job->handle($bgp);

$this->assertEquals(count($asns), IrrdbAsn::where('customer_id', $this->customer->id)->where('protocol', 4)->count());
$this->assertEquals(count($asns), IrrdbAsn::where('customer_id', $this->customer->id)->where('protocol', 6)->count());
$this->assertEquals(0, IrrdbAsn::where('customer_id', $this->customer->id)->where('protocol', 6)->count());

$jobResult = \Cache::get( 'updated-irrdb-' . $job->type . '-' . $job->protocol . '-' . $this->customer->id);
$this->assertArrayHasKey('wiped', $jobResult);
Expand All @@ -123,15 +122,14 @@ public function testPrefixJob()
$this->assertEquals(0, IrrdbPrefix::where('customer_id', $this->customer->id)->where('protocol', 6)->count());

$bgp = Mockery::mock(IrrQuerier::class);
$bgp->expects('setWhois')->with($this->irrdbConfig->host)->times(2);
$bgp->expects('setSources')->with($this->irrdbConfig->source)->times(2);
$bgp->expects('setWhois')->with($this->irrdbConfig->host)->once();
$bgp->expects('setSources')->with($this->irrdbConfig->source)->once();
$bgp->expects('getPrefixList')->with('as1213', 4)->andReturn($prefixesV4);
$bgp->expects('getPrefixList')->with('as1213', 6)->andReturn($prefixesV6);
$job = new UpdateIrrdb($this->customer, 'prefix', 4);
$job->handle($bgp);

$this->assertEquals(count($prefixesV4), IrrdbPrefix::where('customer_id', $this->customer->id)->where('protocol', 4)->count());
$this->assertEquals(count($prefixesV6), IrrdbPrefix::where('customer_id', $this->customer->id)->where('protocol', 6)->count());
$this->assertEquals(0, IrrdbPrefix::where('customer_id', $this->customer->id)->where('protocol', 6)->count());

$jobResult = \Cache::get( 'updated-irrdb-' . $job->type . '-' . $job->protocol . '-' . $this->customer->id);
$this->assertArrayHasKey('wiped', $jobResult);
Expand Down