XF 2.1 Run a piece of code after "x" minutes/hours, what would be the best way to achieve it?

asprin

Active member
Use case scenario:
I'm building an addon which will allow certain limited users to create a new record of a custom entity (let's call this entity Foo). The create form has a date time field where the user will select a value (always a date in future) and will be in Y-m-d hh:mm format which then gets stored as unix timestamp in the database.

PHP:
public static function getStructure(Structure $structure)
{
    $structure->table = 'xf_asp_fb_foo';
    $structure->shortName = 'Asprin\FB:Foo';
    $structure->contentType = 'asprin_fb_foo';
    $structure->primaryKey = 'sys_id';
    $structure->columns = [
        'sys_id' => ['type' => self::UINT, 'autoIncrement' => true],
        'close_time' => ['type' => self::UINT, 'required' => true], // user entered value will be stored here
        'is_time_over' => ['type' => self::BOOL, 'required' => false, 'default' => 0]
    ];
    $structure->getters = [];
    $structure->relations = [];
    $structure->defaultWith = [];

    return $structure;
}

So let's assume a user selects a date time value which is like 30 hours from now and creates the record by clicking the submit button. Now is there a way to:
  • Execute something after 30 hours which will update is_time_over value to 1?
  • Cron seems to be a way forward, but not sure if there's an example of how this can be done programmatically.
  • It would be great if I can pass some arguments to the function that will be called after 30 hours (so that I can use the finder class to fetch that exact row)
This seems doable, but a little help here please?

One way I thought of achieving this is why page loads but I don't feel it's a very good approach since it will require querying the database on every page load.
 
Solution
Probably easiest approach:
Use a job and enqueue that to run in 30 hours.

PHP:
\XF::app()->jobManager()->enqueueLater('myUniqueJobId', \XF::$time + 30 * 3600, 'MyAddOnId:JobClass', ['jobParam1']);
In your addon, set a cron to run a script every 5 or 10 min (whatever 'grace' period you want to allow without pegging your server over and over again)

In this cron script, do a db pull with a date range you want (likely, everything older than now + 5 or 10 min up)

Once you have your list, do the work you require.
 
In your addon, set a cron to run a script every 5 or 10 min (whatever 'grace' period you want to allow without pegging your server over and over again)
Apart from this approach, is there any other way to run something just once instead of polling every "x" mins? Because I would know after how much time the column value needs to be updated so isn't it possible to run something after that amount of time once?
 
You'd have to dynamically create a cron entry with that timestamp which just isn't possible with XF (at least as far as i know it's not). I guess you could write some .sh script to do it, but that seriously over complicates things.

at the end of the day, you have a ton of crons running every 5 min in stock XF. assuming the query is not intensive (it shouldn't be), it won't add load.

XF is UI-driven. Until you get into a back-end driven architecture, with message queues, brokers, Airflow DAGs, and so forth, it's really hard to not long-poll.
 
You'd have to dynamically create a cron entry with that timestamp which just isn't possible with XF (at least as far as i know it's not)
Would have been awesome if it were. I'm also from ServiceNow background and there we have a concept called sys_triggers which does this exactly, thereby also allowing us to do something just once. So I was hoping if it was possible here too. I guess I'll have to take the polling route then.
 
SN is back-end driven, designed with automation tasks in mind.
xf is ui-driven for the most part. and the cron is mostly fake too... it's not actually building a cron tab record on your server. someone loading your website calls the job.php file and it runs stuff. if no one visits, it used to not run. they've added a way to have it run now without traffic but that's fairly new.
 
Probably easiest approach:
Use a job and enqueue that to run in 30 hours.

PHP:
\XF::app()->jobManager()->enqueueLater('myUniqueJobId', \XF::$time + 30 * 3600, 'MyAddOnId:JobClass', ['jobParam1']);
 
Last edited:
Solution
Probably easiest approach:
Use a job and enqueue that to run in 30 hours.

PHP:
\XF::app()->jobManager()->enqueueLater('myUniqueJobId', \XF::$time + 30 * 3600, 'MyAddOnId:JobClass', ['jobParam1']);
This looks promising. I'll definitely try it out and check the results.

Out of curiosity, what does this do behind the scenes? Does this eventually end up creating a cron job (which again would require me to depend on a visit to the site by a visitor) or will this run irrespective of that?

EDIT: A few doubts.
(a) The MyAddOnId:JobClass should point to a class inside the "Job" folder of my addon root folder? i.e. src/addons/Asprin/FB/Job/MyClass.php
(b) How does it know which function to run inside that class?

EDIT2: Seems like I figured out a couple of answers. I did create a new class inside the Job folder with the following:
PHP:
namespace Asprin\FB\Job;

use XF\Job\AbstractJob;

class MyClass extends AbstractJob
{
    public function run($maxRunTime = null)
    {       
        \XF::logException(new \Exception(), false, "Auto job called ");
        return $this->complete();
    }

    public function getStatusMessage()
    {
        return \XF::phrase('processing_email_bounces...');
    }

    public function canCancel()
    {
        return false;
    }

    public function canTriggerByChoice()
    {
        return false;
    }
}
I'm hoping my assumption of return $this->complete(); is correct in that it instructs XF to run the job just once. I earlier had $this->resume() and that just kept looping and creating messages in the logs.

However, I've been unable to pass a parameter to run() method (the 4th argument of enqueueLater() method)

someone loading your website calls the job.php file and it runs stuff
Thanks for pointing this out. Yesterday I created a cron job to run every 10 minutes (on my local install) and left the system idle for 20 minutes. After that I refreshed the page only to find out that the trigger point for the cron job was the page refresh 😥 and not the repeat interval time. So I was kinda wondering if I did something wrong with the cron setup. I did notice something odd also with the job but I guess it's a new thread topic.
 
Last edited:
Out of curiosity, what does this do behind the scenes? Does this eventually end up creating a cron job (which again would require me to depend on a visit to the site by a visitor) or will this run irrespective of that?
It inserts a record in table xf_job to run the job at the specified time.

XF job trigger system will then execute the job when it is due, either through the client via XHR calls to job.php (default) or by calling cmd.php xf:run-jobs via system cronjob once/minute (if configured so).

If you do not have (enough) traffic and you use the default trigger the job might not run on time.

(a) The MyAddOnId:JobClass should point to a class inside the "Job" folder of my addon root folder? i.e. src/addons/Asprin/FB/Job/MyClass.php
Yes. Aspirin\FB:MyClass would be the shortname.

(b) How does it know which function to run inside that class?
The class must be a child of \XF\Job\AbstractJob and implement method run (and a few others).

I'm hoping my assumption of return $this->complete(); is correct in that it instructs XF to run the job just once.
Correct.

I earlier had $this->resume() and that just kept looping and creating messages in the logs.
resume() is necessary if the job has to run longer than $maxRunTime

However, I've been unable to pass a parameter to run() method (the 4th argument of enqueueLater() method)
$this->data
 
Last edited:
Sorry @briansol , had to change the accepted answer as this is more of what I was looking for. Thanks nonetheless for the insights.


XF job trigger system will then execute the job when it is due, either through the client via XHR calls to job.php (default)
So this means it runs automatically and is not dependent of a visit to the site, right? However, how would an XHR call happen if there's no user to provide a client (browser)
 
So this means it runs automatically and is not dependent of a visit to the site, right?
As said before, it depends on how the job trigger is configured. By default clients will trigger job execution via XHR calls - if there is no traffic no jobs will run.

If jobs absolutely must runnon time you would have to either assure that there is sufficient traffic or use system cron.
 
But doesn't that still require traffic to execute? Sorry, trying to understand which one should I go with.
Just FYI, you can set up the cron to make an http request to job.php. However, a cleaner way to do it that doesn't rely on your web server would be to use XenForo's cmd.php system and just set up a cron to run the command php /path/to/xf/cmd.php xf:run-jobs.

You can run jobs both ways, or you can also make that the only way XenForo jobs run under System and performance options (it's an advanced option).

1663437962880.webp
 
Back
Top Bottom