<?php

namespace RedUNIT\Base;

use RedUNIT\Base as Base;
use RedBeanPHP\Facade as R;
use RedBeanPHP\RedException as RedException;
use RedBeanPHP\OODBBean as OODBBean;
use RedBeanPHP\QueryWriter\AQueryWriter;

/**
 * Concurrency
 *
 * Tests whether we can lock beans.
 *
 * @file    RedUNIT/Base/Concurrency.php
 * @desc    Tests concurrency scenarios
 * @author  Gabor de Mooij and the RedBeanPHP Community
 * @license New BSD/GPLv2
 *
 * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community.
 * This source file is subject to the New BSD/GPLv2 License that is bundled
 * with this source code in the file license.txt.
 */
class Concurrency extends Base
{
	/**
	 * Returns the target drivers for this test.
	 * This test only works with Postgres and MySQL/MariaDB.
	 *
	 * @return array
	 */
	public function getTargetDrivers()
	{
		return array( 'pgsql','mysql' );
	}

	/**
	 * Prepares the database connection.
	 *
	 * @return void
	 */
	public function prepare()
	{
		try{
			R::close();
		} catch( \Exception $e ) {}
	}

	/**
	 * This test has to be run manually.
	 *
	 * @return void
	 */
	private function testLockException()
	{
		R::nuke();
		$lock = R::dispense('lock');
		$lock->value = 123;
		R::store($lock);
		$c = pcntl_fork();
		if ($c == -1) exit(1);
		if (!$c) {
			R::selectDatabase($this->currentlyActiveDriverID . 'c2');
			R::freeze(TRUE);
			try { R::exec('SET SESSION innodb_lock_wait_timeout=5');}catch( \Exception $e ){}
			try { R::exec('SET autocommit = 0'); }catch( \Exception $e ){}
			R::begin();
			$lock = R::loadForUpdate('lock', $lock->id);
			$lock->value = 4;
			sleep(10);
			R::store($lock);
			exit(0);
		} else {
			R::selectDatabase($this->currentlyActiveDriverID . 'c2');
			sleep(1);
			R::freeze(TRUE);
			try{ R::exec('SET SESSION innodb_lock_wait_timeout=5');}catch( \Exception $e ){}
			try{R::exec("SET lock_timeout = '1s';");}catch( \Exception $e ){}
			try { R::exec('SET autocommit = 0'); }catch( \Exception $e ){}
			R::begin();
			$exception = NULL;
			try {
				$lock = R::loadForUpdate('lock', $lock->id);
			} catch( \Exception $e ) {
				$exception = $e;
			}
			if ( !$exception ) fail();
			pass();
			$details = $exception->getDriverDetails();
			asrt( ($details[1]===1205 || $details[0]==='55P03'), TRUE );
			var_dump($lock);
		}
		try { R::exec('SET autocommit = 1'); }catch( \Exception $e ){}
		pcntl_wait($status);
		try { R::exec('SET SESSION innodb_lock_wait_timeout=50');}catch( \Exception $e ){}
		try{R::exec("SET lock_timeout = '50s';");}catch( \Exception $e ){}
	}

	/**
	 * Tests basic locking scenarios using fork().
	 *
	 * @return void
	 */
	public function testConcurrency()
	{
		$c = pcntl_fork();
		if ($c == -1) exit(1);
		if (!$c) {
			R::selectDatabase($this->currentlyActiveDriverID . 'c');
			try{ R::exec('SET SESSION innodb_lock_wait_timeout=51');}catch( \Exception $e ){}
			try{R::exec("SET lock_timeout = '51s';");}catch( \Exception $e ){}
			R::exec('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
			sleep(2);
			try { R::exec('SET autocommit = 0'); }catch( \Exception $e ){}
			R::freeze(TRUE);
			R::begin();
			echo "CHILD: SUBTRACTING 2 START\n";
			$i = R::loadForUpdate('inventory', 1);
			$i->apples -= 2;
			sleep(4);
			R::store($i);
			R::commit();
			echo "CHILD: SUBTRACTING 2 DONE\n";
			echo (R::load('inventory', 1));
			echo "\n";
			exit(0);
		} else {
			R::selectDatabase($this->currentlyActiveDriverID . 'c');
			try{ R::exec('SET SESSION innodb_lock_wait_timeout=51');}catch( \Exception $e ){}
			try{R::exec("SET lock_timeout = '51s';");}catch( \Exception $e ){}
			echo "PARENT: PREP START\n";
			R::nuke();
			$i = R::dispense('inventory');
			$i->apples = 10;
			R::store($i);
			R::exec('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
			echo "PARENT: PREP DONE\n";
			sleep(3);
			echo "PARENT: ADDING 5 START\n"; 
			try { R::exec('SET autocommit = 0'); }catch( \Exception $e ){}
			R::freeze( TRUE );
			R::begin();
			$i = R::findForUpdate('inventory', ' id = ? ', array(1));
			$i = reset( $i );
			print_r($i);
			$i->apples += 5;
			R::store($i);
			R::commit();
			echo "PARENT ADDING 5 DONE\n";
			$i = R::getAll('select * from inventory where id = 1');
			print_r($i);
			asrt((int)$i[0]['apples'], 13);
			R::freeze( FALSE );
			try { R::exec('SET autocommit = 1'); }catch( \Exception $e ){}
			pcntl_wait($status); 
		}
	}

	/**
	 * Test whether we can use setSQLSnippet with find() and batch().
	 *
	 * @return void
	 */
	public function testBatchAndFind()
	{
		/* baseline */
		R::nuke();
		$beans = R::dispenseAll('bean*10');
		R::storeAll($beans[0]);
		$ids = array();
		foreach($beans[0] as $bean) $ids[] = $bean->id;
		R::debug( TRUE, 1 );
		$logs = R::getDatabaseAdapter()
            ->getDatabase()
            ->getLogger();
		$beans = R::batch( 'bean', $ids );
		asrt( count( $beans ), 10 );
		$entries = $logs->grep('for update');
		asrt( count( $entries ), 0 );
		$logs->clear();

		/* findOneForUpdate */
		R::nuke();
		$beans = R::dispenseAll('bean*10');
		R::storeAll($beans[0]);
		$ids = array();
		foreach($beans[0] as $bean) $ids[] = $bean->id;
		R::debug( TRUE, 1 );
		$logs = R::getDatabaseAdapter()
            ->getDatabase()
            ->getLogger();
		$beans = R::findOneForUpdate( 'bean' );
		asrt( count( $beans ), 1 );
		$entries = $logs->grep('for update');
		asrt( count( $entries ), 0 );
		$logs->clear();

		/* findForUpdate */
		R::nuke();
		$beans = R::dispenseAll('bean*10');
		R::storeAll($beans[0]);
		$ids = array();
		foreach($beans[0] as $bean) $ids[] = $bean->id;
		R::debug( TRUE, 1 );
		$logs = R::getDatabaseAdapter()
            ->getDatabase()
            ->getLogger();
		$beans = R::findForUpdate( 'bean' );
		asrt( count( $beans ), 10 );
		$entries = $logs->grep('for update');
		asrt( count( $entries ), 0 );
		$logs->clear();

		/* batch + snippet */
		R::nuke();
		$beans = R::dispenseAll('bean*10');
		R::storeAll($beans[0]);
		$ids = array();
		R::getWriter()->setSQLSelectSnippet('for update');
		foreach($beans[0] as $bean) $ids[] = $bean->id;
		R::debug( TRUE, 1 );
		$logs = R::getDatabaseAdapter()
            ->getDatabase()
            ->getLogger();
		$beans = R::batch( 'bean', $ids );
		asrt( count( $beans ), 10 );
		$entries = $logs->grep('for update');
		asrt( count( $entries ), 1 );
		print_r( $entries );
		$logs->clear();

		/* baseline */
		R::nuke();
		$beans = R::dispenseAll('bean*10');
		R::storeAll($beans[0]);
		$ids = array();
		foreach($beans[0] as $bean) $ids[] = $bean->id;
		R::debug( TRUE, 1 );
		$logs = R::getDatabaseAdapter()
            ->getDatabase()
            ->getLogger();
		$beans = R::batch( 'bean', $ids );
		asrt( count( $beans ), 10 );
		$entries = $logs->grep('for update');
		asrt( count( $entries ), 0 );
		$logs->clear();

		/* find + snippet */
		R::nuke();
		$beans = R::dispenseAll('bean*10');
		R::storeAll($beans[0]);
		$ids = array();
		R::getWriter()->setSQLSelectSnippet('for update');
		foreach($beans[0] as $bean) $ids[] = $bean->id;
		R::debug( TRUE, 1 );
		$logs = R::getDatabaseAdapter()
            ->getDatabase()
            ->getLogger();
		$beans = R::find( 'bean' );
		asrt( count( $beans ), 10 );
		$entries = $logs->grep('for update');
		asrt( count( $entries ), 1 );
		print_r( $entries );
		$logs->clear();

		/* baseline */
		R::nuke();
		$beans = R::dispenseAll('bean*10');
		R::storeAll($beans[0]);
		$ids = array();
		foreach($beans[0] as $bean) $ids[] = $bean->id;
		R::debug( TRUE, 1 );
		$logs = R::getDatabaseAdapter()
            ->getDatabase()
            ->getLogger();
		$beans = R::batch( 'bean', $ids );
		asrt( count( $beans ), 10 );
		$entries = $logs->grep('for update');
		asrt( count( $entries ), 0 );
		$logs->clear();

		R::debug( FALSE );
	}

	/**
	 * loadForUpdate/findForUpdate should be applied even if caching is on.
	 * Caching may not interfere with locking beans.
	 *
	 * @return void
	 */
	public function testLockAndCache()
	{
		R::nuke();
		$bean = R::dispense('lock');
		$bean->title = 'lock';
		$id = R::store( $bean );
		R::getWriter()->setUseCache( TRUE );
		$lock = R::loadForUpdate( 'lock', $id );
		R::debug( TRUE, 1 );
		$lock = R::loadForUpdate( 'lock', $id );
		$logs = R::getDatabaseAdapter()->getDatabase()->getLogger()->grep( AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE );
		asrt( count($logs), 1 ); //if no cache clear, then would have been 2
		R::debug( FALSE );
		$lock = R::findForUpdate( 'lock', 'id = ?', array( $id ) );
		R::debug( TRUE, 1 );
		$lock = R::findForUpdate( 'lock', 'id = ?', array( $id ) );
		$logs = R::getDatabaseAdapter()->getDatabase()->getLogger()->grep( AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE );
		asrt( count($logs), 1 ); //if no cache clear, then would have been 2
		R::getWriter()->setUseCache( FALSE );
		R::debug( FALSE );
		R::nuke();
	}
}
