A year (+½) with Hanami in production: the Good, the Bad and some Recipes

A year (+½) with Hanami in production: the Good, the Bad and some Recipes

RubyC 2019 (Kyiv) and GrillRB 2019 (Wroclaw) slides

Ac3e162318c73347bef4d20b1bb7f7f3?s=128

Yevhen "Eugene" Kuzminov

September 15, 2019
Tweet

Transcript

  1. 1.

    A year (+½) with Hanami in production: the Good, the

    Bad and some Recipes Yevhen [Eugene] Kuzminov Ruby Team Lead in MobiDev
  2. 3.

    whoami • 10 years in professional web dev (put HTML

    together since 2001) • 6 years with PHP + 4 years with Ruby • among them 3 years with Trailblazer • You may know me by ◦ “Ruby Web Dev: The Other Way” book http://rwdtow.stdout.in ◦ http://stdout.in/en/post/ruby-ecosystem-bittersweet-or-we-like-to-hate-php • I am in love with Elixir • “In complicated relationships” with RoR and Blockchain • but last 1+½ years I am a Solution Architect of one cool web app...
  3. 5.

    Hanami is... • a full featured web framework • routing

    • controllers / actions • dev code reload • DB access • migrations • scaffolding • console / debug • assets handling
  4. 6.

    What’s wrong with Rails, man? • “sharp knives” © DHH

    • fat controllers • fat models • fat views • fat strong parameters • the whole app is your business logic • Rails-deformation: try to fit any Business Entity in ActiveRecord model if can’t fit - it’s a wrong… Business Entity • going against the Rails-way hurts so much
  5. 7.

    Hanami proposes: • container architecture (monolith that is easy to

    split) • structure for common and specific code • separate class per action, flexible setting per each action • Interactors aka Service Objects • Entity - Repository instead of ActiveRecord • middleware oriented, even actions are middlewares • dry-gems • being free from ActiveSupport “pollution”
  6. 9.
  7. 11.
  8. 14.

    The price of non-mainstream solution • about 3 months to

    become productive, when we started “from scratch” • need to educate each new team member (but they enjoy it :) ) • no known ways to integrate most of existing Ruby code/gems • Rails-ish ecosystems is a bit depressing without Rails • lack of docs, examples, learning resources...
  9. 16.
  10. 17.

    The Story (of one big Hanami app) context of architectural

    and technical decisions SPA, REST, API, SEO, admin area, webhooks and realtime websockets listeners ~ 140 API actions ~ 350 Trailblazer operations ~ 50 entities/models + ~ 50 repositories ~ 30 sidekiq workers 6 different persistent data storages (databases)
  11. 18.

    Trailblazer (business logic) • Using only gem 'trailblazer-operation' • Macroses

    are ActiveModel oriented, need to write own for Hanami • Validation via pure dry-validation • Hanami’s Interactor is too simplistic • TRB is not as useful as with Rails • Still, perhaps, most powerful Service Objects solution • But docs sometimes not clear and functionality tend to be overcomplicated
  12. 19.

    REST API • Separate “container app” • Own request handling

    settings, turned off templates + layouts • Trailblazer Representable + Roar • Action calls TRB Operation, then render JSON from Representer • JWT authentication (custom) • Not really straightforward JSON rendering def render_json(data, code) halt(code, Oj.dump(data)) end
  13. 20.

    Authentication • No out-of-the-box solution • For the REST API

    started with naive JWT token auth • Now switching to gem 'jwt_sessions' by @tuwukee • JWTSessions gives secure and solid foundation • For Cookie-based (non API) auth solution is still to come
  14. 21.

    WebPack[er] (SPA frontend integration) All you need: • Manifest file

    parser • View helper for `javascript_pack_tag` • Config switcher for WebPack Dev Server usage What you get: • Happy frontend devs • Completely custom WebPack setup • No coupling with WebPack, because it is not needed!
  15. 22.

    Sidekiq gem 'sidekiq' custom addons (like scheduler and statistics) are

    working too sidekiq -e development -r ./config/boot.rb -C ./config/sidekiq.yml use standard Sidekiq::Worker mixin classes
  16. 24.

    Hanami::Repository < ROM.RB • general impression - “I don’t understand

    how it works” • constantly trying different options and methods “while it works” • supports only one DB connection • nested Entities not working, return Hashes • custom mapping not working, always maps result to Entity “corresponding” to current Repo • so we did an unexpected move...
  17. 25.

    Use Sequel::Model, Luke! • Yes, I know it’s ActiveRecord pattern,

    don’t panic! :) • ActiveRecord is not bad when used solely for Data Manipulation • Write simplistic own Repository class • Use Sequel queries ONLY inside the Repository • Flexible usage of Sequel::Model or Dataset • Easy and clear huge complicated raw SQL queries • Don’t forget to before_fork { DB.disconnect }
  18. 26.

    Many DBs, but one Repo concept We use simultaneously: •

    PostgreSQL • Redis (persistent) • InfuxDB • Cassandra • JanusGraph • Algolia search To be added soon: • Clickhouse • Algolia >>> Elastic Make a Repository class (not inherited from Hanami::Repository) and use any DB client / SDK calls, return structured data
  19. 27.

    Dry Container + AutoInject • register all deps that require

    single instance • register most Repositories • Dry-Systems could work, but way too complicated # ./config/dry-auto-inject.rb class Container extend Dry::Container::Mixin register('influx', memoize: true) do db_params = {...} InfluxDB::Client.new(ENV['INFLUXDB_DB'], db_params) end … Container['influx'] # include ::Inject['influx']
  20. 28.

    Cache • no standard cache implementation • Readthis::Cache ideal ActiveSupport

    compatible match • Basic cache methods + custom Redis commands support class PostCacheRepository < CacheRepository def find(uuid) cache_get(cache_id(uuid), expires_in: 3600) do PostRepository.new.find(uuid) end end ...
  21. 29.

    Running in Production (with Puma) • gem 'puma' # to

    Gemfile group :production • HANAMI_ENV=production • SERVE_STATIC_ASSETS=false • hanami server --no-code-reloading # detects Puma • ./config/puma.rb # almost like for Rails, but with... on_worker_boot do Hanami.boot end
  22. 30.

    CLI (custom commands) Perhaps you can use plain rake tasks,

    but we used gem ‘hanami-cli’ As a minus - different invocation form: $ hanami db create $ ./hanami-cli db seed
  23. 31.

    Testing • we use Minitest (long story of my personal

    love and hate) • but would migrate to RSpec sometime (as it is by default in 1.3+) • all your favorite things would work: ◦ Capybara ◦ Database Cleaner ◦ VCR ◦ Webmock ◦ etc. • only Factory Bot requires “ActiveSupport” and replaced by Fabricator • test mostly TRB Operations • do not use “Hanami’s advertised” Action tests via “.call”
  24. 32.

    ActionPolicy (Authorization) • any Ruby Auth/Policy lib can be used

    • the less ActionController code/helpers, the better • gem 'action_policy' by Evil Martians common usage with Trailblazer Operation: step :check_policy, Output(:failure) => End(:forbidden) def check_policy(ctx, **) ::PostPolicy.new(user: ctx[:user], post: ctx[:model]).apply(:create?) end
  25. 33.

    WebSockets - A(ction)nyCable gem 'anycable' gem 'litecable' • did I

    mention that Evil Martians ? • requires additional configuration and separate process • LiteCable gem integrates well with Hanami • really fluent and fast operation, very low RAM consumption • clearer Channels code and messages broadcasting logic (that ActionCable) • uses ActionCable JS on frontend
  26. 34.

    Services communication (RabbitMQ) gem 'sneakers' • Consider this very similar

    to Sidekiq, but not limited to Ruby clients • sneakers work --require config/sneakers_boot.rb class MakeAKeynote include Sneakers::Worker from_queue 'rubyc.keynotes' def work(msg) # do smth with `msg` ack! end end
  27. 36.

    Hanami as a modular puzzle # GEMFILE #jRuby v9.2.4 ...

    gem 'celluloid' # was this not a perfect idea? well… yes. gem 'reel-rack' gem 'hanami-router' gem 'hanami-controller' gem 'hanami-validations' gem 'dry-system' gem 'sequel'
  28. 38.

    #router.rb Hanami::Controller.configure do handle_exceptions false end WEB_ROUTER = Hanami::Router.new do

    namespace 'api' do namespace 'meetup' do post '/start', to: ApiActions::Meetup::Start end end end
  29. 39.

    #action class module ApiActions::RubyC class Start include Hanami::Action params do

    required(:speechs_id).filled(:str?) end def call(params) render_errors(params) unless params.valid? self.body = “Ruby, Ruby, RubyC!” end end end
  30. 40.

    Hanami 2 • In progress, but no exactly clear timeline

    • https://discourse.hanamirb.org/t/roadmap-for-v2-0-0-alpha1/475 • https://www.youtube.com/watch?v=LqGBhTSOmTI • Less internal state • More functional Containers • More flexible Configuration • Fresh Dry Gems
  31. 41.

    My vision of ideal backend-web framework • any Ruby conf

    can not take place without mentioning Elixir/Phoenix • composed from loosely coupled libs (greeting from PHP!) • full request path transparency from app server, through router to action • middlewares on all layers, router middleware pipelines • flexible data mapper (maybe even not an ORM) • basic DDD-ish structure for files/folders/modules • not being Resources/CRUD oriented • framework should teach me to write a better apps, to do better architectures
  32. 42.
  33. 43.
  34. 46.

    Facing reality • It’s impossible to “beat Rails” in CRUD

    apps • No need even to try • Clear “complex domain” app niche positioning needed for Hanami • Not trying to pretend “you can do the same even better and faster” than Rails
  35. 47.

    Summary • with Hanami it’s possible to do both small

    and big apps • “batteries not included” • a few compatible out-of-the-box Ruby libs, but they are growing! • you would need to write some code :) • it is you are adjusting the framework, not framework adjusts you • good choice for: ◦ non-CRUD long running projects with complex domain (it is our case) ◦ API ◦ (micro) service oriented projects ◦ non WebUI apps