Propel in a high availability environment

Introduction

This tutorial describes how Propel can be used in a critical web application using high availability. For more information about high availability on linux systems see www.linux-ha.org. This tutorial does not describe how servers can be configured in high availability clusters.

One cluster of two servers

The easiest form of a high availability cluster consists of two servers. One of these servers is the primary server and one the secondary server. In normal operation the primary server handles all requests and the secondary server is idle. As soon as the primary server fails the secondary server takes over all tasks.

There are two choices when setting up such a cluster, one with realtime synchronisation of the database and one where synchronisation occurs at regular intervals. Nothing special has to be done with Propel to use Propel in an environment with synchronisation at regular intervals. This tutorial describes how Propel can be used to achieve realtime synchonisation of the database. This is the most flexible solution.

Synchronising your data

Obviously you need to have the same database structure on both servers. Since Propel will generate the sql queries, this shouldn't be a problem. Furthermore, the runtime configuration of Propel must contain entries for two servers. You can create additional datasources in the runtime configuration by editing runtime-conf.xml (and add a second datasource with a different id). Say we have two datasources foo and bar, now every save method in all generated Propel classes need to be overridden:

public function save() {
       $clone = clone $this;
       $foo = Propel::getConnection('foo');
       $bar = Propel::getConnection('bar');
       $this->realSave($foo);
       $clone->realSave($bar);
}

private function realSave($con = null) {
       parent::save($con);
}

In a similar way all delete functions have to be overridden (at least, if you want to be able to remove things from the database).

public function delete() {
       $clone = clone $this;
       $foo = Propel::getConnection('foo');
       $bar = Propel::getConnection('bar');
       $this->realDelete($foo);
       $clone->realDelete($bar);
}

private function realDelete($con = null) {
       parent::delete($con);
}

Prevent double data

Since you are using a high availability cluster, you should be aware of the fact that it might happen that one of the servers is down. Configure your cluster so that if any of the servers is down the remaining server will listen to both the ipadress of foo and the ip address of bar. The code above would cause any data to be saved twice when only one server is running. You can prevent this from happening by verifying the status of your cluster. To do this define the following function in a superclass of you Propel classes (like BaseObject.php).

protected function areBothLocal($host1, $host2) {
       $ip1 = gethostbyname($host1);
       $ip2 = gethostbyname($host2);

       $localips = explode("\n", `/sbin/ifconfig | grep -i "eth1" -A 1|grep "inet addr"|cut -d " " -f 12|cut -d ":" -f 2`);
       if (in_array($ip1, $localips) && in_array($ip2, $localips))
           return true;

       return false;
}

Using this function you can change the delete() and save() functions to prevent double entries.

public function save() {
       $clone = clone $this;
       $foo = Propel::getConnection('foo');
       $bar = Propel::getConnection('bar');
       $this->realSave($foo);
       if (!$this->areBothLocal('foo', 'bar'))
             $clone->realSave($bar);
}

public function delete() {
       $clone = clone $this;
       $foo = Propel::getConnection('foo');
       $bar = Propel::getConnection('bar');
       $this->realDelete($foo);
       if (!$this->areBothLocal('foo', 'bar'))
             $clone->realDelete($bar);
}

This setup will work, however it is not completely safe. You can get into trouble when your databases have become out of sync somehow (I underestimated this problem the first time I used this setup).