Posterous theme by Cory Watilo

Access request object from models and more

Sometimes for some weird reason you need to use cookie, session or even request objects from controller in models. In my case I needed unique token for each http request inside logic.

First thing got to my mind was to take request object from controller (ActiveDispatch::Request) and grab it’s object_id. But then I realized that I need it in deep layers of application logic, and carrying it from controller would be quite painful.

I tried using a singleton class. It was good idea at first:

# lib/token.rb
require 'singleton'

class Token
  include Singleton

  attr_accessor :token_object

  def self.set(val)
    instance.token_object = val
  end

  def self.get
    instance.token_object.object_id.to_s
  end

end

# app/application_controller.rb
class ApplicationController < ActionController::Base
  before_filter :setup_token

  def setup_token
    Token.set(self.request)
  end
end

I was intended to use Token.get in app.

This way has a potential issue – race condition. If application would not be preforked, but executed in threads, the object could be modified from another thread. I wrote a little test case spawning few threads, setting some value and sleeping for a random time. Value was indeed changed by other threads.

Dmytro Shteflyuk helped me a lot pointing to solution based on thread variables. I found basic idea in Geoff Lane’s blog and came with a simple solution:

class Token
  def self.set(obj)
    Thread.current[:token] = obj
  end

  def self.get
    Thread.current[:token].nil? ? nil : Thread.current[:token].object_id
  end

end

Canceling mail delivery from within mailer

In project I’m working on mail notifications are handled by resque_mailer, and are sent from models in after_create callback. When you call deliver – actually you are calling it on Resque::Mailer::Rails3MailerProxy – it puts mailer class name and arguments into the queue. Argument in our case is id of some stored event, for which user should be notified. Then background worker takes this data from queue, creates mailer, and calls

@mailer_class.send(:new, @action, *@args).message.deliver

And usually this isn’t bad.

But now we are creating records within DB transaction. Under some circumstances transaction could be rolled back. Oh wait! after_create doesn’t know anything about it, and it queues mail notification with object’s id… which isn’t available anymore!

The problem is – how to abort mail delivery from within mailer?

First thing you’d try is return false from mailer action. But mailer will anyway render template, and you’ll get the error.

I didn’t find any solution, so I dug in the mailer code and came with simple wrappers.

First, deliver is actually called on Mail::Message object. In case of error you should replace it with some dummy:

module BulletproofMailer
  class BlackholeMailMessage < Mail::Message
    def self.deliver
      false
    end
  end
end

Ok, but how to feed it into mailer? Here comes another wrapper where we are replacing self.message:

module BulletproofMailer
  class AbortDeliveryError < StandardError
  end

  class Base < ActionMailer::Base

    def abort_delivery
      raise AbortDeliveryError
    end

    def process(*args)
      begin
        super *args
      rescue AbortDeliveryError
        self.message = BulletproofMailer::BlackholeMailMessage
      end
    end
  end
end

Using these wrapper mailer would look like this:

class EventMailer < BulletproofMailer::Base
  include Resque::Mailer
  def event_created(event_id)
    begin
      @event = CalendarEvent.find(event_id)
    rescue ActiveRecord::RecordNotFound
      abort_delivery
    end
  end
end

PS: Yeah, I know we should rewrite mail callbacks, and we will do it. But a bit later.

Edit a few previous commits

Working in feature branch I put wrong issue number to commit message. In fact, I did it not with last commit, but with a two or three in a row.

If this is just a last commit you could easily use git commit --amend.

Finally I found solution on stackoverflow. I’ll quote it here:

If the commit you want to fix isn’t the most recent one:

git rebase --interactive $parent_of_flawed_commit

If you want to fix several flawed commits, pass the parent of the oldest one of them.

An editor will come up, with a list of all commits since the one you gave.

Change pick to reword (or on old versions of Git, to edit) in front of any commits you want to fix. Once you save, git will replay the listed commits.

Git will drop back you into your editor for every commit you said you want to reword, and into the shell for every commit you wanted to edit. If you’re in the shell:

Change the commit in any way you like.

git commit --amend
git rebase --continue

Most of this sequence will be explained to you by the output of the various commands as you go. It’s very easy, you don’t need to memorize it – just remember that git rebase —interactive lets you correct commits no matter how long ago they were.

FreeBSD IP address change

In FreeBSD ip address is written in /etc/rc.conf.

If you changed it and want to restart network interfaces with latest changes, just run

/etc/rc.d/netif restart

But this wouldn’t affect routing. So if you changed default gateway too, you need to run

/etc/rc.d/routing stop
/etc/rc.d/routing start

To view routing table run

netstat -rn

And, finally, to add gateway by hand exec

route add default 192.168.1.254

Many thanks to VIVEK GITE for his post on nixCraft.

Redmine 1.1.1 and rubygems 1.5 issue

After upgrading rubygems Redmine started to display error message

rake aborted!
undefined local variable or method `version_requirements' for #<Rails::GemDependency:0xb7351b0c>

It is described in ticket #7516 of Redmine itself, and workaround is to modify enviroment.rb and add code before initialize section

if Gem::VERSION >= "1.3.6" 
  module Rails
    class GemDependency
      def requirement
        r = super
        (r == Gem::Requirement.default) ? nil : r
      end
    end
  end
end

Prepare a copy of existing host

Today I faced a new task: setup server for stress testing. It will be like our preproduction server – same packages, same disk structure.

To partition a disk the same way as it was partitioned on another host you can use sfdisk:

sfdisk -d /dev/sda > parts.txt

Then copy file to another host and apply it:

sfdisk /dev/sda < parts.txt

I didn’t found simple way to do the same with lvm volumes though.

Next thing – installing the same packages. It is easily done via dpkg/apt.

On host1 do

host1: dpkg –get-selections | grep -v deinstall > selected_pkgs

And on the new host run

dpkg --set-selections < selected_pkgs
apt-get dselect-upgrade

Copying configuration (/etc) and data is simple enough to describe (tar, scp), and it depends on services you are running.

Moving to Posterous

Успешно перевел блог на Posterous. Зачем? Все просто - захотелось чистого, светлого...

На самом деле предпосылки были уже давно - часть материала устарела, часть вообще надо выбросить. Подсветка синтаксиса в старых постах хромает, оформление никуда не годится.

В конце года я опробовал tumblr. Впечатления хорошие, но не для программерского блога: подсветка синтаксиса из коробки отсутствует, чтобы ее включить - надо пошаманить в исходниках темы. Комментарии - через disqus (что неплохо, но отсутствие его поддержки в разных темах раздражает). Возможности импортировать публикации из wordpress - нету (есть неоффициальный скриптик, которым я все туда и втянул).

Итого могу сказать - tumblr неплох, но именно мне больше в конечном итоге подошел posterous.

И вот 1го числа (на самом деле уже второе, но я еще не сплю) я решил и заимпортил все со старого сайта. С подсветкой синтаксиса возникли определенные проблемы, но они благополучно решились перечитыванием нескольких статей:

Поддержка markdown - это просто восхитительно: вся верстка постов станет много легче и проще.

В общем, переезд состоялся. Старый блог будет доступен некоторое время по адресу http://dmitry-old.shaposhnik.name.

Так как у меня появилась привычка читать материалы в первую очередь на анлийском, то неплохо бы и свои посты публиковать не на русском. Останется ли русский перевод - не знаю. Зачем на английском? Во-первых, охват аудитории будет знчительно больше. Во-вторых - нет проблем с терминологией. В-третьих - какая-никакая а все же практика.

И в конце хочу поздравить всех с наступившим 2011 годом, до которого мы таки дожили! Ура-ура-ура!

Samba, AFP and OS X

Настраивал файлохранилище. В качестве эксперимента – добавил поддержку AFP. Поставить самбу – нетяжело, и особо вдаваться в детали я не стану. Все коробочное, кроме общей шары:

[storage]
  comment = General storage area
  path = /home/storage
  guest ok = yes
  browseable = yes
  create mask = 0666
  directory mask = 0777
  read only = no

Попробуйте на такую шару скопировать файл из-под винды. Правильно, права будут 0666. А теперь из-под линукса/мака. Права совсем не 0666, а скорее 0644 или 600. Кроме того, файл создастся от имени залогиненого пользователя. То есть пользователи не смогут удалить файлы оставленные им другими пользователями. Я долго копался, и благодаря Роману нашел решение:

[global]
  ...
  unix extensions = no
  ...

Это решает проблему с правами. Но чуть позже я вернул unix extensions и добавил в шару опцию

force user = nobody

Но на OS X 10.6.3 внезапно явилась проблема: при загрузке папки она ставит на нее расширенные аттрибуты. Ругается, и не дает загрузить файлы. Вручную же файлы именно вовнутри папки загружаются. Все это сопровождается сообщением “operation can’t be completed because you don’t have permission to access…” (раз, два). Решением стало добавление

[global]
  ...
  acl check permissions = no
  ...

С AFP в Ubuntu 10.04 все хорошо – пакет пересобирать не надо, он собран уже с шифрованой передачей пароля. Однако конфиг подровнять пришлось. В /etc/netatalk/afpd.conf в конце пишем:

- -transall -uamlist uams_dhx.so,uams_dhx2.so,uams_guest.so -nosavepassword

Кроме варианта по-умолчанию сюда добавлен uams_guest.so для доступа к анонимной свалке. Рядом в файле /etc/netatalk/AppleVolumes.default пишем:

~/              "Home Directory"                        options:usedots
/home/storage/  "Common storage" dperm:0777 fperm:0666  options:usedots

Как принудительно заставить писать от имени nobody в storage я не нашел, потому выставил соответствующие права. Опция usedots не дает преобразовывать точку в “:2e”.

Финальным шагом был анонс сервисов через бонжур при помощи avahi-daemon. В /etc/avahi/services/afpd.service:

<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">

<service-group>
    <name replace-wildcards="yes">%h</name>
    <service>
        <type>_afpovertcp._tcp</type>
        <port>548</port>
    </service>
    <service>
        <type>_device-info._tcp</type>
        <port>0</port>
        <txt-record>model=Xserve</txt-record>
    </service>
</service-group>

В /etc/avahi/services/samba.service:

<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">

<service-group>
    <name replace-wildcards="yes">%h</name>
    <service>
        <type>_smb._tcp</type>
        <port>139</port>
    </service>
</service-group>

Enjoy!

Plesk and virtual FTP users

В Plesk для каждого домена заводится FTP путем создания обычного системного юзеря. Дополнительный фтп можно сделать не иначе как через под-домен. Но иногда возникает необходимость дать ftp-доступ в под-папку домена.

Так как плеск использует proftpd, то конфигурация чертовски проста.

Создаем /etc/proftpd.authuserfile.

Правим /etc/proftpd.include и добавляем:

AuthUserFile /etc/proftpd.authuserfile

Правим /etc/proftpd.authuserfile и добавляем записсь вида:

username:crypted_pass:uid:gid:gekos:home:shell

Пример:

someacc:xhppo.NGU0Fjw:10065:2523::/var/www/vhosts/example.org/private/someacchome:/bin/false

crypted_pass можно получить вызвав

openssl passwd -crypt passwordhere

Или просто вызвав системный crypt.

Подсмотреть UID/GID можно в /etc/passwd. Нужен он для раздачи прав, чтобы владелец домена также мог править/удалять файлы, загруженные виртуальным пользователем.

UPD:

На cybercity упоминается утилита ftpasswd, при помощи которой легко генерируются записи:

ftpasswd --passwd --name {username} --file /etc/ftpd.passwd --uid {UID} --gid {GID} --home {HOME} --shell /bin/false