27 Apr 2012

Closing a site for maintenance using HaProxy

Say you have a website running behind haproxy, and you want to block access to the backend for everyone except yourself, so you can perform some maintenance. 

People visiting the website during maintenance should be served a special maintenance page, with a HTTP 503 response code, to indicate that the site is temporarily unavailable. You want this page to be different from the regular 503 error page, so that it's clear that it's planned maintenance. 

I found that an easy way to achieve this is to use a separate 'maintenance' backend which is disabled by default, but which can be enabled using the haproxy's stats socket interface. 

backend maintenance
    server maintenance dummy:8090 disabled weight 0
    errorfile 503 /path_to_maintenance_file/503_maintenance.http

This backend has one server, but it has weight 0, which means this backend will always be 'down', resulting in a 503 response.

To direct traffic to this backend, we use some ACL rules in the frontend: 

frontend  main *:80
    default_backend             app-servers

    acl maintenance_mode srv_is_up(maintenance/maintenance)
    acl is_admin src 123.123.123.123
    use_backend maintenance if maintenance_mode !is_admin

Now if the maintenance backend is up, haproxy will use that instead of the default backend, except when the request is from 123.123.123.123 (obviously this should be the ip address of your own machine that you want to perform maintenance from).

Now all we have to do to switch to 'maintenance mode' is enable the maintenance/maintenance server:

echo 'enable server maintenance/maintenance' | socat unix-connect:/var/lib/haproxy/stats stdio

Note that you need HaProxy 1.4 for this, and your socket needs to be configured for level 'admin'.

15 Sep 2011

Changing the type of a primary key in a large InnoDB table that is referenced by foreign key constraints

Say you find out that someone (not you, of course) created a table with MEDIUMINT as the column type of the primary key, while it should have been an INTEGER, or even a BIGINT, considering the rate at which it is growing. Say you find out about this only when the table is about to overflow. You need to do something quickly, before no more rows can be added to this table. 

In my experience, the fastest way to change the schema of a large table is to dump the entire table to a file, truncate it, alter it, and then read the dump back into the table. There are a couple of problems though:

  • Triggers may have been defined. They may trigger when you truncate the table and/or when you import it from the file. Since there is (surprisingly) no way to temporarily disable these, you need to drop them first, and recreate them afterwards. 
  • Foreign key constraints and unique indexes can make the process very slow, and can lead to all kind of cascading actions we don't want. We don't need those checks, since we will restore the table to (almost) exactly the same state, so we need to temporarily disable them.  

DISCLAIMER: I cannot be held responsible for any damage to your database as a result of the steps I describe here. It's always a good idea to make a full backup of your database before you make any changes. 

So here's the procedure: 

  1. Make sure you have exclusive access to the database. Put up your 'maintainance' page or whatever. 
  2. Check which foreign key constraints are referencing this column. You can do this using the following query (assuming that the table is called my_table and the schema is my_schema): 

    select constraint_name, table_name, column_name from information_schema.key_column_usage where referenced_table_name = 'my_table' and referenced_column_name = 'id' and constraint_schema = 'my_schema'; 

    This will produce a nice list of foreign keys that reference this column. 

  3. Drop those foreign key constraints. Be sure to make a note of them though, so you can restore them later.
  4. Check whether there are any triggers connected to this database, using the show triggers command. Again, make a note of them (better yet: dump them to a file) so you can restore them later. Now drop those triggers. 
  5. Dump my_table into a file. You will need FILE privileges for this:

    select * from my_table into outfile '/tmp/mytabledump';

    Depending on the size of the table, this may take a few minutes. 

  6. Disable foreign key checks and unique checks:

    set foreign_key_checks = 0;
    set unique_checks = 0;
  7. Truncate the table:

    truncate table my_table:

  8. Alter the table. For example:

    alter table my_table modify column id unsigned integer not null auto_increment;  

  9. Modify the foreign keys referencing this column as well so that they are of the same type. 
  10. Recreate the foreign key constraints you dropped in step 3. 
  11. Import your dump back into the table:

    load data infile '/tmp/mytabledump' into table my_table;

    This step takes the longest. On our table (which contained a few million forum replies) it took half an hour. Of course this depends on the speed of your machine, the amount of memory, etc. 

  12. Recreate the triggers you removed in step 4 (if any).  
  13. Re-enable foreign_key_checks and unique_checks:

    set foreign_key_checks = 1;
    set unique_checks = 1; 

  14. Profit! 
24 Jun 2011

Symfony: sfMemcacheCache and removePattern(): potentially huge overhead

Memcached by itself does not allow removing entries by a matching a pattern on the cache keys. After all, it's Memcached's simplicity that makes it so darn fast. 

"Fortunately", Symfony's sfMemcacheCache implementation has a workaround. When the storeCacheInfo option is enabled, it keeps a list of all cache entries within its "namespace". Whenever you add an entry, it gets the list from Memcached, adds an entry to it, and then puts it back into Memcached. When you want to remove entries by pattern, it gets the list from Memcached, searches that list for matches, deletes those entries from Memcached and from the list, and then puts the list back into Memcached. 

Now this is fine for a small site with a limited number of cache-keys (although I'm not sure about race hazard). But for a site with a large or dynamic number of cache-keys, the list can grow to enormous proportions. I was once analyzing a site with severe performance issues. I used a profiling tool to see why simple requests were taking up so much memory and time. Then I found that Symfony was actually downloading and uploading a list of 12 megabytes in size from/to Memcached after every 'put' or 'remove' operation. "That can't be good for performance", was my first thought. 

To fix this, we went looking for all the 'removePattern' invocations in the code, and changed all of them to something more efficient. Then we disabled storeCacheInfo and performance went up dramatically. 

 

20 May 2011

Calculating someone's next birthday using a MySQL function.

I recently needed a MySQL-query to select all users who have a birthday coming up soon. To make this easier, I came up with the following function:

CREATE FUNCTION next_birthday(d DATE)
RETURNS DATE READS SQL DATA
RETURN IF (
 (CONCAT(YEAR(CURDATE()), '-' , MONTH(d) , '-', DAY(d)) < CURDATE()), 
 (CONCAT(YEAR(CURDATE()) + 1, '-' , MONTH(d) , '-', DAY(d))), 
 (CONCAT(YEAR(CURDATE()), '-' , MONTH(d) , '-', DAY(d)))
);

Given a date of birth, this function returns the next occurence of this date (including the current date).
There must be a nicer way to do this though. If you know it, I would love to hear it.

 

20 May 2011

Installing oauth.so for PHP 5.3 in MAMP with Mac OS Snow Leopard

Installing via PECL didn't really work out for me. I found something here, but I had to change some stuff to make it work. So here's what I did: 

  • Install Apple XCode Developer Tools if you don't have it already. 
  • You will need the PHP header files. They should be in you Mac OS SDK folder, but phpize expects them in your php directory, so create a symlink: 
    ln -s /Developer/SDKs/MacOSX10.6.sdk/usr/include/ /Applications/MAMP/bin/php5.3/include 
  • Download the module here: http://pecl.php.net/get/oauth
  • Unpack it and cd to the directory you unpacked it in (for me this was ~/Downloads/oauth-1.1.0/oauth-1.1.0)
  • Run this command:
    sudo /Applications/MAMP/bin/php5.3/bin/phpize
     
  • Configure with the following command:
    MACOSX_DEPLOYMENT_TARGET=10.6 CFLAGS='-O3 -fno-common -arch i386 -arch x86_64' LDFLAGS='-O3 -arch i386 -arch x86_64' CXXFLAGS='-O3 -fno-common -arch i386 -arch x86_64' ./configure --with-php-config=/Applications/MAMP/bin/php5.3/bin/php-config

  • Run make

  • Copy the module to your extensions directory
    cp modules/oauth.so /Applications/MAMP/bin/php5.3/lib/php/extensions/no-debug-something-blabla/

  • Enable the module by adding this to your php.ini:
    extension=oauth.so 

  • Restart apache
    sudo apachectl restart

  • Check your php_info() to see if it's working. 

I had to recreate the steps from my memory and my bash history so I hope I didn't miss anything. If it doesn't work for you, please comment. If it did work, feel free to comment as well :)  

 

5 Dec 2010

Subversion commit problems.

Yesterday I was trying to commit some changes to a moderately large Java project I am working on. I do not know how it happened, but somehow, my working copy got corrupted, causing Subclipse to refuse to commit my changes, printing only the message "skipped".

In an attempt to get a more meaningful error message, I tried the command line version of svn. This one told me that one of my files was "out of date", so I tried an svn update. The svn update didn't result in any change though. All it said was "at revision ..." with the current revision number. 

I tried reverting the file that svn said was out of date. I copied the changes somewhere else, to be able to redo them afterwards. To be honest, I do not remember exactly what I did (sorry about that), but at some point, I got the following message from svn: 

 

Transmitting file data .......................svn: Commit failed (details follow):
svn: Delta source ended unexpectedly

I had never seen this error before. I tried various things, excluding some files from the commit, running svn cleanup, etc. (Again, sorry for not having recorded exactly what I did). At some point the message svn gave me was the following:

 

Transmitting file data .svn: Commit failed (details follow):
svn: Cannot write to the prototype revision file of transaction '777-1' because a previous representation is currently being written by this process

I decided that this was not going to work, and I had to take more drastic measures.
What I did in the end was this: 

  • I created a unified diff of my working copy (using subclipse's create patch command) against the head of the repository. 
  • I made a fresh checkout from the repository, creating a new project in my workspace
  • I applied the patch created in the first step to this fresh checkout (again, in subclipse). I excluded some delete and add combinations which I knew were actually files that had moved to a new directory or files that were renamed. So I only patched the files that hadn't moved and hadn't been renamed. 
  • After that, I manually did the required svn move commands. 
  • To patch the contents of those moved files, I exported them from my corrupt working copy and dragged those exported files from the filesystem onto eclipse's package explorer on top of the existing files, overwriting the files. Now from svn's point of view, those files were first moved and then changed, which is what I wanted. 
  • I did a 'compare to.. each other' between the two projects in eclipse to make sure they were now identical, content-wise. 
  • I finally committed the changes from my new project to the repository. 

 

.. and svn accepted my commit!

Now you may wonder why I didn't just apply the patch literally, but my feeling was that the working copy must have become corrupted due to some incorrect move / rename actions (as this has happened to me before) and I was worried that I would lose history on those files. That's why I decided to redo those move actions manually. 

I hope this helps someone having the same problems. 

 

10 Nov 2010

How to get a Symfony task to autoload the same classes as your web app.

I recently found out that a CLI task in Symfony 1.4 doesn't necessarily autoload the right classes when using symfony's cascading system. For example, I had a ForumReply class with a getUser() method that would return an object of the class sfGuardUser. However, inside the task, it was returning the sfGuardUser implementation of the plugin, rather than my custom implementation located in /lib/model, which would usually be returned inside the web application. 

I solved the problem by manually registering the autoloader like this:

$autoload = sfSimpleAutoload::getInstance(sfConfig::get('sf_cache_dir').'/project_autoload.cache');
$autoload->loadConfiguration(sfFinder::type('file')->name('autoload.yml')->in(array(
  sfConfig::get('sf_symfony_lib_dir').'/config/config',
  sfConfig::get('sf_config_dir'),
)));
$autoload->register();

Hope this helps someone having the same problem. 

Bart van Wissen's Space

I'm a programmer working primarily with Symfony, and recently Symfony2. In my spare time I'm also developing in Java for the Android platform.