Skip to content
View in the app

A better way to browse. Learn more.

ernestdefoe.online

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
ernestdefoe.online

Extensions, themes & support for Flarum and Invision Community

Vibe coding for the community web. Report a bug, request a feature, or dig into the source — this is where the tools you use get built, in the open.

We do custom Bespoke Invision Community apps. If you have an idea for something you want then use the contact form to get in touch with us.
Knowledge base

Things that cost me a day, so they cost you none

Working notes from building Invision Community and Flarum applications. Mostly the failures that give no error at all — the ones where everything installs cleanly and quietly does the wrong thing.

92 articles

Invision Community 5

86 articles

Extensions and contracts

39

What each extension point is for, what it must declare, and what happens when it is wrong — which is usually nothing visible.

Languages and text

5

The string table, translation, and the places where text does not appear where you expected it to.

Theming, templates and forms

9

Theme hooks, CSS that survives both colour schemes, and building forms that do not throw on render.

Background work and scheduled tasks

5

The queue system, work that has to happen after the response, and jobs that finish without doing anything.

Data, settings and storage

11

The database layer, settings, tags, file storage, and backing up a live site.

AI features and expectations

5

What these features do, what they cost, and what buyers reasonably but wrongly assume they do.

Application structure and releases

11

The JSON files an application is made of, versioning and upgrade steps, and testing from the command line.

Realtime, chat and calls

1

WebSocket gateways, relays and the server-side pieces live features depend on — where "it works when I test it" and "it works for your members" are different claims.

Nothing matches that.

Your background task is running inside a visitor's page load

An Invision Community site that has not been given a cron job runs its scheduled tasks during ordinary page views. That is the default, and most self-hosted communities never change it. If your application does anything slow in a queue or a task — and calling an outside API is slow — a random visitor pays for it with their page load, and with the database connection that page load is holding.

This is not a theoretical concern. It took a live community down.

The symptom

The report to look out for, near enough verbatim:

PHP-FPM was completely overloaded, and so MariaDB too. But the MySQL databases haven't gotten significantly larger.

That combination is the whole diagnosis. Workers are being held, not overworked, and no amount of looking at table sizes will explain it:

  • Each held PHP-FPM worker keeps its MySQL connection open for as long as it is held, so MySQL saturates on connections while the data is untouched.
  • A pool of, say, twenty workers is exhausted by twenty concurrent slow calls, at which point every other request queues and the entire site looks down.
  • Because the cause is an outbound call, server CPU and disk look fine, which sends people hunting for a DDoS or a bad query instead.

Why it happens

The setting is task_use_cron, and its default value is normal. Under normal, \IPS\Dispatcher\Standard::__destruct() picks up one queued task and runs it at the end of a page request:

if ( $this->runTasks and Settings::i()->task_use_cron == 'normal' and !Request::i()->isAjax() )
{
    $this->inDestructor = true;

    $task = Task::queued();

    if ( $task )
    {
        $task->runAndLog();
    }
}

So the question is not "is my task slow?" but "who is waiting while it runs?" On a cron-driven site, nobody. On a default site, a visitor.

The fix

Cap how long an outbound call may take when a page request is waiting on it, and keep the generous limit for everything else. Dispatcher::i()->inDestructor tells you which situation you are in.

public static function seconds( ?int $background = NULL ): int
{
    return static::blocking() ? 20 : ( $background ?? 90 );
}

public static function blocking(): bool
{
    /* defined() first - \IPS\CLI is not declared in every entry point,
       and an undefined constant is a fatal Error in PHP 8. */
    if ( defined( '\IPS\CLI' ) and \IPS\CLI )
    {
        return FALSE;
    }

    try
    {
        return (bool) Dispatcher::i()->inDestructor;
    }
    catch ( \Throwable $e )
    {
        return FALSE;
    }
}

Then route every outbound call through it:

$response = Url::external( $endpoint )->request( Timeout::seconds( 90 ) )->post( $payload );

Twenty seconds is chosen to be comfortably below a typical max_execution_time and far below any FPM request_terminate_timeout, so your call gives up before anything above it does.

What not to do

The obvious-looking fix is to detect the destructor and refuse to run there, deferring the work to a proper task run. Do not do this. On a site with no cron, the destructor is the task runner. Skipping it means the feature never runs at all, and you have traded a slow site for a broken application — which the administrator will notice later and understand less.

Bound the wait. Do not skip the work.

Tell the administrator

Capped is not the same as correct. A 20-second ceiling protects the server but can truncate whatever the call was doing. If your application depends on outbound calls, say plainly in the AdminCP when the site is in this mode:

if ( Settings::i()->task_use_cron === 'normal' and !\IPS\CIC )
{
    /* warn: tasks are running during visitors' page loads */
}

Note the \IPS\CIC check — Invision Community Cloud runs tasks properly, so the warning would be wrong there.

Audit an existing application

Any application with a Queue extension or a Task, plus an outbound HTTP call, is exposed. Find them:

grep -rn "request(" $(find applications/myapp -name "*.php") | grep -v "Request::i()"

Anything much over 30 seconds needs the guard. When this was audited across a catalogue of two dozen applications, seven were affected — the worst had six calls allowed 120 seconds and one allowed 300.



User Feedback

Recommended Comments

There are no comments to display.

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.