Madogiwa Blog

主に技術系の学習メモに使っていきます。

cronの読み方をいつも忘れるので、いい感じにParseするgemを作ってみた💎

みなさん、こんにちは。まどぎわです(・∀・)
いつもcronの読み方を忘れてしまい検索して調べて、書くのが面倒くさかったので、いい感じにParseして表示してくれるgemを作って公開しました🙇‍♂️

cronとは?

cron とは、ジョブ(スクリプト)を自動実行するためのデーモンプロセスです。そして、Linux システムの管理を行なう場合、ログのローテートや、バックアップなど、定期的に自動実行したいジョブが数多くあります。
cron の設定ガイドから引用

下記のような形で実行時間を定義出来るやつです。例えば下記の場合、毎日7:00と23:00に実行されるような設定になります。

# 分 時 日 月 曜日
00 7,23 * * * 

でも数字と*で書かれていて、毎回どこがどこを表すのか忘れてしまう。。。

CronConfigParser

そういうわけで、いい感じにParseしてくれるgemを作ってみました💎✨

rubygems.org

👇リポジトリはこちら

github.com

使い方

基本的な使い方はこんな感じで、cronの設定をいい感じに確認することが出来ます🙌

CronConfigParser::Parser.call('00 5 * * * Asia/Tokyo')
=> #<CronConfigParser::CronConfig:0x00007fa4f492e820
 @days=["*"],
 @hours=["5"],
 @minutes=["00"],
 @months=["*"],
 @timezone="Asia/Tokyo",
 @wdays=["*"]>

また、返却されたobjectにminutes_configured?を実行すると、それが定義されているかどうかをチェックできます。(コードでcronを解析したいのような要件がない限りは、あんまり使い所はないかも😅)

# return false if configured nil or '*'
config = CronConfigParser::Parser.call('00 5 * * * Asia/Tokyo')
config.minutes_configured?
=> true
config.days_configured?
=> false

あとは、簡単なcronの設定の検証(必須、英字等が含まれていないかの構文チェック)を行うことが出来ます👮

# not configured require property.
CronConfigParser::Parser.call('00 5,13 * * ')
=> CronConfigParser::ConfigRequiredError

# configured invalid property.
CronConfigParser::Parser.call('00 5,a * * * Asia/Tokyo')
=> CronConfigParser::ConfigSyntaxError

validation: falseをつけることで検証を無効化することも出来ます👩‍🔧

CronConfigParser::Parser.call('00 5,a * * * Asia/Tokyo', validation: false)
=> #<CronConfigParser::CronConfig:0x00007fcedf09cdf0
 @days=["*"],
 @hours=["5", "a"],
 @minutes=["00"],
 @months=["*"],
 @timezone="Asia/Tokyo",
 @wdays=["*"]>

これから

今後は、次実行時間を教えてくれるCronConfig#next_execute_atとかを作って行こうかなと思っていますm( )m

これが出来ると、5時と10時ちょうどだけに実行したいときに* 5,10 * * *と定義してしまい、5時と10時に毎分実行されるような定義をしてしまう前に気づけるので、便利かなぁと。

おわりに

私みたいにcronの設定を毎回忘れちゃう人がいたら、よかったら使ってみてください🙇‍♂️

ruby勉強botの呟くクラスを追加しました🎉

みなさん、こんにちは。まどぎわです(・∀・)

ruby勉強botの呟くクラスをいろいろ追加しました📣

👇クラスを追加したcommitが下記です。

github.com

今までは、StringやArrayと行ったよく使うクラスに限って呟いていたのですが、 いろいろと物足りなくなってきたので、CSV,Kernel,Benchmark, Proc等のクラスのメソッドも呟くようにしました🙌

Benchmarkとか意外と使う機会もあって、便利ですよね👀✨

rubyリファレンスマニュアルの標準添付ライブラリとか見てみると結構便利そうな機能が沢山あっていいですよ。 docs.ruby-lang.org

良さそうなのがあったらまた呟くClassに追加してみようと思います👋

(お世話になっているので、なんかしらるりまにコントリビュートしたい👀) github.com

Swift4.2で画像アップロード周りの仕様が変わっててハマったのでメモ🎑

こんばんは、まどぎわです(・∀・)
最近、下記の本でiosアプリの勉強を軽くしてたのですが、Swiftの画像アップロード周りの実装仕様が書籍と自分の環境では変わっていて本の通りに書いても上手いこといかなかったので、やりかたをメモしておきます✍

作って学ぶ iPhoneアプリの教科書 【Swift4&Xcode 9対応】 ~人工知能アプリを作ってみよう! ~(特典PDF付き)

作って学ぶ iPhoneアプリの教科書 【Swift4&Xcode 9対応】 ~人工知能アプリを作ってみよう! ~(特典PDF付き)

前提

私の環境は、下記の通りです。

name version
xcode 10.1
swift 4.2.1

書籍のサンプル実装(Swift4想定)

最初に書いた書籍に記載されていたサンプルの実装を記載しました。概要としては、ボタン押下時に画像を選択して画面上のUIImageに反映するという機能です🙇‍♂️
info[UIImagePickerControllerOriginalImage]の部分でCannot subscript a value of type '[String : Any]' with an index of type 'UIImagePickerController.InfoKey'が発生して、上手く動かなかった。。。😢

import UIKit

class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
  @IBOutlet weak var myImageView: UIImageView!

  var imagePicker: UIImagePickerController!
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    imagePicker = UIImagePickerController()
    imagePicker.delegate = self
  }
  
  @IBAction func tapButton(_ sender: Any) {
    imagePicker.sourceType = .photoLibrary
    present(imagePicker, animated: true, completion: nil)
  }
  
  func imagePickerController(
    _ picker: UIImagePickerController,
    didFinishPickingMediaWithInfo info: [String : Any]
  ) {
    imagePicker.dismiss(animated: true, completion: nil)
    // ERROR: Cannot subscript a value of type '[String : Any]' with an index of type 'UIImagePickerController.InfoKey'
    guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else {
      return
    }
    myImageView.image = image
  }
}

Swift4.2の実装

下記のように実装を変更したら上手く動くようになりました🙌
変更点は下記です。

  • imagePickerControllerの引数をinfo: [UIImagePickerController.InfoKey : Any]に変更
  • info[.originalImage]で選択した画像を取得するように変更
import UIKit

class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
  @IBOutlet weak var myImageView: UIImageView!

  var imagePicker: UIImagePickerController!
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    imagePicker = UIImagePickerController()
    imagePicker.delegate = self
  }
  
  @IBAction func tapButton(_ sender: Any) {
    imagePicker.sourceType = .photoLibrary
    present(imagePicker, animated: true, completion: nil)
  }
  
  func imagePickerController(
    _ picker: UIImagePickerController,
    didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]
  ) {
    imagePicker.dismiss(animated: true, completion: nil)
    guard let image = info[.originalImage] as? UIImage else {
      return
    }
    myImageView.image = image
  }
}

どうやらSwift 4.2からUIImagePickerControllerの仕様が変わったみたいですね💦

参考資料

qrunch.net

developer.apple.com

sidekiq pro / enterpriseのwikiを読んだので気になった機能の概要をMEMO✍

今回はタイトル通り、sidekiq pro / enterpriseのwikiを読んだので、忘れないように気になった機能の概要をメモしておきます✍

⚠私が拙い英語力 + 技術力で理解した内容のMEMOです。内容が間違っている可能性があるので、注意してください。⚠

前提事項

整理した内容は、2019/04/30現在のsidekiqのwikiに記載されている内容です。

Home · mperham/sidekiq Wiki · GitHub

sidekiq pro

Batches

https://github.com/mperham/sidekiq/wiki/Batches

Batchとして複数のJobの実行をまとめられる機能。
※概要は下記のコードを参照

# Batchの作成
batch = Sidekiq::Batch.new
# Batchの説明
batch.description = "Batch description (this is optional)"
# Callbacks
# `:success`: すべてのJobが成功して完了
# `:complete`: いくつかのJobが失敗を含んで完了
batch.on(:success, SomeClass, 'uid' => current_user.id)
batch.on(:complete, 'AnotherClass#method', 'uid' => current_user.id)
# BatchにJobを登録
# MEMO: Batchは1つ以上のJobを持たなければならない
# https://github.com/mperham/sidekiq/wiki/Batches#notes
batch.jobs { rows.each { |row| RowWorker.perform_async(row) } }
puts "Just started Batch #{batch.bid}"

class SomeClass
  def on_complete(status, options)
    puts "Uh oh, batch has failures" if status.failures != 0
  end
  def on_success(status, options)
    puts "#{options['uid']}'s batch succeeded.  Kudos!"
  end
end

Sidekiq::Batch.newでBatchのインスタンスを作成し、Sidekiq::Batch#jobsのブロック内でWorker.perform_asyncを呼び出すことで、BatchにJobを登録することが出来る。

またSidekiq::Batch#onsuccess(成功)complete(完了※失敗を含む)した場合のCallbackを登録することが出来る。

下記の例のような複雑なWorkflowを組むことも出来る👀

Reliability

https://github.com/mperham/sidekiq/wiki/Reliability

sidekiqの信頼性を向上される機能。大きく分けて2つの機能が提供される。

  • super_fetch: Jobが完了するまでRadisのqueueを削除しない機能、もしもsidekiqのprocessが落ちて再起動した or 前回の走査から1時間経過した場合は、Redisのqueueをフルスキャンし、queueを再実行する。
  • reliable_scheduler: より信頼性の高いschedule実行機能、Redisからjobをpopして実行してくれる。(sidekiq proではLuaを使って、より信頼性の高いスケジューラ)を提供してくれるらしい。※このへんはよく分かってない・・・。

有効にするためにはinitializers/sidekiq.rb内で下記を実行する。

Sidekiq::Client.reliable_push! unless Rails.env.test?

Sidekiq.configure_server do |config|
  config.super_fetch!
  config.reliable_scheduler!
end

Client-side Reliability

https://github.com/mperham/sidekiq/wiki/Pro-Reliability-Client

sidekiqのClient側で信頼性を向上させる機能。具体的には、最初にメモリ上のqueueにpushし、RedisへのNetwork接続が確立されてから、Redisへqueueのpushを行う機能。

有効にするためにはinitializers/sidekiq.rb内で下記を実行する。

Sidekiq::Client.reliable_push! unless Rails.env.test?

Expiring Jobs

https://github.com/mperham/sidekiq/wiki/Pro-Expiring-Jobs

一定期間後に無効化されるJobを作成する機能。

有効にするためにはinitializers/sidekiq.rb内で下記を実行する。

require 'sidekiq/pro/expiry'

Worker内で、sidekiq_options expires_in: 1.hourとするとJobの有効期間を1時間にすることが出来る。(1時間以内に実行されなければ、もう実行しない。)

class SomeWorker
  include Sidekiq::Worker
  sidekiq_options expires_in: 1.hour
  ...
end
# 実行時に指定する場合
SomeWorker.set(expires_in: 1.day).perform_async(...)

※Batchに含まれているJobがexpiredされた場合は、成功とみなされるため注意する。

sidekiq enterprise

Rolling Restarts

https://github.com/mperham/sidekiq/wiki/Ent-Rolling-Restarts

sidekiqのRolling Restartを行う機能。sidekiqのrolling restartには、einhornというprocessマネージャーを使用する。

  • einhornsh --execute upgradeがrolling restartのトリガーとなるので、デプロイ後等、プログラムが置き換わったあとに実行するようにする。
  • 新しいprocessが立ち上がってから10秒後に、まだ古いprocessが起動してたらUSR2を送って古いprocessを正常に終了させてくれる。

Rate Limiting

https://github.com/mperham/sidekiq/wiki/Ent-Rate-Limiting

Jobの実行を制限するような機能を使える。制限はSidekiq::Limiterを使って追加する。

Concurrent

Jobの同時実行数を制限する機能。Sidekiq::Limiter.concurrentで制限する。下記例では、同時実行数は50に制限される。

ERP_LIMIT = Sidekiq::Limiter.concurrent('erp', 50, wait_timeout: 5, lock_timeout: 30)

def perform(...)
  ERP_LIMIT.within_limit do
    # call ERP
  end
end

Bucket, Window

指定期間内での実行数を制限する機能。Sidekiq::Limiter.bucketSidekiq::Limiter.windowで提供される。 c

# Bucketc
# 下記例では、1秒あたりに30回までと実行が制限される
def perform(user_id)
  user_throttle = Sidekiq::Limiter.bucket("stripe-#{user_id}", 30, :second, wait_timeout: 5)
  user_throttle.within_limit do
    # call stripe with user's account creds
  end
end

# Window
# 下記例では、5秒間に1回までになる?Windowはよくわからなかった。。。
def perform(user_id)
  user_throttle = Sidekiq::Limiter.window("stripe-#{user_id}", 5, :second, wait_timeout: 5)
  user_throttle.within_limit do
    # call stripe with user's account creds
  end
end

bucketとwindowの違いがわからん。。。

Unlimited

既存の制限を外す機能。Sidekiq::Limiter.unlimitedで提供される。 ※管理ユーザだけ制限を解除するような用途で使用する。

ERP = Sidekiq::Limiter.concurrent("erp", 10)

def perform(...)
  lmtr = current_user.admin? ? Sidekiq::Limiter.unlimited : ERP
  lmtr.within_limit do
    # always executes for admins
  end
end

Periodic Jobs

https://github.com/mperham/sidekiq/wiki/Ent-Periodic-Jobs

Jobの定期実行機能。
initializers/sidekiq.rb内で下記のようにcrontab形式でスケジュールを定義出来る。※WebUIでも一覧を確認出来る。

Sidekiq.configure_server do |config|
  config.periodic do |mgr|
    # see any crontab reference for the first argument
    # e.g. http://www.adminschoice.com/crontab-quick-reference
    # or   https://crontab.guru/ 
    mgr.register('0 * * * *', SomeHourlyWorkerClass)
    mgr.register('* * * * *', SomeWorkerClass, retry: 2, queue: 'foo')
    mgr.register(cron_expression, worker_class, job_options={})
  end
end

Unique Jobs

一意になるようなJob(Redis内に複数のqueueが存在しないようなJob)を定義することが出来る機能。

有効にするためにはinitializers/sidekiq.rb内で下記を実行する。

Sidekiq::Enterprise.unique! unless Rails.env.test?

Workerを下記のように定義すると、10分後または、Jobが正常に処理された際に次回のJobがqueueにpushされる。

class MyWorker
  include Sidekiq::Worker
  sidekiq_options unique_for: 10.minutes

  def perform(...)
  end
end

おわりに

今回はsidekiq proとenterpriseで提供される機能で私が気になったものを整理してみましたm( )m

proのBatchやReliabilityの機能は、プロダクト運用で有用そうなので、ビジネスで利用するときは使うと良さそうな機能ですね👀

enterpriseもRolling RestartsやPeriodic Jobsなど、かゆいところに手が届く機能が多く良さそうに見えました👀

みなさんも気になったら詳しい内容が公式Wikiに記載されてるので、読んでみてくださいー。

Home · mperham/sidekiq Wiki · GitHub

それでは👋

Sidekiqがどうやって動いているのか、コードを読んで概要を掴めた気がしたのでメモしてみる

みなさん、こんにちは。まどぎわです。
rubyで非同期処理やるときのデファクトスタンダード的なgemsidekiqのコードを読んで、概要が割とつかめた気がしてきたので、どういう感じで動いてるか自分の理解の範囲でメモしてみました🙇

github.com

sidekiqの機能としては大きく分けて、

  • Redisへのqueueのpush
  • Redisからqueueのpopとjobの実行

だと思ったので、それについてsidekiqのコードと合わせて概要を整理してみました。
※記載しているコードについては、読みやすいコードを削除しているので全文が読みたい方は、それぞれのリンク先で確認いただけますと🙏

前提

今回調べたsidekiqのversionは、2019/04/28現在のmasterである、6.0.0.pre1です。

# frozen_string_literal: true

module Sidekiq
  VERSION = "6.0.0.pre1"
end

Redisへのqueueのpush

非同期処理の呼び出しは、perform_asyncによって行われる。 引数にselfargsを与えて、client_pushを呼び出している。selfには、hogeJob等のjob設定される。

def perform_async(*args)
  client_push("class" => self, "args" => args)
end

https://github.com/mperham/sidekiq/blob/b76dfb9e056d6c17d7c2fe66dd9ab3f38aa5423f/lib/sidekiq/worker.rb#L92

client_pushではredisへ接続するためのpoolを取得し、引数を文字列に変換してSidekiq::Client.new(pool).push(item)を呼び出している。

def client_push(item) # :nodoc:
  # redisに接続するためのpoolを取得
  pool = Thread.current[:sidekiq_via_pool] || get_sidekiq_options["pool"] || Sidekiq.redis_pool
  # stringify
  item.keys.each do |key|
    item[key.to_s] = item.delete(key)
  end
  
  # redisへのpush処理の呼び出し
  Sidekiq::Client.new(pool).push(item)
end

https://github.com/mperham/sidekiq/blob/b76dfb9e056d6c17d7c2fe66dd9ab3f38aa5423f/lib/sidekiq/worker.rb#L142

pushメソッドでredisへの登録処理を呼び出している👀

module Sidekiq
  class Client
    def push(item)
      # to_sとかしてベーシックなjsonのような形式に変換
      normed = normalize_item(item)
      # middrewareを実行してitemを返却
      payload = process_single(item["class"], normed)
    
      if payload
        # radisのpush処理
        raw_push([payload])
        # jidを返す
        payload["jid"]
      end
    end
    
    private
    
    # radisへのpush処理
    def raw_push(payloads)
      @redis_pool.with do |conn|
        conn.multi do
          atomic_push(conn, payloads)
        end
      end
      true
    end
    
    # jsonに変換してconnectionを使ってradisに登録
    def atomic_push(conn, payloads)
      # scheduledの場合
      if payloads.first["at"]
        conn.zadd("schedule", payloads.map { |hash|
          at = hash.delete("at").to_s
          [at, Sidekiq.dump_json(hash)]
        })
      # 通常の場合
      else
        queue = payloads.first["queue"]
        now = Time.now.to_f
        to_push = payloads.map { |entry|
          entry["enqueued_at"] = now
          Sidekiq.dump_json(entry)
        }
        # ここがredisへの登録処理の本丸
        conn.sadd("queues", queue)
        conn.lpush("queue:#{queue}", to_push)
      end
    end

https://github.com/mperham/sidekiq/blob/d16572bcdd0aa52985f9dc9e79f5179a6c828154/lib/sidekiq/client.rb#L69

queueから取り出して実行

sidekiqの起動

sidekiqの起動はCLI.runで行われる。runの中で、Sidekiq::Launcherインスタンスが作成されてlauncher.runが実行される。

module Sidekiq
  class CLI
   def run
      boot_system
      if environment == "development" && $stdout.tty? && Sidekiq.log_formatter.is_a?(Sidekiq::Logger::Formatters::Pretty)
        print_banner
      end
      # 省略: radisのversionのチェックとかいろいろやる
      launch(self_read)
    end
    
    def launch(self_read)
      # 省略
      @launcher = Sidekiq::Launcher.new(options)
      begin
        launcher.run
        while (readable_io = IO.select([self_read]))
          signal = readable_io.first[0].gets.strip
          handle_signal(signal)
        end
        # 省略
      end
    end

https://github.com/mperham/sidekiq/blob/d16572bcdd0aa52985f9dc9e79f5179a6c828154/lib/sidekiq/cli.rb#L36

Launcher#runの中では、スレッドの作成、ポーリングの開始、Manager#startが呼ばれます。 ※Pollerは、N秒に一回scheduleされたjobがあればradisのqueueに入れるようなことをやってる。

module Sidekiq
  class Launcher
    def initialize(options)
      @manager = Sidekiq::Manager.new(options)
      @poller = Sidekiq::Scheduled::Poller.new
      @done = false
      @options = options
    end

    def run
      @thread = safe_thread("heartbeat", &method(:start_heartbeat))
      @poller.start
      @manager.start
    end
  end
end

https://github.com/mperham/sidekiq/blob/d16572bcdd0aa52985f9dc9e79f5179a6c828154/lib/sidekiq/launcher.rb#L34

Manager#newの中で並列実行数concurrencyの数だけ、worker(Processor.new(self))を作成して、Manager#startで、全てstartさせる。

module Sidekiq
  class Manager
    def initialize(options = {})
      # 省略
      @count = options[:concurrency] || 10
      @count.times do
        @workers << Processor.new(self)
      end
      # 省略
    end

    def start
      @workers.each do |x|
        x.start
      end
    end

https://github.com/mperham/sidekiq/blob/d16572bcdd0aa52985f9dc9e79f5179a6c828154/lib/sidekiq/manager.rb#L43

RadisからqueueのpupとJobの実行

Processorがメインの処理、ここでqueueから取り出したjobを実行している。
ざっくりとした流れは、

  1. runが実行され、process_oneが終了されるまで実行され続ける。
  2. process_oneの中でredisからdequeueして、引数workとしてprocessに渡す。
  3. processの中で、workから各種情報を引き出し、dispathを呼び出して、worker(jobのインスタンス)を作成して、execute_jobworker.performを実行し、処理を実行している。

※下記が読みやすいように、いろいろ省略したSidekiq:: Processorのコードです。

module Sidekiq
  class Processor
    def initialize(mgr)
      # 省略
      @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
      @reloader = Sidekiq.options[:reloader]
      @job_logger = (mgr.options[:job_logger] || Sidekiq::JobLogger).new
      @retrier = Sidekiq::JobRetry.new
    end
    
    # runが実行される。
    def start
      @thread ||= safe_thread("processor", &method(:run))
    end
    
    def run
      # ここがdoneになるまで繰り返されてるので、
      # queueから取り出されてjobを実行するという処理が、ずっと続く。
      process_one until @done
      @mgr.processor_stopped(self)
      # 省略
    end
    
    # ここで、radisからqueを取り出してjobに入れてprocessで処理を開始
    def process_one
      @job = fetch
      process(@job) if @job
      @job = nil
    end
    
    # 取得して、終了してたらもう一回queueに入れ直すような処理
    def fetch
      j = get_one
      if j && @done
        j.requeue
        nil
      else
        j
      end
    end
    
    # BasicFetch#retrieve_workでradisからパースしたオブジェクトを取得
    # https://github.com/mperham/sidekiq/blob/d16572bcdd0aa52985f9dc9e79f5179a6c828154/lib/sidekiq/fetch.rb#L36
    def get_one
      work = @strategy.retrieve_work
      # 省略
      work
      # 省略
    end

    # jobの実行処理
    def process(work)
      # このへんはradisから取り出したオブジェクトをいい感じにする処理
      jobstr = work.job
      queue = work.queue_name
     job_hash = Sidekiq.load_json(jobstr)

      ack = true
      begin
        # この中でmiddrewareを実行して、workerと引数を渡してjobを実行してる。
        dispatch(job_hash, queue) do |worker|
          Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
            execute_job(worker, cloned(job_hash["args"]))
          end
        end
      rescue Sidekiq::Shutdown
        ack = false
      rescue Sidekiq::JobRetry::Handled => h
        raise e
      rescue Exception => ex
        raise e
      ensure
        work.acknowledge if ack
      end
    end
    
    # worker(jobのインスタンス)のworker.performを呼び出している。
    def execute_job(worker, cloned_args)
      worker.perform(*cloned_args)
    end
    
    # ここでlog出したり、workerをjobのclassのインスタンスに変更してる。
    def dispatch(job_hash, queue)
      pristine = cloned(job_hash)

      @job_logger.with_job_hash_context(job_hash) do
        @retrier.global(pristine, queue) do
          @job_logger.call(job_hash, queue) do
            stats(pristine, queue) do
              @reloader.call do
                klass  = constantize(job_hash["class"])
                worker = klass.new
                worker.jid = job_hash["jid"]
                @retrier.local(worker, pristine, queue) do
                  yield worker
                end
              end
            end
          end
        end
      end
    end

https://github.com/mperham/sidekiq/blob/d16572bcdd0aa52985f9dc9e79f5179a6c828154/lib/sidekiq/processor.rb#L60

おわりに

今回はsidekiqがどのように動いているか、コードを読みながら概要をまとめてみました。

Radisからpush/popしているためjson形式のやり取りになり、symbolやmodelのインスタンス等を引数で渡すと、いろいろ問題が起きてしまうんですね。(ActiveJobはこの辺をよしなにやってくれてそうです。)

またコードを読む中で変数名が省略されている箇所があったので、PR送ったらmergeしてもらえたので、sidekiqのコントリビューターになりました🙌

github.com

(また地味にwikiも修正してたりします)
https://github.com/mperham/sidekiq/wiki/Best-Practices/_history

RubyKaigi 2019 Cookpad Daily Ruby Puzzlesを解いてみたので自分の解答をメモ

みなさん、こんにちは。まどぎわです(・∀・)
RubyKaigiでCookpadさんのブースで出題されていたRuby Puzzleを、Ruby Kaigi中に楽しくやっていたので、 解答も下記で公開されたので、自分の解答をメモしておきます。

techlife.cookpad.com

Day 1

Problem 1-1

# Hint: Use Ruby 2.6.
puts "#{"Goodbye" .. "Hello"} world"

My Answer(2文字)

puts "#{"Goodbye" .. && "Hello"} world"

HintにあるようにRuby2.6から追加された終端なしRangeを使って、 "Hello"だけを返せればいいのかなと思ったので&&を使ってみました。

www.ruby-lang.org

Problem 1-2

puts&.then {
  # Hint: &. is a safe
  # navigation operator.
  "Hello world"
}

My Answer(2文字)

puts 1&.then {
  # Hint: &. is a safe
  # navigation operator.
  "Hello world"
}

これはputsの返り値が、nilになってしまってthen以降が実行されなくなっていたので、 puts 1&.thenとして、putsの返り値を使用しないようにしてました。

Problem 1-3

include Math
# Hint: the most beautiful equation
Out, *,
     Count = $>,
             $<, E ** (2 * PI)
Out.puts("Hello world" *
         Count.abs.round)

My Answer (2文字)

include Math
# Hint: the most beautiful equation
Out, *,
     Count = $>,
             $<, E ** (2 * PI*0)
Out.puts("Hello world" *
         Count.abs.round)

Countが1に近い値になれば、"Hello world"が1回実行されるので、 E ** (2 * PI*0)として、1に近い値を作りました。

Day 2

Problem 2-1

def say
  -> {
    "Hello world"
  }
  # Hint: You should call the Proc.
  yield
end

puts say { "Goodbye world" }

My Answer(1文字)

def say
  -> {
    "Hello world"
  }.
  # Hint: You should call the Proc.
  yield
end

puts say { "Goodbye world" }

これは.yield.callと同じなので、それを使うようにしました。

docs.ruby-lang.org

Problem 2-2

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  yield "Hello world"
end

puts e.next

My Answer(2文字)

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  g.yield "Hello world"
end

puts e.next

gEnumerator::Yielderが返却される。

docs.ruby-lang.org

そして検索してたら下記のようなページを発見し、g.yieldと書けそうなことがわかったのでそれを使いました。

qiita.com

Problem 2-3

$s = 0
def say(n = 0)
  $s = $s * 4 + n
end

i, j, k = 1, 2, 3

say i
say j
say k

# Hint: Binary representation.
$s != 35 or puts("Hello world")

My Answer(2文字)

$s = 0
def say(n = 0)
  $s = $s * 4 + n
end

i, j, k = 1, 2, 3

say i
say j
say k

# Hint: Binary representation.
$s+8 != 35 or puts("Hello world")

$sが27を返すので、35となるように+8しました。

Day 3

Problem 3-1

def say s="Hello", t:'world'
  "#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.

puts say :p

My Answer(1文字)

def say s="Hello", t:'world'
  "#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.

puts say t:p

pの返り値はnilなので、s="Hello", t:nilとなり、"Hello world"が出力されます。

Problem 3-2

def say s, t="Goodbye "
  # Hint: You can ignore a warning.
  s = "#{ s } #{ t }"
  t + "world"
end

puts say :Hello

My Answer(3文字)

def say s, t="Goodbye "
  # Hint: You can ignore a warning.
  st = "#{ s } "#{ t }"
  st + "world"
end

puts say :Hello

ここは最初say :Hello,pのような形でいけるかと思ったのですが、 :Helloはsymbolのため+メソッドが使えないのです。。。

なので、st = "#{ s } "とすることで、stに"Hello"だけが入るようにして、 st + "world"として"Hello world"となるようにしました。

Problem 3-3

def say
  "Hello world" if
    false && false
  # Hint: No hint!
end

puts say

My Answer(2文字)

def say
  "Hello world" if
    :false && :false
  # Hint: No hint!
end

puts say

これは単純で、falsetrueになるようにすればいいので、 !falseと思ったのですが、それだとあまりにもと思ったので:falseにしました。

Extra Stage

Ruby Kaigi後に出題されたExtra Stageについてもやってみたので、解答をメモしておきます✍

"Cookpad Daily Ruby Puzzles" in RubyKaigi 2019 - Extra Stage · GitHub

⚠️⚠️⚠️ここからは解答がまだ発表されていないので、ネタバレ注意です⚠️⚠️⚠️























Problem Extra-1

Hello = "Hello"

# Hint: Stop the recursion.
def Hello
  Hello() +
    " world"
end

puts Hello()

My Answer(2文字)

Hello = "Hello"

# Hint: Stop the recursion.
def Hello
  Hello+#() +
    " world"
end

puts Hello()

これはHintの通り、メソッド呼び出しを止めればいいので、Hello+#() +として、変数側のHelloを参照するようにしました。

Problem Extra-2

s = ""
# Hint: https://techlife.cookpad.com/entry/2018/12/25/110240
s == s.upcase or
  s == s.downcase or puts "Hello world"

My Answer(1文字)

s = "Dz"
# Hint: https://techlife.cookpad.com/entry/2018/12/25/110240
s == s.upcase or
  s == s.downcase or puts "Hello world"

これはHintのURLを参考したら該当文字Dzが載っていたので、それを使うようにしました。

Problem Extra-3

def say
  s = 'Small'
  t = 'world'
  puts "#{s} #{t}"
end

TracePoint.new(:line){|tp|
  tp.binding.local_variable_set(:s, 'Hello')
  tp.binding.local_variable_set(:t, 'Ruby')
  tp.disable
}.enable(target: method(:say))

say

My Answer(2文字)

def say
  s = 'Small'
  t = 'world'
  puts "#{s} #{t}"
end

TracePoint.new(:line){|tp|
  tp.binding.local_variable_set(:s, 'Hello')
  #tp.binding.local_variable_set(:t, 'Ruby')
  #tp.disable
}.enable(target: method(:say))

say

tp.disableコメントアウトしたところ"Hello Ruby"が返されるようになったので、 tp.binding.local_variable_set(:s, 'Hello')だけが実行されるようにしました。

おわりに

Cookpadさんの問題が、面白すぎてセッションに集中出来なかった部分もありますが・・・笑
問題に取り組むなかで、マニュアルを読んだり、いろいろ調べたりするなかで、 rubyのイディオムやリテラル周りの知識が見について非常に勉強になりました🙌

来年のRuby Kaigiでは、もっといい感じの答えが出せるように頑張ろう💪

rails勉強botでActionViewのメソッドが呟かれるようになりました🎉

みなさん、こんばんは。 昨日までrubykaigi2019で福岡に行ってたまどぎわです💎🍜

表題の通り、rails勉強botActionViewのメソッドがつぶやかれるようになりました🙌

ActionViewのメソッドは、割と忘れがちな気もするので気になるメソッドがあったら見てみてください👀✨

👇ActionViewrailsガイドでいうとこの辺です

railsguides.jp

またrails勉強botの中身をリファクタリング(?)して、簡単につぶやくClassを追加できるようにしました🎉

こんな感じで追加出来ます!簡単🙌

class MessageBuilder::ActionView::Base < MessageBuilder::Base
  RAILS_CLASS = ActionView::Base
  RAILS_TOP_CLASS_NAME = 'actionview'.freeze
  RAILS_CLASS_FILE_REGXP = /action_view.*/.freeze
  RAILS_CLASS_REGXP = /action_view/.freeze
end

MessageBuilder::Baseにclassからメソッドをランダムに取得したり、 メソッドのsource_locationを取得して、Githubへのリンクを生成したりする処理を実装して、 それらを継承するようにしました。

(一旦楽になったけど、継承という選択が正しかったのかどうかは微妙・・・。一応、MessageBuilder::ActionView::Baseは、MessageBuilderであるはis-aの関係になっている気もするので、まぁいいかという気持ち🙈)

👇MessageBuilder::Baseの実装が気になる方はこちら

github.com

ruby勉強botも主要classしか呟かないようにしてたけど、rubykaigiに参加してTracepointとかコアなclassの内容も少し知りたくなってきたので、ちょっとつぶやくClassを追加していこうかなと思いました💪

それでは😴