What does your Cron.php look like ?

Report issues with Enuuk Auction Platform or Mods here - remember to raise a ticket with phpauction.net as well
Post Reply
RWAP
Site Admin
Posts: 750
Joined: Fri Jan 08, 2010 2:23 am
Location: Stoke-on-Trent
Contact:

What does your Cron.php look like ?

Post by RWAP » Wed Aug 08, 2012 9:04 am

One of the most important files in Enuuk is the class\Cron.php which runs numerous maintenance tasks.

However, having been working on a new website, I still see the odd problem whereby an auction ends and the final value fee gets charged twice...

Now why does this happen?

A little lateral thinking - the cron job basically runs each time someone navigates to a new page - it then checks what listings are due to start, which are due to end, and activates them, or finishes them - sending out end of auction emails, and charging the final value fees as appropriate.

Sounds simple...

However, what if two people navigate to a new page at the same time (or within milliseconds) - there are two cron jobs running, and it may well be that one cron job is still working through the listings to finish whilst the other builds up its list of listings - OOPS - both cron jobs could have the same list of items to finish (or at least some will be in both lists). This results in multiple emails, and final value fees - especially if your server is a little slow!

The answer lies in the three main functions:

Code: Select all

    static public function run(MyPDO $DB, Core_Context $context)
    {
        self::checkDelayedBidsInPennyAuctions($DB, $context);
        self::openDatePendingOffers($DB, $context);
        self::closeExpiredOffers($DB, $context);
        self::relistOffers($DB, $context);
        self::checkMembership($DB, $context);
    }
    
    static public function openDatePendingOffers(MyPDO $DB, Core_Context $context)
    {
        $now = date("Y-m-d H:i:s"); //Used to get corrected time based on timezone settings
        $offers = Offer::getListFromDB($DB, 'where offers.active = 0 and offers.datePending = 1 and draft = 0
                            and offers.startDate < "'.$now.'" and offers.endDate > "'.$now.'"
                            and winner is NULL');
        foreach($offers as $v){
            $o = Offer::getInstanceFromDB($DB, $v['id']);
            $o->active = 1;
            $o->saveToDB($DB);
        }
    }
    
    static public function closeExpiredOffers(MyPDO $DB, Core_Context $context)
    {
        $now = date("Y-m-d H:i:s"); //Used to get corrected time based on timezone settings
        $offers = Offer::getListFromDB($DB, 'where offers.active = 1
                                        and offers.endDate <= "'.$now.'"');
        //Finish and save the offer & send notifications
        $offersObj = array();
        foreach($offers as $v){
            $o = Offer::getInstanceFromDB($DB, $v['id']);
            $o->prepareCommonObservers($DB, $context);
            $o->finishOffer();
            $o->saveToDB($DB);
        }
    }

    static public function relistOffers(MyPDO $DB, Core_Context $context)
    {
        $now = date("Y-m-d H:i:s"); //Used to get corrected time based on timezone settings
        $offers = Offer::getListFromDB($DB, 'where offers.active = 0
                                        and offers.draft = 0
                                        and offers.endDate <= "'.$now.'"
                                        and offers.winner is null
                                        and relisting > 0');

        // Relist and save to DB (asap)
        // Note: this part is done ASAP to avoid other users re-relisting in paralel
        $offersObj = array();
        foreach($offers as $v){
            $o = Offer::getInstanceFromDB($DB, $v['id']);
            $o->relist($DB); //Deletes bid history, and reset prices and dates
            $offersObj[] = $o;
        }
So here, I present my tried and tested solution - replace the above with:

Code: Select all

    static public function run(MyPDO $DB, Core_Context $context)
    {
        // Use file lock to stop other Cron jobs running at same time
        $lockAcquired=false;
        $lockFile=Image::UPLOAD_PATH.'CronLock';
        $lockStatus=0;
        if (file_exists($lockFile) && filesize($lockFile)>0){
            $fp = fopen($lockFile, 'r+');
            $lockStatus =  fread($fp, filesize($lockFile));
            if (!$lockStatus==0){
                // Unlikely for a cron job to take longer than 5 minutes
                // So we can presume that the cron has not released itself for some reason
                if (time()<$lockStatus+300) {
                    fclose($fp);
                    return;
                }
            }
        } else {
            // Create lock file
            $cronDate=time();
            file_put_contents($lockFile, $cronDate);
            $fp = fopen($lockFile, 'r+');
        }
        if (flock($fp, LOCK_EX)) {  // acquire an exclusive lock
            $lockAcquired=true;
            ftruncate($fp, 0);      // truncate file
            rewind($fp);
            $cronDate=time();
            fwrite($fp, $cronDate);
            fflush($fp);            // flush output before releasing the lock
        }
        if ($lockAcquired){
            // Avoid running if we are going to update the Twitter Feed itself otherwise we can getinto a loop
            if (strtolower($context->parameters['feed']) =='twitter'){
                $runExtraTests=false;
            } else {
                $lockDate=Image::UPLOAD_PATH.'CronDate';
                $runExtraTests=false;
                if (file_exists($lockDate) && filesize($lockDate)>0){
                    $fh = fopen($lockDate, 'r');
                    if (filesize($lockDate)>0) $cronID =  fread($fh, filesize($lockDate));
                    fclose($fh);
                }
                if (time()>=$cronID+60) $runExtraTests=true; // Seems daft to run the cron job more than once every 60 seconds
                if (strtolower($context->parameters['v']) =='html') self::checkMembership($DB, $context);
                self::checkDelayedBidsInPennyAuctions($DB, $context);
                self::closeExpiredOffers($DB, $context);
                self::openDatePendingOffers($DB, $context);
                self::openDatePendingOffers($DB, $context);
                if ($runExtraTests){
                    self::relistOffers($DB, $context);
                    // Store last time Cron job run
                    $fh = fopen($lockDate, 'w');
                    $cronID=time();
                    fwrite($fh, $cronID);
                    fclose($fh);
                }
            }
            // Release Cron lock
            ftruncate($fp, 0);      // truncate file
            rewind($fp);
            fwrite($fp, '0');
            fflush($fp);            // flush output before releasing the lock
            flock($fp, LOCK_UN);    // release the lock
        }
        fclose($fp);
    }

    static public function openDatePendingOffers(MyPDO $DB, Core_Context $context)
    {
        $now = date("Y-m-d H:i:s"); //Used to get corrected time based on timezone settings
        $offers = Offer::getListFromDB($DB, 'where offers.active = 0 and offers.datePending = 1 and draft = 0
                            and offers.startDate <= "'.$now.'" and offers.endDate > "'.$now.'"
                            and winner is NULL');
        foreach($offers as $v){
            Offer::activate($DB,1,$v['id']);
        }
    }

    static public function closeExpiredOffers(MyPDO $DB, Core_Context $context)
    {
        $now = date("Y-m-d H:i:s"); //Used to get corrected time based on timezone settings
        $offers = Offer::getListFromDB($DB, 'where offers.active = 1
                                        and (offers.endDate <= "'.$now.'" || offers.type="'.Offer::LOT.'")');
        if ($offers){
	        //Finish and save the offer & send notifications
			foreach($offers as $v){
				$o = Offer::getInstanceFromDB($DB, $v['id']);
                $close=true;
                if ($o instanceof LotAuction){
                    if ($o->quantity>0 && strtotime($o->endDate)>time()){
                    	unset ($offers[$k]);
                        $close=false;
                   	}
                }
                if ($close){
			        $o->prepareCommonObservers($DB, $context);
			        $o->finishOffer();
					$o->saveToDB($DB);
                }
			}
        }
        unset($offers);
    }

    static public function relistOffers(MyPDO $DB, Core_Context $context)
    {
        $now = date("Y-m-d H:i:s"); //Used to get corrected time based on timezone settings
        $offers = Offer::getListFromDB($DB, 'where offers.active = 0
                                        and offers.draft = 0
                                        and offers.endDate <= "'.$now.'"
                                        and (offers.winner is null || offers.type="'.Offer::LOT.'")
                                        and relisting > 0');

        // Relist and save to DB (asap)
        // Note: this part is done ASAP to avoid other users re-relisting in paralel
        $offersObj = array();
        foreach($offers as $k=>$v){
            $o = Offer::getInstanceFromDB($DB, $v['id']);
			$relist=true;
			if ($o instanceof LotAuction){
				if ($o->quantity==0){
					$relist=false;
				}
			}
			if ($relist){
				$o->relist($DB); //Deletes bid history, and reset prices and dates
				$offersObj[$k] = $o;
			}
        }
Then, in class\Offer.php - add the following function (above the line):

Code: Select all

/// COMMON METHODS TO EXTEND ///  

Code: Select all

    /**
    *   Updates the active state of an auction in the Database
    *   @param  MyPDO   $DB database instance
    *
    */
    public function activate(MyPDO $DB,$active,$id)
    {
        $query = $DB->prepare('update  '.self::DB_TABLE.' set active=:active where id=:id');
        $return = $query->execute(array(':active'=>$active,':id'=>$id));
        unset($query);
        return $return;
    }
I did report this to phpAuction some time ago (many months ago), but I guess their server is quick enough (with not enough listings) for the cron job to finish quickly.

For the observant, you will see that extra code has been added to deal with the LOT auctions - as they are not being relisted if there are still items left !!

There are other improvements which can be made to the speed, by replacing the Offer::getListFromDB() calls with a simpler Offer::getCRONFastListFromDB() - although these details are only provided as part of my speed enhancement module :D
Last edited by RWAP on Wed May 01, 2013 1:07 pm, edited 2 times in total.
Reason: Improved the locking mechanism

RWAP
Site Admin
Posts: 750
Joined: Fri Jan 08, 2010 2:23 am
Location: Stoke-on-Trent
Contact:

Re: What does your Cron.php look like ?

Post by RWAP » Mon Mar 16, 2015 10:37 pm

v3.5 implements a version of the locking code (using a lock stored in the database itself)

Post Reply

Who is online

Users browsing this forum: No registered users and 1 guest