zed.0xff.me

You MUST free memory you got from ALLOC_N & friends

It’s not documented anywhere, but you must call “xfree()” (note ‘x’ there) on memory blocks you got from ALLOC_N & friends.
Ruby will not free that memory automatically during it’s usual GC process.

Guys from ruby-talk and rubyforge also noticed that.

sample code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
VALUE method_parse(VALUE self, VALUE text) {
    VALUE s;
    char *p    = RSTRING(text)->ptr;
    in_buf_len = RSTRING(text)->len;

    // [skipped some code here]

    buf = ALLOC_N(char, bufsize); 

    // protect buf from GC (theoretically)
    rb_iv_set(self,"@obj",Data_Wrap_Struct(rb_cData,NULL,NULL,buf));

    // [skipped some hard work here]

    // make ruby string from our char[] data
    s = rb_str_new(buf,bufptr-buf);

    // cleanup
    rb_iv_set(self,"@obj",Qnil);
    xfree(buf);
    buf  = NULL;
    bufsize = 0;

    return s;
}

Особенности кэширования классов в Rails

Задача

Вот есть например у нас некий самописный класс для взаимодействия со сторонним API:

1
2
3
4
5
6
7
class SomeAPIClient
  def initialize params = {}
    @host  = params[:host]  || 'some.api.host'
    @port  = params[:port]  || 12345
  end
...
end

И кладем мы его в #{RAILS_ROOT}/lib/, чтобы его можно было легко юзать из классов рельсового приложения.

Дальше мы задумываемся о том, что из нашего Rails-приложения, мы будем всегда обращаться к одним и тем же хосту и порту апи-сервера. И надо где-то их вконфигурить. А приложение-то у нас opensource, и хостится на github-e, поэтому хост и порт (и возможно какие-то другие параметры типа api-key) у нас для разных юзеров приложения могут быть разными.

Отсюда вопрос: Как бы это сконфигурить так, чтобы и либу не особо корежить, и чтоб наиболее Rails-way было?..

Мне вот лично пришла в голову следующая мысль:

1
2
3
4
5
6
7
8
9
class SomeAPIClient
  cattr_accessor :default_port, :default_host

  def initialize params = {}
    @host  = params[:host] || default_host || 'some.api.host'
    @port  = params[:port] || default_port || 12345
  end
...
end

Проблема

Всё вроде бы красиво, свои значения для default_host & default_port мы назначаем в #{RAILS_ROOT}/config/initializers/my_api_client.rb, который исключаем из git-репозитария путём помещения в .gitignore.

Но! Но не зря в топике написано про кэширование классов.. в development окружении первый запрос к API выполняется отлично, а вот уже на втором и всех следующих – вступает в действие config.cache_classes = false, рельсы перечитывают декларацию нашего апи, при этом удаляя старый класс из памяти через remove_const. Ну а initializers естественно не перечитывают.

В production всё ОК, потому что там обычно config.cache_classes = true.

Решение

Копаясь во внутренностях Rails обнаружил вот это:
( actionpack-2.3.2/lib/action_controller/dispatcher.rb )

1
2
3
4
5
6
7
8
9
10
11
      # Add a preparation callback. Preparation callbacks are run before every
      # request in development mode, and before the first request in production
      # mode.
      #
      # An optional identifier may be supplied for the callback. If provided,
      # to_prepare may be called again with the same identifier to replace the
      # existing callback. Passing an identifier is a suggested practice if the
      # code adding a preparation block may be reloaded.

      def to_prepare(identifier = nil, &block)
...

Таким образом, чтобы решение стало работоспособным надо обернуть нашу инициализацию в do_prepare:
( #{RAILS_ROOT}/config/initializers/my_api_client.rb )

1
2
3
4
ActionController::Dispatcher.to_prepare(:my_api) do
  SomeAPIClient.default_host       = 'my.api.host'
  SomeAPIClient.default_port       = '23456'
end

И в development mode этот код будет срабатывать перед обработкой каждого запроса, а в production – только перед обработкой самого первого запроса.

Done.

rspec views & params

Пытаюсь тестить спеку вьюшки(view). Причем внутри нее такая логика:

1
<%= verbose_search_results(user) if params[:verbose] %>

И я не нашел более вменяемого способа передать в нее params, чем следующий:

1
2
3
4
it "should do what I said"
  @controller.stub!(:params).and_return({:verbose => true})
  render 'admins/users/list'
end

И тут есть белая сторона медали, а есть черная.
Белая – при этом стандартные параметры 'action' и 'controller' также нормально передаются, т.е. мой and_return действует недеструктивно.
Черная – params, переданный внутри такой спеки, становится невосприимчив к ключам-символам!

т.е. if params[:verbose] работать НЕ будет (хотя на живых рельсах работает прекрасно).
А вот if params['verbose'] будет работать и там и там.

PS: может быть, конечно, есть более вменяемый способ передать params, но пока я его не обнаружил.

UPD: как оказалось rspec инициализирует параметры 'action' и 'controller' уже на готовом хэше params, т.е. чтобы всё работало также как и на живых рельсах, надо делать так:

1
2
3
4
it "should do what I said"
  @controller.stub!(:params).and_return(HashWithIndifferentAccess.new({:verbose => true}))
  render 'admins/users/list'
end

т.е. явно инициализировать params как HashWithIndifferentAccess, а не просто хэш.

IMHO как-то это всё-таки кривовато, и должен быть более прямой и очевидный способ..