Flourish PHP Unframework
This is an archived copy of the forum for reference purposes

Delayed closing of db connection

posted by mblarsen 9 years ago

Hi

For my project I've replaced session handling by a custom class that writes the sessions to the database.

I'm using fDatabase for the database connection.

The system calls session write after __destruct() has been evoked on the fDatabase object, thus connection is lost.

Do you have any suggestions for resolving this issue?

I added a method to fDatabase to set a flag to (not) automatically close the connection in __destruct(). Consequently I have to close the connection myself using the "private" __destruct() method when the system invokes close on the session.

So it seems like the session is being closed after the objects have been destroyed. What about passing fSession::close() to register_shutdown_function()? I think this will force the session to close before the objects are destroyed.

register_shutdown_function(array('fSession', 'close'));
posted by wbond 9 years ago

Did this end up working for you, or did you find another solution?

posted by wbond 9 years ago

No it did work for me unfortunately. I've added some logging to get a view of the order of things.

But the main problem is that I use the __destruct() method of a few classes to persist them to session. So to keep state across each request (states like, shopping basket, discounts, etc.).

Here is the output.

SessionManager::open
SessionManager::read
fSession::set(active_shop_id => 2)
Page loaded
fDatabase::__destruct()
Application::__destruct()
fSession::set(Application => O%3A11%3A%22Application%22%3A2%3A%7Bs%3A19%3A%22%00Application%00config%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22universe%22%3Bs%3A10%3A%22multiverse%22%3B%7D)
SessionManager::write

Note that SessionManager::close is never called, because the db connection has been closed, causing an error.

The following log is with this piece of code enabled (the code you suggested):

register_shutdown_function(array('fSession', 'close'));

This is the output

SessionManager::open
SessionManager::read
fSession::set(active_shop_id => 2)
Page loaded
SessionManager::write
SessionManager::close
fDatabase::__destruct()
Application::__destruct()
fSession::set(Application => O%3A11%3A%22Application%22%3A2%3A%7Bs%3A19%3A%22%00Application%00config%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22universe%22%3Bs%3A10%3A%22multiverse%22%3B%7D)
SessionManager::open
SessionManager::read

In this situation the SessionManager::close method is invoked as it should, but upon destruction (and attempt to persisit objects to session) the code is once again trying to open the session. This time without a db connection.

The race is on ...

Any suggestions?

posted by mblarsen 9 years ago

You can actually control the order of when the __destruct() methods are called. They are called in the reverse order of the __construct() methods.

So, the first object you should construct is fDatabase. After creating that, I would create your !SessionManager object. Then after that, create all of your other objects. This should cause your other objects to be destructed first, then the !SessionManager, which should write the session data to the database, and then fDatabase should close the connection.

posted by wbond 9 years ago

This won't work either. I would have to use the 'register_shutdown_function' you suggested. Which is fine. But when I use the destruct to serialize my state objects, they will upon destruction try to reopen the session. Then we have the problem again.

I guess the only solution would be to note use destruct for this.

For these objects I could register a shutdown function as well. This would most likely solve the problem.

(mixed feelings :)

posted by mblarsen 9 years ago

I guess that since you cannot control / determine the order of __destruct() invokes it would not make sence to implement a hook in fDatabase::close().

Any suggestions? or think I will vote for something like:

fDatabase::disableAutoClose();

And the corresponding small change to fDatabase::__destruct()

	public function __destruct()
	{
		if (!$this->connection) { return; }
		if ($this->auto_close !== TRUE) { return; }

From the SessionManager::close() I would invoke a fDatabase::close() method.

But this still leaves the problem that at this point the db object, not the connection, has been destructed.

Update:

The latter seems not to be a problem. Actually the !SessionManager is the first to be destructed on this setup. Any insight to this? Using objects after their destruction? (plain evil?)

posted by mblarsen 9 years ago

For anyone interested, I ended up doing something like this.

Code left out to keep it simple.

First a base class with a persist method. It could just as well be an interface. The important element is the persist() method invoked from the SessionManager below.

class PersistentObject
{
	public function persist()
	{
		fSession::set(get_class($this), urlencode(serialize($this)));
		return TRUE;
	}
}

The SessionManager that registers objects for persistens and serializes them before writing the session to storage:

class SessionManager
{
	private static $singleton;

	private $db;
	private $objects_to_persist = array();
	
	public static function &get()
	{
		if (self::$singleton == NULL) {
			self::$singleton = new SessionManager();
		}
		return self::$singleton;
	}
	
	function registerPersistentObject($object)
	{
		$this->objects_to_persist[] = $object;
	}
	
	public static function persistObjects()
	{
		foreach (self::$singleton->objects_to_persist as $object) {
			call_user_func_array(array(&$object, 'persist'), array());
		}
	}
	
	public static function getPersistedObject($className)
	{
		$serialized_object = fSession::get($className);
		if (isset($serialized_object) === TRUE) { 
			return unserialize(urldecode(fSession::get($serialized_object)));	
		}
		return FALSE;
	}
	
	function __construct()
	{
		session_set_save_handler( 
			array( &$this, "open" ), 
			array( &$this, "close" ),
			array( &$this, "read" ),
			array( &$this, "write"),
			array( &$this, "destroy"),
			array( &$this, "gc" )
			);
	}
	
	function setStorage(&$storage)
		{
			if ($storage instanceof fDatabase) {
				$this->db = $storage; 
			} else if (is_string($storage)) {
				fSession::setPath($storage);
			}
		}
	
	function open($session_path, $session_name)	{ ... }

	function close() { ... } // $db is automatically closed

	function read($id) { ... }

	function write($id, $data) { ... }

	function destroy($id) { ... }
	
	function gc() { ... }
}

A class extending PersistentObject:

class Application extends PersistentObject
{
        private static $singleton;

	public $universe;

	public static function &get()
	{
		if(!isset(self::$singleton)) {
			self::$singleton = SessionManager::getPersistedObject(__CLASS__);
			if(self::$singleton === FALSE) {
				self::$singleton = new Application();
			}
		}
		return self::$singleton;
	}
	
	public function __construct()
	{
		SessionManager::get()->registerPersistentObject($this);
	}

	public function __sleep()
	{
		return array('universe');
	}
	
	public function __wakeup()
	{
		SessionManager::get()->registerPersistentObject($this);
	}
        
        ...
}

And last, your init file:

// First SessionManager::persistObjects is registered to that is called before the session is closed.
register_shutdown_function(array('SessionManager', 'persistObjects'));
// Second fSession::close is registed to ensure that the session is written to database before the connection is closed in fDatabase::__destruct().
register_shutdown_function(array('fSession', 'close'));
$session_db = new fDatabase( ... );
SessionManager::get()->setStorage($session_db);
fSession::open();
posted by mblarsen 9 years ago

Hey Will - would something like this be possible to work into fSession directly?

posted by vena 9 years ago