“...I've been working since 2008 with Ruby / Ruby on Rails, love a bit of Elixir / Phoenix and learning Rust. I also poke through other people's code and make PRs for OpenSource Ruby projects that sometimes make it. Currently working for InPay who are based in Denmark...”

Rob Lacey
Senior Software Engineer, Copenhagen, Denmark

Codemaster's Dizzy returns

Old blog posts I never published, except I have now #5

The first time I used a computer I was about 7 I think. My mum was always ahead of her time and I actually cite her (or blame her) for being such a geek and getting interested in computers in the first place. She bought me my first computer the Amstrad CPC 6128. We got it 2nd hand I remember going round to the owners house and leaving with a pile of boxes of bits, games, manuals and other books.

I don’t recall the time I bought Treasure Island Dizzy but I remember being completely immersed in it, the puzzles and going back and forth trying to work out which items I’d need to get past each stage and open those locked rooms. I might even say this encouraged me to pay attention to detail at such a young age.

After so many years of 3D games. I’ve played them and I just don’t get on with them. Mario 64 on the Nintendo DS just annoyed me. I think World Of Warcraft is the only one I’ve stuck with. I really miss the 2D platform games I enjoyed during my formative years and I wish they still made them. I am so pleased to see now that Dizzy is making a comeback on iPhone, iPad and Android platforms. I did a little dance and I hunted high and low for information on the old games. I found the fabulous YolkFolk Dizzy fan site which has basically everything you need.

It seems also you can get Dizzy clones of the original games (as close to the original as possible) with Spectrum graphics. Presumably they are cloned to avoid copyright issues. There is even a Dizzy game engine named Dizzy Age which programmers can build their own Dizzy games with, and it was this engine that was used to build the clones of the original games.

Getting back on track, the lovely people at Codemasters have brought Dizzy back to life with a remake of Dizzy – Prince of the YolkFolk for modern mobile platforms. It’s between £1.49 and £2.49 depending on what your platform of choice is. I need to pinch Kat’s Motorla Xoom in order to play as my HTC isn’t supported. Dang.

They are running ongoing competitions on their Dizzy Game Facebook page which you should checkout. Always enter competitions…because even I managed to win myself some badges which I can wear with pride down the pub.

SSH Tunnelling

Old blog posts I never published, except I have now #3

ssh root@xen1.robl.me -R :3002:127.0.0.1:3000 sleep 99999
root@li38-149:~# netstat -ltanup
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
......
tcp        0      0 0.0.0.0:3002            0.0.0.0:*               LISTEN      13807/sshd: root@no
......

Redirecting STDERR

So you don’t like STDERR vomming all over your console?

This script almost works but doesn’t redirect anything that is already hold the original file descriptor, e.g. C extensions.

#!/usr/bin/env ruby

STDERR.puts "Something"
$stderr = File.open(File::NULL, 'w')
STDERR.puts "...and something else"

This however reopens the same file descriptor and covers all bases.

#!/usr/bin/env ruby

STDERR.puts "Something"
$stderr.reopen('/dev/null', 'w')
$stderr.sync = true
STDERR.puts "...and nothing"

And we get…

./test.rb 
Something

yes blocks and metaprogramming are very clever...

…but if you can’t read the code without bashing your head against a wall, its pretty useless.

Old blog posts I never published, except I have now #2

Now I admit that this solution has some merit, it filters incoming params in a particular controller action and DRYs up some of the saving and redirecting that is common place in every controller. But when I took this project on it just complicated things so much that it took 4 times as long to make any changes to the app as it needed to.

do_object_edit("recruiter/new_subscription", :pay_subscription, :agreed_terms, :subscription_type_id) do |o|
  o.transaction_detail = "INCOMPLETE"
  o.amount_paid = 0
end
def do_object_edit(template, action, *fields, &block)
  logger.debug "in do_object_edit"
  logger.debug "allowing edit of #{fields.join(', ')}" unless fields.empty?
  if submitted_using_button?("Cancel")
    if action.is_a?(Symbol)
      redirect_to :action => action
    else
      redirect_to action
    end
    return
  elsif request.post?
    fields.map!{|f| f.to_sym}
    params[:object].delete_if{|k,v| !fields.include?(k.to_sym)} unless fields.empty?
    @object.attributes = params[:object]
    begin
      ActiveRecord::Base.transaction do
        if block_given?
          yield @object
        end
        @object.save!
        flash_message "Details Saved"
        if action.is_a?(Symbol)
          redirect_to :action => action
        else
          redirect_to action
        end
        return
      end
    rescue ActiveRecord::RecordInvalid
      logger.warn $!
      flash_message "An Error Occurred"
    end
  end
  render :template => template
end

Yes is DRY because this is repeated everywhere in some form or other, but its not as readable as this.

s = Subscription.new(params[:subscription]) do |s|
  s.transaction_detail = 'INCOMPLETE'
  s.amount_paid = 0
end

unless s.save
  redirect_to failure
else
  redirect_to success
end

So as another developer taking on a dead project, this just made things harder. So my comment really is, coding an application isn’t just for you its for the client and if you can’t read it easily a month after you’ve written it someone else won’t be able to either.

It stinks of coding arrogance over creating a maintainable project. And the client loses…because it takes 5 times longer to get someone to fix it or make changes in the future.

Ruby &&

Old blog posts I never published, except I have now #1

I really like that the && operator in Ruby returns the final argument, or the result of the last evaluation

rl@bloodandguts:~/repos$ irb
>> "take" && "give"
=> "give"
>> "take" && "give" || "take" && "blum"
=> "give"
>> "take" && false || "take" && "blum"
=> "blum"
> "take" && Numeric.new || "take" && "blum"
=> #<Numeric:0x7f2841bffa70>

Shrinking PDFs

Shrinking PDFs so that they aren’t 50Mb for 10 pages is always a good thing to remember. This command line tool form Adobe is particularly helpful

ps2pdf -dPDFSETTINGS=/ebook ~/Desktop/AnimalHealthCertificate.pdf ~/Desktop/AnimalHealthCertificate-compressed.pdf

Now this Animal Health Certificate will actually get through and not bounce.

rl@loathsome ~ % ls -lah ~/Desktop/AnimalHealthCertificate*
-rw-r--r--@ 1 rl  staff   3.1M 21 Sep 11:01 /Users/rl/Desktop/AnimalHealthCertificate-compressed.pdf
-rw-r--r--@ 1 rl  staff    39M 21 Sep 10:59 /Users/rl/Desktop/AnimalHealthCertificate.pdf

What process is running on port 3000?

Can’t start your Rails app because it’s already running but you can’t find it?

rola@INPAY-RNQP709MTC backend_v2 % sudo lsof -i :3000
COMMAND     PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
Microsoft 14734 rola   28u  IPv6 0xb9dec1e002660162      0t0  TCP localhost:63852->localhost:hbci (ESTABLISHED)
ruby      42857 rola   14u  IPv4 0x2e7917eea3a29d7b      0t0  TCP localhost:hbci (LISTEN)
ruby      42857 rola   16u  IPv6 0xfe39e6fb86a27f74      0t0  TCP localhost:hbci (LISTEN)
ruby      42857 rola   22u  IPv6 0x5c9b52f85ccf9a54      0t0  TCP localhost:hbci->localhost:63589 (CLOSED)
ruby      42857 rola   23u  IPv6 0xb5673178bc310d9b      0t0  TCP localhost:hbci->localhost:63590 (CLOSED)

Assignment still proceeds even with undefined local variable or method

This week I had a really horrible bug. One spec in our suite in our CI knocked out every subsequent spec after skipping. The spec works in isolation. just not on CI and not in a full suite run.

RSpec.shared_contexts 'with some feature' do
  skip('This is not available on CI') if ENV['CI']
  original_driver = Capybara.javascript_driver

  Capybara.javascript_driver = :something_else
ensure
  Capybara.javascript_driver  = original_driver
end

RSpec.describe 'something' do
  include_context 'with some feature'
  it do
    visit '/'
  end
end

Turns out our Capybara.javascript_driver ended up being nil, and our test suite fell over because CI doesn’t support non-headless Chrome. And the reason…

I would appear that variable assignment in Ruby still assigns nil if we attempt to assign a variable from another variable or method that is undefined.

irb(main):001> a = some_method_or_var_that_is_not_defined
(irb):1:in `<main>': undefined local variable or method `some_method_or_var_that_is_not_defined' for main (NameError)

a = some_method_or_var_that_is_not_defined
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
	from <internal:kernel>:187:in `loop'
	from /Users/rl/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/irb-1.14.0/exe/irb:9:in `<top (required)>'
	from /Users/rl/.asdf/installs/ruby/3.3.0/bin/irb:25:in `load'
	from /Users/rl/.asdf/installs/ruby/3.3.0/bin/irb:25:in `<main>'
irb(main):002> a
=> nil

Who knew? I didn’t.

Ruby 3.3.0 and Resque

Just deployed a new Ruby 3.3.0 branch to staging and it would appear that our Resque jobs just stopped working. Usual procedure check the logs, look for an error and backtrace. Not logs. Ok there is no log/resque.log. There is nothing in the staging.log. Ok. So the process that kicks off our service is monit. Monit log.

/var/log/monit.log

[UTC Feb 21 09:12:46] error    : 'resque-scheduler' process is not running
[UTC Feb 21 09:12:46] info     : 'resque-scheduler' trying to restart
[UTC Feb 21 09:12:46] info     : 'resque-scheduler' start: '/etc/init.d/resque-scheduler start'
[UTC Feb 21 09:13:16] error    : 'resque-scheduler' failed to start (exit status 0) -- no output
[UTC Feb 21 09:15:16] error    : 'resque' process is not running
[UTC Feb 21 09:15:16] info     : 'resque' trying to restart
[UTC Feb 21 09:15:16] info     : 'resque' start: '/etc/init.d/resque start'
[UTC Feb 21 09:15:46] error    : 'resque' failed to start (exit status 0) -- '/etc/init.d/resque start': /etc/init.d/resque: 12: kill: No such process

Brilliant, it states it has exit status 0 and nothing else. OK. Script tells us that it should boot the process, nothing out of the ordinary here. Nothing to indicate that it is locked to an older Ruby version or anything.

/etc/init.d/resque

PID=/home/deploy/app/shared/pids/resque.pid
CMD="cd /home/deploy/app/current; PIDFILE=$PID BACKGROUND=yes bundle exec rake environment resque:work QUEUE=* RAILS_ENV=production"
echo $CMD

It is backgrounded though, if there was an error it wouldn’t end up in STDOUT. Ok, how about we boot without the background option.

$ bundle exec rake environment resque:work QUEUE=* RAILS_ENV=staging
rake aborted!
NoMethodError: undefined method `[]' for nil
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/resque-2.0.0/lib/resque/logging.rb:8:in `log'
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/resque-2.0.0/lib/resque/logging.rb:13:in `info'
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/resque-2.0.0/lib/resque/worker.rb:854:in `log'
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/resque-2.0.0/lib/resque/tasks.rb:19:in `block (2 levels) in <main>'
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/rake-13.0.6/exe/rake:27:in `<top (required)>'
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/bundler-2.5.5/lib/bundler/cli/exec.rb:58:in `load'
/home/deploy/app/shared/bundle/ruby/3.3.0/gems/bundler-2.5.5/lib/bundler/cli/exec.rb:58:in `kernel_load'

Ah, there is is. So the Resque::Logging.log proxies onto the Resque.logger if it is defined.

module Resque
  # Include this module in classes you wish to have logging facilities
  module Logging
    module_function

    # Thunk to the logger's own log method (if configured)
    def self.log(severity, message)
      Resque.logger.__send__(severity, message) if Resque.logger
    end

    # Log level aliases
    def debug(message); Logging.log :debug, message; end
    def info(message);  Logging.log :info,  message; end
    def warn(message);  Logging.log :warn,  message; end
    def error(message); Logging.log :error, message; end
    def fatal(message); Logging.log :fatal, message; end
  end
end

Let’s try it.

% rails c
Loading development environment (Rails 6.1.7)
rirb: warn: can't alias measure from irb_measure.
irb(main):001> Resque.logger.debug('blah')
/Users/rl/.asdf/installs/ruby/3.3.0/lib/ruby/3.3.0/logger.rb:384:in `level': undefined method `[]' for nil (NoMethodError)

    @level_override[Fiber.current] || @level

Ah, that’s a problem. The Resque.logger we’re using seem incompatable with Logger.

And we’re not the only ones to see this. https://talk.jekyllrb.com/t/error-when-executing-bundle-install/8822/6

It would appear that Logger has changed, there is a fix in Jekyll 4.3.3 for their issue. So what’s our issue.

https://github.com/jekyll/jekyll/commit/595cc230678952fab244a62dc1811ea39ec34041

Resque by default defines the Resque.logger as an instance of MonoLogger. Never heard of it.

lib/resque.rb

# Log to STDOUT by default
Resque.logger           = MonoLogger.new(STDOUT)
Resque.logger.formatter = Resque::QuietFormatter.new

Our Gemfile.lock reckons we’re using 1.1.1 of mono_logger

Gemfile.lock

mongoid_paranoia (0.5.0)
       mongoid (~> 7.3)
     mono_logger (1.1.1)
     msgpack (1.7.2)
     multi_json (1.15.0)
     multi_xml (0.6.0)

There is a new patch version of mono_logger available. And there it is in the commit history.

https://github.com/steveklabnik/mono_logger/commit/14bfb8302609123d702ace527c9114a8ff7c438a

They have also fixed it. Logger initializer has changed, so if you’re going to use it sub-class Logger use their initializer. Upgrading to bundle update mono_logger bumps the version to 1.1.2 and fixed our issue.

Bundler issue on Ruby 3.3.0 Upgrade

Honestly, Ruby 3.2.2 to Ruby 3.3.0 upgrades have been really painless. I’ve hit every server and installed Ruby 3.3.0 with YJIT enabled and we’re ready to go as soon as the new Ruby_3.3.0 branches specs pass.

However, this one is rather annoying.

% bundle install
Fetching gem metadata from https://rubygems.org/........
Resolving dependencies....................
--- ERROR REPORT TEMPLATE -------------------------------------------------------

NoMethodError: undefined method `version' for an instance of Bundler::IncompleteSpecification

This current Gemfile.lock indicates we last bundled this with Bunder 2.3.20. I know the latest version is 2.5.5

RUBY VERSION
   ruby 3.2.2p53

BUNDLED WITH
   2.3.20

Ok in the Gemfile we have locked to a specific version of bundler, I’m not sure why. I’d think that bundler is pretty safe to allow the host system to have whatever version is installed manage this.

gem 'bundler', '~> 2.3.8'

If I remove this line from the Gemfile, nothing changes. I still get Unfortunately, an unexpected error occurred, and Bundler cannot continue. It’s expecting that version of bundler to manage it still.

Bundler       2.3.20
  Platforms   ruby, arm64-darwin-23
Ruby          3.3.0p0 (2023-12-25 revision 5124f9ac7513eb590c37717337c430cb93caa151) [arm64-darwin-23]
  Full Path   /Users/rl/.asdf/installs/ruby/3.3.0/bin/ruby
  Config Dir  /Users/rl/.asdf/installs/ruby/3.3.0/etc
RubyGems      3.5.3
  Gem Home    /Users/rl/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0
  Gem Path    /Users/rl/.gem/ruby/3.3.0:/Users/rl/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0
  User Home   /Users/rl
  User Path   /Users/rl/.gem/ruby/3.3.0
  Bin Dir     /Users/rl/.asdf/installs/ruby/3.3.0/bin
OpenSSL       
  Compiled    OpenSSL 3.2.0 23 Nov 2023
  Loaded      OpenSSL 3.2.0 23 Nov 2023
  Cert File   /opt/homebrew/etc/openssl@3/cert.pem
  Cert Dir    /opt/homebrew/etc/openssl@3/certs
Tools         
  Git         2.39.3 (Apple Git-145)
  RVM         not installed
  rbenv       not installed
  chruby      not installed

Even if 2.5.5 is the default.

% gem list bundler

*** LOCAL GEMS ***

bundler (2.5.5, default: 2.5.3, 2.4.21, 2.4.0, 2.3.20)
bundler-audit (0.9.1)
capistrano-bundler (2.1.0, 2.0.1)

So I have to force bundler to use the correct version for this bundle install to work. And success.

bundle _2.5.5_ install
---8<---
Bundle complete! 115 Gemfile dependencies, 321 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.