11/08/2018, 21:16

Tôi đã tuột quần với Bundler như thế nào?

Vài ngày trước mình gặp phải một cái lỗi rất quái khi làm việc với Bundler và Puma. 1. Quay lại vài ngày trước ... Như ta đã biết Bundler cung cấp hai hàm Bundle.require và Bundle.setup để group và require các thư viện trong project của chúng ta. Với Bundle.setup chúng ta có thể ...

Vài ngày trước mình gặp phải một cái lỗi rất quái khi làm việc với Bundler và Puma.

tuot-quan

1. Quay lại vài ngày trước ...

Như ta đã biết Bundler cung cấp hai hàm Bundle.requireBundle.setup để group và require các thư viện trong project của chúng ta. Với Bundle.setup chúng ta có thể khai báo những gem group ta muốn thêm vào $LOAD_PATH một cách tường minh.

Ví dụ ta có một Gemfile và một đoạn code như sau.

gem "rack", groups: [:development, :production]
gem "sinatra", groups: [:development, :production]
gem "puma", group: :production
# config.ru

require 'bundler'
Bundler.setup(:development)

require 'sinatra/base'
class MyApp < ::Sinatra::Base; end

run MyApp

Để mình giải thích một chút đoạn code trên.

require 'bundler'
Bundler.setup(:development)

Như trong documentation đã viết, mình dùng Bundle.setup để chỉ có những gem trong development group mới require được.

require 'sinatra/base'
class MyApp < ::Sinatra::Base; end

run MyApp

:point_up: Một app Sinatra đơn giản.

Sau đó ta sẽ tiến hành bật server lên với bundle exec.

bundle install
bundle exec rackup

# Puma starting in single mode...
# * Version 3.8.2 (ruby 2.2.2-p95), codename: Sassy Salamander
# * Min threads: 0, max threads: 16
# * Environment: development
# * Listening on tcp://localhost:9292
# Use Ctrl-C to stop

KABOOM, server được bật bằng Puma :collision:! Vì sao thế - Phạm Khánh Hưng? Nếu bạn đã đọc lướt thì quay lại Gemfile ở trên, Puma nằm trong group production cơ mà?

2. Tiến hành tìm lỗi

Chúng ta hãy tiến hành tìm lỗi bằng phương pháp 5 Whys.

Lỗi của Rack phải không?

Trước hết ta sẽ xem qua cách Rack chọn server để boot app.

Mặc định Rack sẽ dùng WEBrick nếu nó không tìm thấy server nào cả. Cơ mà nếu tìm thấy một server mạnh hơn như Puma hay Thin, Rack sẽ ưu tiên dùng server đó.

Có vẻ như Rack làm đúng công việc của nó. Tuy vậy, như đã nói ở trên, chẳng phải Puma không require được sao?

Lỗi của Bundler phải không?

Bằng tất cả sự tò mò, mình tiến vào đọc code của bundle exec. Ở dòng code này, Bundle cố gắng setup tất cả những gì ta có ở Gemfile và đưa nó vào $LOAD_PATH.

def kernel_load(file, *args)
  args.pop if args.last.is_a?(Hash)
  ARGV.replace(args)
  $0 = file
  Process.setproctitle(process_title(file, args)) if Process.respond_to?(:setproctitle)
  ui = Bundler.ui
  Bundler.ui = nil
  require "bundler/setup" ## Dòng này ********************
  signals = Signal.list.keys - RESERVED_SIGNALS
  signals.each {|s| trap(s, "DEFAULT") }
  Kernel.load(file)
end

Khi bundle exec chạy, hàm kernel_load này luôn chạy trước Bundle.setup của chúng ta và một khi Bundle.setup được gọi, nó sẽ cache lại (memoize) mọi thứ và những lần gọi tiếp theo sẽ bị hủy.

Túm cái quần lại là khi chạy bundle exec, nó không chạy hàm Bundle.setup của chúng ta.

Như vậy mọi thứ đã thông, đó là lý do Rack có thể tìm thấy Puma và sau đó bỏ qua WEBrick khi bật app.

Mình có tạo một issue trên repo của Bundler và đồng thời một pull request để làm cho ra ngô ra khoai vụ này. Thật bất ngờ là anh bạn @segiddins, chủ repo, lại bảo đấy là do Bundler cố tình làm thế. Thế thì huề cả làng.

3. Bài viết này sẽ giúp tôi tăng lương thế nào?

Mình nghĩ là cách làm này của Bundler phần nào đó sẽ tạo ra ... những bất ngờ không lường trước được. Nếu chúng ta lỡ vô tình load phải những gem chỉ dành cho môi trường development và test thì sao? Và chuyện gì sẽ xảy ra nếu cái gem mà ta load nhầm đó là ... DatabaseCleaner.

Và trên hết không có tùy chọn nào trong bundle exec để chúng ta khai báo group cả.

Vậy ta làm gì?

bundle config without

Đừng bao giờ chạy bundle install một mình, luôn chạy nó với tùy chọn --with hay --without.

bundle install --without production
bundle install --with development

Với tùy chọn này Bundler sẽ chỉ tải những gem trong group được khai báo ở --with và bỏ qua những group được khai báo trong --without.
Khi bạn không có tải thư viện thì ... không có cơ hội nào để nó load cả. Đồng thời Bundler đủ thông minh để nhớ config này cho những lần gọi sau, bạn chỉ cần config một lần.

Tự viết lại bundle exec của riêng bạn

Nếu bạn muốn hardcore một chút, ta dùng rau nhà tự trồng.

# bin/bundle-exec

require "rubygems"
require "bundler"

Bundle.setup(:default, ENV.fetch("RACK_ENV", "development").to_sym)

Chạy bằng cách

./bin/bundle-exec rackup

Dùng Bundler.reset!

Sau khi đọc source code của Bundler, mình nhận ra rằng ta có thể dùng Bundle.reset! để ... xóa hết mọi thứ, sau đó dùng Bundle.setup để làm lại cuộc đời.

Điều này không giải quyết được vấn đề Puma được load, nhưng ta có thể đảm bảo từ đoạn code của chúng ta trở đi, chỉ có những gem trong development được load.

# config.ru

require "rubygems"
require "bundler"

Bundle.reset!
Bundle.setup(:default, :development)

Thú thực là mình chưa test đoạn code này, nếu bạn muốn thử cảm giác tuột quần thì ... dùng thử.

Bài viết đăng lại từ blog Quần Cam.

Bạn cũng có thể xem bản tiếng Anh tại đây.

0