Lazy Updates

This week for So Play We All I’ve given myself another reminder of the importance of scheduling. We only budgeted 3 hours, but I flew to my home base of Chicago and have been very busy catching up with friends and family. I could’ve gotten code done earlier, but instead here I am up against the deadline and coding poorly due to sleep deprivation.

Meanwhile, in last week’s response, Jim continued to show off his superior time-management skills and argue his long-term approach. I think Jim’s gone too far towards YAGNI in building so much up-front, but I can’t fault his steady progress. This is a great post to read if you’re curious about why he’s visibly behind.

I started with three small things, first fixing a unique index. The combination of x and y should be unique for each plat, instead I had two indexes specifying that x should be unique and y should be unique. Oops. Simple fix with a migration, though.

Next, I updated the credits to mention I’m the one developing the game as well as include a few more libraries I’ve added. The last small thing was fixing the cities I created in the database, which all had 0 for x and y because I forgot I had set them to attr_protected.

I spent the bulk of this week’s time on lazy column updating. The player stores up “travel time” at a rate of 1 second per second. If you’re offline for an hour, when you log in you can travel anywhere within an hour’s distance.

There are basically two approaches to this. The simpler way is to have a program run regularly (usually via cron) to update the column. In this case, a program that runs every five minutes would add 300 seconds to each player’s travel time.

But this is very inefficient. It updates players who may not log in for hours or days (or never again), and forces players who are active to wait several minutes between updates.

The other approach is to store the last time the player’s travel time was
updated and recalculate how much time they have lazily, only when the game
requests it. This is the approach I’m taking.

It’s pretty fiddly code, though, with plenty of opportunity for subtle errors to creep in. I’ve been writing this part in a test-driven development style and it’s helped a lot. I’m not done, but without tests I’d have to keep the whole program in my (sleep-deprived) head and try to remember to test all the little edge cases. Instead I have a nice little suite that runs down all the use cases and all the ways I’ve managed to break the code even as I’m writing it. The earlier a test fails, the sooner I can fix problems.

So nothing user-visible this week, but let me share those tests so you can see some of the ways this lazy updating works:

class LazyUpdatesTest < ActiveSupport::TestCase
  def setup
    Timecop.freeze
    @p = Player.new
  end

  def teardown
    Timecop.return
  end

  test "calls lazy_update for column" do
    @p.expects :lazy_update_travel_time
    @p.travel_time
  end

  test "does not interfere with default value" do
    assert_equal 0, @p.travel_time
  end

  test "doesn't change value with no time change" do
    assert_equal @p.travel_time, @p.travel_time
  end

  test "passes initialized value into update function" do
    p = Player.new
    p.travel_time = 100
    @p.expects(:lazy_update_travel_time).with(100, 0)
    p.travel_time
  end

  test "accepts new values for attributes" do
    p = Player.new
    p.travel_time = 100
    assert_equal 100, p.read_attribute(:travel_time)
  end

  test "doesn't changed initialized value with no time change" do
    p = Player.new
    p.travel_time = 100
    assert_equal 100, p.travel_time
  end

  test "passes default old value and delta to update function without time change" do
    @p.expects(:lazy_update_travel_time).with(0, 0)
    @p.travel_time
  end

  test "passes non-default old value and changed delta to update function" do
    p = Player.new
    p.travel_time = 100
    Timecop.travel(10.seconds)
    p.expects(:lazy_update_travel_time).with(0, 10)
    #assert_equal 110, p.travel_time
  end

  test "updates value"
  test "updates updated_at column"
end

As I finish this code and it gets a chance to mature I'll probably bundle it as a Ruby gem and release it as open source. Which will help Luke, but oh well.

I know it's not splashy user-facing, but it's solid progress on an efficient game, backed by tests to ensure reliability. Please vote for Oaqn in this week's poll.

5 Responses to “Lazy Updates”