PHP, MySQL, Dates & Timezones

Nov 20 2012 Published by under Technology

I’ve recently had the displeasure of working with timezones on my main project at work. I work on an internal administrative system for Buffalo Studios. It has all sorts of functionality, such as content editing tools, customer service tools, reports, graphs and more. Its sole purpose is to make the lives of our internal customers easier, as well as to provide some functionality that doesn’t belong in our game environments.

When my project (let’s call it Admin) initially started, the servers it was running on were all in the America/Los_Angeles time zone. Everything from Apache to MySQL, all in America/Los_Angeles. A few databases Admin connected to were in one other time zone – UTC. When Admin was first built, it was built using the initial version of our internal PHP Framework. This framework had no knowledge of timezones and the like, since it was designed for our games, which are all run on UTC and don’t have to worry much about timezones.

With this in mind, Admin had a small utility to convert UTC dates over to America/Los_Angeles. Any date coming out of these alternate databases were stored in UTC, so a quick PHP conversion for display and I was good. Until recently.

The most recent Daylight Savings Time switch this fall caused some date & time problems for us. Not all of our servers were on UTC, and the fact that the same hour happened twice in a row caused some problems for some of our automated scripts. The decision was made to switch Admin’s servers on over to UTC at our earliest convenience. I didn’t think this would be a big problem, but it sure turned out to be.

The first thing I needed to do was to switch my local MySQL database to UTC for testing purposes. We were going to leave the app itself in America/Los_Angeles, because that’s the timezone that the vast majority of Buffalo operates in. So the system would be UTC and the app would display dates in America/Los_Angeles. That meant that all dates coming out of Admin’s database, after the switch to UTC, needed to have the timezone conversion done to them.

At first, I made this change within PHP itself. The funtionality that allowed me to easily say “this date is UTC, convert it” was extended to all dates coming out of the Admin database. Unfortunately, this posed a problem on our QA environment. QA Admin was still running in America/Los_Angeles, because it wouldn’t be converted to UTC until after our upcoming code release. So dates were borked in the meantime with the app assuming that the source data was in UTC.

Normally I would have just switched the QA server to UTC, but that’s not possible for us yet. QA Admin resides on the same machine as Dev Admin… As well as Production Admin’s MySQL database. So I couldn’t make that switch because it would adversely impact the Production database (this is being remedied early next week – Production Admin’s database will be split to its own dedicated set of servers!). So I figured the dates would just be temporarily borked during testing.

But testing couldn’t be performed properly because there were new scheduler features in some of Admin’s functionality. Being able to delay certain actions until later, for example. So how could the QA department properly test this date-centric functionality? Additionally, what if we needed to make timezone switches again?

I was willing to write these problems off, until I got one more request that made me re-evaluate how Admin was handling dates. The head of our IT department requested the ability for users to be able to set their timezone. We have some users around the world, so it makes sense for them to be able to view dates in their timezone. Thus… I had to rework the way that Admin handled dates.

The first step was for the app to automatically set the default timezone upon load. This was easy:

ini_set('date.timezone', 'America/Los_Angeles');

The next step was to have the user’s timezone preference set as the default as early as possible. This was accomplished in much the same way. We’d select the user’s timezone out of the database and then run the same PHP function above with that result.

One thing I wanted to use was the ability to tell MySQL what my connection’s timezone was. MySQL has the ability to set the system and connection timezone. If you set your connection timezone to something other than the system timezone, then dates that are selected from MySQL are automatically adjusted to your connection’s timezone. This was accomplished by doing the following, in PDO after the connection was established:

$statement = $pdo->prepare('SET time_zone=?');
$statement->execute(array(
    date_default_timezone_get()
));
$statement->closeCursor();

I’m using date_default_timezone_get() because the database connection could be made at any time, after a possible timezone switch. The thing is, though, is that the database connection to get the user’s timezone has already been established. Once I’ve retrieved that timezone, I basically need to set the timezone for any active database sessions:

foreach (self::$databases as $database)
{
    $statement = $database->prepare('SET time_zone=?');
    $statement->execute(array(
        $timezone
    ));
    $statement->closeCursor();
}

The next problem I ran into: not all of our databases are MySQL. Not all of them support setting the timezone. Thus, I still had to backup on converting the dates out of databases to the app’s timezone. Can’t win them all, right? But I had to rework the way the timezone conversion ran. We had a convenience function that did timezone conversion and formatting for output, all-in-one:

static public function dateTimeFormat($timestamp, $isUtc = true, $format = self::DEFAULT_DATETIME_FORMAT)
{
    if ($timestamp instanceof DateTime)
    {
        $dateTime = $timestamp;
    }
    else
    {
        $dateTime = new DateTime;
 
        if (is_scalar($timestamp))
        {
            if (!preg_match('#^[0-9]+$#', $timestamp))
            {
                $timestamp = strtotime($timestamp);
            }
 
            $dateTime->setTimestamp($timestamp);
        }
    }
 
    if ($isUtc)
    {
        $localTimezone = new DateTimeZone('America/Los_Angeles');
        $dateInterval = DateInterval::createFromDateString($localTimezone->getOffset($dateTime) . ' seconds');
 
        $dateTime->add($dateInterval);
    }
 
    return $dateTime->format($format);
}

The problem here is that it assumed America/Los_Angeles for the target timezone… And only allowed conversion from UTC. Well, we now needed to support all target timezones. This function was used a lot in Admin, so it took me a long time to make the conversion over to the following functions:

static public function dateTimeFormat($timestamp, $format = self::DEFAULT_DATETIME_FORMAT)
{
    if ($timestamp instanceof DateTime)
    {
        $dateTime = $timestamp;
    }
    else
    {
        $dateTime = new DateTime((preg_match('#^[0-9]+$#', $timestamp) ? '@' : null) . $timestamp);
    }
 
    return $dateTime->format($format);
}
 
static public function convertFromTimezone($timestamp, $fromTimezone)
{
    if ($timestamp instanceof DateTime)
    {
        $dateTime = $timestamp;
    }
    else
    {
        $dateTime = new DateTime((preg_match('#^[0-9]+$#', $timestamp) ? '@' : null) . $timestamp, new DateTimeZone($fromTimezone));
    }
 
    $dateTime->setTimezone(new DateTimeZone(date_default_timezone_get()));
 
    return $dateTime;
}
 
static public function convertToTimezone($timestamp, $toTimezone)
{
    if ($timestamp instanceof DateTime)
    {
        $dateTime = $timestamp;
    }
    else
    {
        $dateTime = new DateTime((preg_match('#^[0-9]+$#', $timestamp) ? '@' : null) . $timestamp);
    }
 
    $dateTime->setTimezone(new DateTimeZone($toTimezone));
 
    return $dateTime;
}

With these functions, I could properly take any date out of a non-timezone-supported database and convert to the app’s timezone. Additionally, if I needed to insert to these databases, I could convert to their timezone on the way in, as well. The only problem is that any of those databases could just change their timezones in the future, and it would take a bit of time to convert all of the code over if that happened. My next step on that problem, that I have yet to do, is to define constants associated with those databases so that I can reference the constant and only change it once if I ever have to. Or, I could build a way to make that conversion in our modeling systems.

One last problem I dealt with once I started using these functions – DateTime()’s constructor does indeed accept a Unix timestamp. Unfortunately, I had problems with it staying in the proper timezone. I didn’t read 100% of the DateTime() documentation on PHP.net, and thus missed a point where using a Unix timestamp to instantiate a DateTime() always results in the DateTime() setting itself to the UTC timezone. Something else to keep in mind.

So that’s it, right? No, wrong again. MySQL only does conversions on TIMESTAMP columns. DATE, TIME and DATETIME are not supported. And wouldn’t you know it, one of our games’ legacy tables all use DATETIME. So I couldn’t use automatic conversion for those anyway. All that work I put in on these three new functions would be fruitful for having to continue manually converting some dates even out of a MySQL database.

Before you get your hopes up that my odyssey wrapped up there… Don’t. I’m using Gentoo Linux for my local development machine, and everything had been handled for me when I installed MySQL. But Admin is running on Ubuntu servers, and for some reason the default Ubuntu configuration of MySQL does not include automatic installation of timezone support in MySQL. After some research online, I found that I had to manually populate MySQL’s timezone data, like so:

mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql

Before I did this, I got errors when attempting to set MySQL’s timezone in the connection:

SQLSTATE[HY000]: General error: 1298 Unknown or incorrect time zone: ‘America/Los_Angeles’

After importing from zoneinfo, I was able to set the timezone on the connection. Yet another headache. But, thankfully, once that was done, I was set! I’d handled everything I needed to, so far, to make this conversion work. And these changes all now work on our Dev/QA servers because it doesn’t matter now what timezone MySQL is in for Admin. It automatically converts the dates on the way out. Since it’s a database I have 100% control over, I was able to make sure all dates were defined as TIMESTAMPS. Actually, the Admin database is the only one with the connection timezone set. Our other MySQL databases are outside my control and they do not have the timezones installed. So I have to manually convert anyway, in PHP.

Something to note, though, is that Admin isn’t fully using DateTime() in the way that I want it to be. It would take a lot of effort to make the full conversion, and that’s time I just don’t have. I had to take an intermediary route instead, splicing the style it used to have with a kind of bridge. If I had to do it over again, I’d do things properly from the get-go.

So here are my recommendations for building a PHP+MySQL app that needs to support timezones. In fact, I’d recommend doing this anyway. It’s good practice, especially if your app will be dealing with dates at all.

  1. Use DateTime() whenever you’re dealing with dates. Don’t use time(). Don’t use date(). Just say no to Unix timestamps in PHP if you can help it.
  2. Make sure MySQL’s timezones are installed and always set the connection timezone.
  3. Always set PHP’s default timezone at the app level, then allow user’s to select their own timezone – this should set both PHP’s timezone and the MySQL connection’s timezone.
  4. If you have to manually convert timezones in PHP, associate the source of the data (i.e. a database) with its known timezone. For example, if you have a named database, something like this: const DBNAME = ‘America/New_York’;
  5. Always use TIMESTAMPs in MySQL. You’ll probably want a DEFAULT value, which would be ’0000-00-00 00:00:00′.

No responses yet

I Am Not A Rock Star [Updated Once]

Aug 22 2012 Published by under Technology, Work

The last 10 months have been pretty busy for me as far as job searches go. Last year Meteor Games started its decline towards bankruptcy, which it filed for earlier this year (so far as I know). The big layoff happened in mid-November, leaving me and a few others as part of a skeleton crew to wrap things up for the remainder of 2011. I started my job search during this End of Days scenario and ended up interviewing at a lot of places, before starting at Buffalo Studios to begin the new year.

Unfortunately, my job search did not end there. Things were not all as they seemed at Buffalo for me, at least early on. I was dissatisfied with some aspects of my job at the time and rather than try to stick things out, I ended up resuming my job search. To be honest, I probably shouldn’t have jumped ship so quickly, but I did. I ended up at a company called Needly. Due to circumstances entirely beyond my control, I ended up getting hired in for a job that I didn’t exactly interview for. An unintentional bait-and-switch resulted in my extreme dissatisfaction. Again my job search resumed, and through an interesting chain of events, I ended up back at Buffalo.

I’m very happy at Buffalo now. The problems I had at the beginning of the year are not present. I work on internal tools and systems. Basically, I’m a support guy for much of the organization. Need realtime graphs built? You can probably come to me. Need tools to edit player data? Me. Need a custom web board to surface customer service issues? I’m your man, Stan.

So during the last 10 months, I think you can say I’m pretty in-tune with the current state-of-affairs within the LAMP stack tech market in west LA. I’ve had a bunch of interviews and some code challenges. I’ve seen countless job requirements floating about. I’ve had an insane number of recruiters constantly getting in touch with me about possible opportunities – and all of that started a few months after I started at Meteor, so that’s been pretty constant. The companies I’ve interviewed at have been extremely varied in terms of what kinds of software they work on, their business practices, their culture and more. I’ve come a long way and with all of this experience, here I am to tell you one simple sentence that will require a lot of explanation.

I am not a rock star.

Many, many job requirements I came across use marketing-style terms such as “rock star” and “change the world.” Obviously you know what “change the world” means, but in terms of software engineers, a “rock star” is basically what you’d imagine them to me – someone who kicks ass, takes names and blows the competition away. Ideally you’d love to have tons of rock stars all over your engineering organization, right? Well, of course you want quality engineers. Aside from them, you’ll also need a good product, good product management, good executives, good financial backing, etc. But good engineers, yes.

Typically these marketing terms are coupled in with job reqs that have all sorts of interesting benefits and perks. I came across one that said the company offered three catered meals per day. Per day, folks! Shit, you’d almost never have to go grocery shopping for your home, save for weekends, right? Other perks are things like stock options, foosball tables and more.

I interviewed at one place before joining Buffalo the first time that had some incredible talent in the building. These guys knew their shit. Actually, I came across a few different places that had some high intelligence in-house. I felt humbled by one of them – Gravity. Man, those guys have sick skills and knowledge. More power to them. Anyway, this one place I interviewed at in late 2011 had this mentality of really enjoying what they work on. Everybody is excited to be there. To revolutionize the industry they were in. To change the world. Dedicated to their craft. Getting the job done.

This was all told to me, of course. I wouldn’t have been able to make this kind of characterization of their engineers (and other professionals) during the time I was there. I got so much marketing speak out of these guys it made my head spin. I had to actually ask numerous questions, some that I’m not comfortable asking in a job interview setting, just to get pertinent details as to what their environment and culture were like.

Basically, this place worked their engineers to the bone. I know someone who is friends with one of the leads at this place. He told me they’re pulling 60- to 70-hour weeks regularly to get shit done. They’re staying late on weekdays and coming in at least one day on almost every weekend. And their product hasn’t even gone to market yet. This was not an environment I wanted to be in.

See, places that use terminology like “rock star” and “change the world” are trying to sell you a job. In my experience, these places are looking for guys that have some talent, of course, but talent that they can burn out in no time flat. I understand that west LA is a startup scene. Long hours may have to be put in. But that’s not the kind of stuff I’m interested in. And potential employers can’t put “May work 50-60 hours per week” on their job reqs – they’ll never hire anybody. So they market the shit out of these things to get people in the door.

These places are trying to market a job to me, and I don’t like it. I am really good at what I do, don’t get me wrong. But I’m not a “rock star” because I want to work a human work week. I like putting in 40 hours and having the remainder of my week to myself to do what I please. Run OMGN. Hang out with friends. Play some games. Watch some TV and movies. I don’t want to spend my life working for someone else’s product. Even if I’ve got a bit of a stake in it, because chances are you’ll never make big bucks at a startup if you’re not a founder. The chances of making it big are too low.

Do I want to change the world? Sure. But I don’t want to sacrifice my life to do it. I want to live my life. Why change the world if I can’t enjoy the change I’ve created? Seriously, I’d rather have no catered meals at work at all and work 40 hours than have all of my food paid for because I’m spending 12 hours per day at the office.

I’m pretty close to being a 501 developer. Go read that link. In general, I think the idea of a 501 developer is one that doesn’t care about their craft or output. This is not the case with me. I take great pride in my work. If I push a bug out to production, I take it personally, especially since right now I have nobody on my projects to code review me. I’ve got a decent amount of autonomy at Buffalo and I like that, but it can lead to pitfalls in my output occasionally. Working with others is a good thing. Just the same, don’t mistake my pride in my work for a willingness to work forever.

A job is a business transaction. You’re paying me $X for Y hours per week. I am selling you my productivity. If I do not perform, you can fire me, just like you may replace a car that turned out to be a lemon. It’s a business transaction, to me. So why would I just up an offer to give you more and more of my precious time for the same amount of money? By staying late each day, my effective hourly rate drops because I’m salaried. Seriously, nobody has infinite time on this planet. Why would I give so much of it up when I could be making my quality of life better by spending time with those that mean the most to me?

This isn’t to say that exceptions can’t be made. If that critical system takes a shit, you’ve gotta get up and deal with it, lest nobody have a job when the morning comes. That’s understandable. Emergencies happen. What is not understandable, to me, is an expectation of working more than 40 hours per week. Adjust your product schedules if that’s the case. Don’t burn out your employees. Have a human working environment. Besides, studies made back when the 40-hour work week became standard show that 40 hours is an optimal number for human productivity. Anything more and you start getting more mistakes and diminishing returns.

I will always see terms like “rock star” and “amped up” for what they are – marketing terms to try to get brogrammers in the door. Some of us have lives, you know. Some of us have significant others and kids. Families. Family is far, far more important than putting in that extra 10 hours because you didn’t give enough time to create THE NEXT BIG THING THAT WILL OVERTAKE FACEBOOK.

[UPDATE]

A former boss/coworker of mine shared an article with me and a bunch of other software engineers we used to work with. I couldn’t find it, but he recently re-shared it. This is an excellent read about tech start-ups, and was part of the reason I even thought about blogging in the first place:

Michael O. Church: Don’t waste your time in crappy startup jobs

No responses yet

Timing Out PHP Soap Calls

Oct 21 2009 Published by under Archive

So I’ve got an interesting technical post for you today. I know I don’t normally post technical things here on my blog, but I felt this was such an interesting exercise in triumphing over a big issue here at work that I wanted to post about it.

We call several third-party vendors for web-based services in my department. We’re really keen on keeping transaction times low, so we have a set timeout for each vendor to ensure we don’t wait too long. Most of our vendor calls are Curl calls, but we have a couple that use Soap.

Curl has built-in functionality to enable a timeout. PHP’s internal Soap library does not. Thus, PHP’s documentation says to enable the following to set a limit on Soap calls:

ini_set("default_socket_timeout", 5); // 5 seconds

This is all fine and dandy. Or, at least, I thought it was. When we first had an outage with one Soap-based vendor, this timeout mechanism worked correctly. However the second time, it did not. We still waited up to 60 seconds for the call despite our code having not changed. Highly distressing is the word.

I researched this issue at great length, and used one of our own Soap services to test out a theory. I put this into the code:

ob_implicit_flush();
echo " ";
sleep(15);

The ob_implicit_flush() function call forces PHP to send any output as it immediately becomes available. Normally, PHP sends it all at the close of the script, or if you use other output buffering functions. Here, I’m forcing some content to be passed back to the caller then sleeping beyond the wait time of 5 seconds.

The results? It waited. So the socket timeout feature in PHP only applies until you receive content. If you receive any content within the socket timeout interval, it will keep the socket open and continue to wait. The timeout actually serves from the opening of the connection to the reception of content, not the entire length of time the socket will remain open.

Thus, I had to find a new route to keeping our Soap calls short. My next attempt was limiting the script execution time via either of these two functions:

set_time_limit(5); // 5 seconds
ini_set("max_execution_time", 5); // 5 seconds

Unfortunately, this didn’t help. First of all, both of these two functions have the exact same effect and use the same underlying PHP functionality. Secondly, they only set limits for internal PHP execution. Any time you have an external data source or blocking system call, this is not calculated in the execution time (at least on Linux; on Windows everything is considered). So any database calls, Soap calls, system calls… These are untimed.

At this point I was at my wits end. I could not figure out a way to limit Soap calls save for building a barebones script to make the Soap call and calling that script with a Curl call.

I ventured into the Soap documentation on the PHP website to see if there was a way I could use the SoapClient class to build the Soap request XML and to parse a Soap response XML into an object, thus allowing me to transport the XML in whatever way I chose. No dice. However, I did discover something interesting while looking at the documentation.

PHP allows you to extend the SoapClient class, I knew that. What I did not know is that you could override certain functions, one of them being __doRequest(). By overriding this function, you can make the request to the remote server however you like.

So I tested this out. And holy crap, it worked. The input to the function is the actual Soap XML, not a Soap object, and the function simply returns the Soap response XML, not an object. It is also passed a few other things, such as the location of the Soap web service. We’re in business.

I built a class extending SoapClient and enabled timeout functionality. When a timeout is used, it actually uses Curl for the call and sets the timeout there. When no timeout is required, it uses the default mechanism to send the request. See part of my class below. It may not be totally robust, but hey, I just needed a timeout. And I couldn’t give you the entire class functionality either; it’s derived from copyrighted code.

class SoapClientTimeout extends SoapClient
{
	private $timeout;
 
	public function __setTimeout($timeout)
	{
		if (!is_int($timeout) && !is_null($timeout))
		{
			throw new Exception("Invalid timeout value");
		}
 
		$this->timeout = $timeout;
	}
 
	public function __doRequest($request, $location, $action, $version, $one_way = FALSE)
	{
		if (!$this->timeout)
		{
			// Call via parent because we require no timeout
			$response = parent::__doRequest($request, $location, $action, $version, $one_way);
		}
		else
		{
			// Call via Curl and use the timeout
			$curl = curl_init($location);
 
			curl_setopt($curl, CURLOPT_VERBOSE, FALSE);
			curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
			curl_setopt($curl, CURLOPT_POST, TRUE);
			curl_setopt($curl, CURLOPT_POSTFIELDS, $request);
			curl_setopt($curl, CURLOPT_HEADER, FALSE);
			curl_setopt($curl, CURLOPT_HTTPHEADER, array("Content-Type: text/xml"));
			curl_setopt($curl, CURLOPT_TIMEOUT, $this->timeout);
 
			$response = curl_exec($curl);
 
			if (curl_errno($curl))
			{
				throw new Exception(curl_error($curl));
			}
 
			curl_close($curl);
		}
 
		// Return?
		if (!$one_way)
		{
			return ($response);
		}
	}
}

23 responses so far