Run an asynchronous PHP task using Symfony Process - windows

For time-consuming tasks (email sending, image manipulation… you get the point), I want to run asynchronous PHP tasks.
It is quite easy on Linux, but I'm looking for a method that works on Windows too.
I want it to be simple, as it should be. No artillery, no SQL queueing, no again and again installing stuff… I just want to run a goddamn asynchronous task.
So I tried the Symfony Process Component.
Problem is, running the task synchronously works fine, but when running it asynchronously it exits along the main script.
Is there a way to fix this?
composer require symfony/process
index.php
<?php
require './bootstrap.php';
$logFile = './log.txt';
file_put_contents($logFile, '');
append($logFile, 'script (A) : '.timestamp());
$process = new Process('php subscript.php');
$process->start(); // async, subscript exits prematurely…
//$process->run(); // sync, works fine
append($logFile, 'script (B) : '.timestamp());
subscript.php
<?php
require './bootstrap.php';
$logFile = './log.txt';
//ignore_user_abort(true); // doesn't solve issue…
append($logFile, 'subscript (A) : '.timestamp());
sleep(2);
append($logFile, 'subscript (B) : '.timestamp());
bootstrap.php
<?php
require './vendor/autoload.php';
class_alias('Symfony\Component\Process\Process', 'Process');
function append($file, $content) {
file_put_contents($file, $content."\n", FILE_APPEND);
}
function timestamp() {
list($usec, $sec) = explode(' ', microtime());
return date('H:i:s', $sec) . ' ' . sprintf('%03d', floor($usec * 1000));
}
result
script (A) : 02:36:10 491
script (B) : 02:36:10 511
subscript (A) : 02:36:10 581
// subscript (B) is missing

Main script must be waiting when async process will be completed. Try this code:
$process = new Process('php subscript.php');
$process->start();
do {
$process->checkTimeout();
} while ($process->isRunning() && (sleep(1) !== false));
if (!$process->isSuccessful()) {
throw new \Exception($process->getErrorOutput());
}

If php supports fpm for Windows, you can listen to kernel.terminate event to provide all expensive tasks after response has been sent.
Service:
app.some_listener:
class: SomeBundle\EventListener\SomeListener
tags:
- { name: kernel.event_listener, event: kernel.terminate, method: onKernelTerminate }
Listener:
<?php
namespace SomeBundle\EventListener;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
class SomeListener
{
public function onKernelTerminate(PostResponseEvent $event)
{
// provide time consuming tasks here
}
}

Not the best solution, but:
$process = new Process('nohup php subscript.php &');
$process->start();

Related

Check status of an scheduler task before adding and running another task

I have a question about the task scheduling in laravel framework. I already have defined the commands currency:update and currency:archive in the list of console commands. Now, I want to run these two commands in the schedule method but with the following condition:
if this is a time to run the currency:archive command, don't run the other command currency:update until the previous command (i.e. currency:archive) ends; otherwise run the currency:update command.
This is my current code in schedule method:
$schedule->command('currency:archive')
->dailyAt('00:00')
->withoutOverlapping();
$schedule->command('currency:update')
->everyMinute()
->withoutOverlapping();
How should I modify it?
Thanks.
In the laravel schedule docs the following two features are mentioned:
Truth test contraints docs
$schedule->command('emails:send')->daily()->skip(function () {
return true;
});
Task hooks docs
$schedule->command('emails:send')
->daily()
->before(function () {
// Task is about to start...
})
->after(function () {
// Task is complete...
});
You could consider setting a variable like $achriveCommandIsRunning to true in the before() closure. In the skip() closure you can return $archiveCommandIsRunning; and in the after() closure you can set the variable back to false again.
Thanks,
I modified my code as you said and it works now.
Here is the final code:
$isArchiveCommandRunning = false;
$schedule->command('currency:archive')
->dailyAt('00:00')
->before(function () use (&$isArchiveCommandRunning) {
$isArchiveCommandRunning = true;
})
->after(function () use (&$isArchiveCommandRunning) {
$isArchiveCommandRunning = false;
})
->withoutOverlapping();
$schedule->command('currency:update')
->everyMinute()
->skip(function () use (&$isArchiveCommandRunning) {
return $isArchiveCommandRunning;
})
->withoutOverlapping();

How to check if queue is working

I need to start a game every 30 seconds, cron job minimum interval is a minute, so I use queue delay to do it
app/Jobs/StartGame.php
public function handle()
{
// Start a new issue
$this->gameService->gameStart();
// Start new issue after 15 seconds
$job = (new \App\Jobs\StartGame)->onQueue('start-game')->delay(30);
dispatch($job);
}
And I start first game by console
app/Console/Commands/StartGame.php
public function handle()
{
$job = (new \App\Jobs\StartGame)->onQueue('start-game');
dispatch($job);
}
The question is, I want to use cron job to check if the start game queue is running, if not then dispatch, in case of something like server stop for maintenance, is it possible?
You can write a log of the "start-date" in a file, for example : data_start.log
1- every time gameStart() is called the content of file (date) is checked, and verified if is between 29sec and 31sec.
2- update the content of file.
example of write :
<?php
$file = "data_start.log";
$f=fopen($file, 'w');
fwrite( $f, date('Y-m-d H:i:s') . "\n");
?>

Stop queue on error

I am trying to delete my queue on error.
I have tried the following:
file: global.php
Queue::failing(function($connection, $job, $data)
{
// Delete the job
$job->delete();
});
But when my queue fails like this one does:
public function fire($job, $data){
undefined_function(); // this function is not defined and will trow a error
}
Then the job is not deleted for some reasons.
Any ideas?
From their user manual, section Checking The Number Of Run Attempts:
If an exception occurs while the job is being processed, it will automatically be released back onto the queue.
I think you need to set the maximum number of tries to 1, so the job will fail on the first error.
php artisan queue:listen --tries=1
you can prevent your job from throwing exception
public function handle()
{
try {
//your code here
}catch (\Exception $e){
return true;
}
}

How to make Behat wait for Angular ajax calls?

I have a reporting page that is basically a table you can add and remove columns from. When you add a column, the data for that column is fetched and loaded with ajax, using angular.
Consider this Behat scenario:
Given I have a user named "Dillinger Four"
And I am on "/reports"
When I add the "User's Name" column
Then I should see "Dillinger Four"
How can I make Behat wait until angular's ajax call completes? I would like to avoid using a sleep, since sleeps add unnecessary delay and will fail if the call takes too long.
I used the following to wait for jquery code:
$this->getSession()->wait($duration, '(0 === jQuery.active)');
I haven't found a similar value to check with angular.
Your link above was helpful, just to expand on it and save someone else a little time.
/**
* #Then /^I should see "([^"]*)" if I wait "([^"]*)"$/
*/
public function iShouldSeeIfIWait($text, $time)
{
$this->spin(function($context) use ($text) {
$this->assertPageContainsText($text);
return true;
}, intval($time) );
}
/**
* Special function to wait until angular has rendered the page fully, it will keep trying until either
* the condition is meet or the time runs out.
*
* #param function $lambda A anonymous function
* #param integer $wait Wait this length of time
*/
public function spin ($lambda, $wait = 60)
{
for ($i = 0; $i < $wait; $i++)
{
try {
if ($lambda($this)) {
return true;
}
} catch (Exception $e) {
// do nothing
}
sleep(1);
}
$backtrace = debug_backtrace();
throw new Exception(
"Timeout thrown by " . $backtrace[1]['class'] . "::" . $backtrace[1]['function'] . "()\n" .
$backtrace[1]['file'] . ", line " . $backtrace[1]['line']
);
}
Then in your Scenario use:
Then I should see "Something on the page." if I wait "5"
You can use code from Angular's Protractor library to wait for loading. Here you can find a function waitForAngular(). It simply waits for a client-side function with the same name
Here's working PHP code.
class WebContext implements Context
{
/**
* #Then the list of products should be:
*/
public function theListOfProductsShouldBe(TableNode $table)
{
$this->waitForAngular();
// ...
}
private function waitForAngular()
{
// Wait for angular to load
$this->getSession()->wait(1000, "typeof angular != 'undefined'");
// Wait for angular to be testable
$this->getPage()->evaluateScript(
'angular.getTestability(document.body).whenStable(function() {
window.__testable = true;
})'
);
$this->getSession()->wait(1000, 'window.__testable == true');
}
}

writing output to a file with CakePHP shell

I am trying to just simply write "hello world" to a file, from a cakephp shell, with plans to eventually write a sitemap.xml file using our product models. I found this: Question which got me started...
But i am thinking either ConsoleOutput is not supported in Cake 1.3.6 (which i'm using), or i need to include the class that holds it.
The error i get when trying to run the file from terminal:
PHP Fatal error: Class 'ConsoleOutput' not found in /public/vendors/shells/sitemap.php on line 7
Here is my code:
class SitemapShell extends Shell {
public function __construct($stdout = null, $stderr = null, $stdin = null) {
// This will cause all Shell outputs, eg. from $this->out(), to be written to
// TMP.'shell.out'
$stdout = new ConsoleOutput('file://'.TMP.'shell.out');
// You can do the same for stderr too if you wish
// $stderr = new ConsoleOutput('file://'.TMP.'shell.err');
parent::__construct($stdout, $stderr, $stdin);
}
public function main() {
// The following output will not be printed on your console
// but be written to TMP.'shell.out'
$this->out('Hello world');
}
}
You are correct that ConsoleOutput didn't feature in CakePHP 1.3 - can you upgrade to a version 2.*?
If not you could just use regular PHP:
$fp = fopen('hello.txt', 'w');
fwrite($fp, 'hello world');
fclose($fp);
Hope this helps.
Toby

Resources