Madogiwa Blog

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

ブラウザのWindow、Documentのイベントを検証するツールを作ったよ💻

ブラウザ系のイベントをモバイルとかブラウザとか色々調査することがあったので、 いろいろ試せるbrowser-event-checkerというツールを作ってみました👀

github.com

browser-event-checker

使い方

使い方は簡単で、ブラウザでアクセスすると発火した各種イベントが画面に記録されていきます✍

f:id:madogiwa0124:20190608223511g:plain

これだけです😇

ですが、スマホとかだとconsole.logが見れないので各ブラウザでイベントが発火しているので調べるの辛いと思うので、 そのへんが画面に表示されるのは便利なのかなと💦

リロードしても画面はそのままになるはずなので、閉じるとかリロードとかで発火するイベントのログも見ることができます👀

ちなみにサポートしているイベントは下記の通りです🙇‍

  • Window
    • pageshow
    • load
    • focus
    • beforeunload
    • pagehide
    • blur
  • Document
    • visibilitychange

仕組み

どうやってイベントの発火を記録して表示いるのかというと、かなりシンプルでコードだと下記のような感じです👩‍💻

// ログを反映する画面要素
let consoleNode = document.getElementById('console');
// LocalStrageからログを取得
const initConsoleStack = window.localStorage.getItem('consoleStack');
// あれば変数にいれて無ければ、[]を判定
let consoleStack = initConsoleStack ? initConsoleStack.split(',') : [];

// ブラウザのLocalStrageにlogを保存
function pushLocalStrage(message) {
  return new Promise(function(resolve){
    consoleStack.push(consoleStack.length + ': ' + message);
    window.localStorage.setItem('consoleStack', consoleStack);
    resolve();
  })
}

// LocalStrageに保存されているログ([hoge, fuga, piyo...])を改行タグ区切りで画面に反映
function setConsoleStackToNode() {
  consoleNode.innerHTML = consoleStack.join('</br>');
}

// window.load時にログを出力して記録・表示
window.addEventListener('load', function () {
  const message = "browser-event-checker: window load!";
  console.log(message);
  pushLocalStrage(message).then(setConsoleStackToNode);
})

ローカルストレージを使っているので、リロードとか閉じるとかのイベントも取れる感じですね。

おわりに

わりとイベント系のブラウザ、端末のバリエーションチェックとか大変だと思うので、よかったら使ってみてください🙇‍
※多分モダンブラウザでしか動かないので、IEで動作するかは知りません😇

参考

developer.mozilla.org

developer.mozilla.org

JavaScript: Promiseについて分からなすぎたので手を動かしながら学んだことをMemo

JavaScriptのPromiseがあまりにも分からなかったので、手を動かしながらいろいろと動きを勉強したので、メモしておきます✍

Promiseの基本

Promiseとは、MDNを見てみると下記のような説明がされています。
非同期処理の実行の完了や失敗を検知することが出来るようなObjectみたいですね👀

Promise オブジェクトは非同期処理の最終的な完了処理(もしくは失敗)およびその結果の値を表現します。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise

MDNのサンプルコードが下記です。new Promise()でPromiseのオブジェクトを作成して、functionの引数のresolvethenで渡したcallback、rejectcatchで渡したcallbackが入ります。※thenが成功、catchが失敗した際に実行されます。

下記のサンプルでは、0.3秒後にcallbackに引数'foo'を渡して実行するPromiseのオブジェクトを作成して、 promise1.thenに引数の値を標準出力するfunctionをcallbackとして設定しているので、fooがコンソールに表示されます。

promise1自体を標準出力するとPromiseのオブジェクトが返却されます。

var promise1 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('foo');
  }, 300);
});

promise1.then(function(value) {
  console.log(value);
  // expected output: "foo"
});

console.log(promise1);
// expected output: [object Promise]

いろいろ動かして試してみる。

まずは、検証用に下記のようなものを用意しました。
sleepは、timeで指定した時間後にcallbackを実行する遅延処理です。その他については普通に標準出力する処理です。

function sleep(time, callback) {
  setTimeout(() => { callback(null) }, time);
}
const exec_1 = () => { console.log('async 1 execute!!') }
const exec_2 = () => { console.log('async 2 execute!!') }
const exec_3 = () => { console.log('async 3 execute!!') }

下記のような処理を実行するとresultに書いたような結果になります。sleepで1秒まっているので、 exec_2よりもexec_3が先に実行されてしまっていますね。

exec_1()
sleep(1000, exec_2)
exec_3()

/* result
async 1 execute!!
async 3 execute!!
async 2 execute!!
*/

これをPromiseを使って、exec_2、exec_3の順番で実行されるようにしていこうと思います。

exec_2のあとにexec_3を実行するために遅延処理をPromiseにする。

まずは、sleepをPromiseのオブジェクトを返却するようにして、完了のタイミングで処理を実行出来るようにします。

function delay(time) {
  return new Promise((resolve) => {
    sleep(time, resolve)
  })
}

👇delaythenを使って処理修正したのが下記です。 delay(1000).then(exec_2)で1秒待ったあとにexec_2が実行されます。 thenの返り値もまたPromiseのオブジェクトになるので、then(exec_2).then(exec_3)によってexec_2実行後にexec_3が実行されます。

そのため結果は、resultのようになります。

exec_1()
delay(1000).then(exec_2).then(exec_3)
/* result
async 1 execute!!
// 1 sec
async 2 execute!!
async 3 execute!!
*/

thenのなかで更にPromiseを作成して各メソッドを1秒間隔で実行する

こうなってくると1秒間隔で各メソッドを実行したくなるわけですが、単純に下記のようにすると上手くいかないわけですよ。。

delay(1000).then().then(delay(1000).then(exec_3))

/* result
async 1 execute!!
// 2 sec
async 2 execute!!
async 3 execute!!
*/

なぜ上手く行かないかというと下記のような処理になってしまっていたわけですね。。。

  1. exec1が実行 -> async 1 execute!!が表示
  2. 1秒待つ
  3. exec2が実行予約(1秒後) -> delay(1000).then(exec_2)が実行
  4. exec3が実行予約(1秒後) -> delay(1000).then(exec_3)が実行
  5. 1秒待つ(exec_2、exec_3のdelayはほぼ同時に実行されるため)
  6. exec2が実行 -> async 2 execute!!が表示
  7. exec3が実行 -> async 2 execute!!が表示

上記を考慮して修正したコードが下記です。

exec_1()
delay(1000).then(
  () => {
    return new Promise(
      resolve => { exec_2(); delay(1000).then(resolve) }
    )
  }
).then(exec_3)

/* result
async 1 execute!!
// 1 sec
async 2 execute!!
// 1 sec
async 3 execute!!
*/
  1. exec1が実行 -> async 1 execute!!が表示
  2. 1sec待つ -> delay(1000)が実行
  3. exec_2と1秒後にcallbackを実行するPromiseが設定される -> delay(1000).then(..)が実行される
  4. thenが実行されてexec_2(); delay(1000).then(exec_3)が実行されるため、exec_2が実行されたあと1秒待ってexec_3が実行される。 -> exec_2(); delay(1000).then(resolve)が実行される

これでresultのように1秒間隔で実行される処理が実現出来ました🙌

おわりに

今回はJavaScriptのPromiseが分からなすぎたので、いろいろ実行しながら学んでみました。
やはりドキュメントを読むだけでなくて、手を動かすといろいろと理解が深まりますね。

それでは👋

参考

developer.mozilla.org

qiita.com

railsじゃなくても`activesupport`を使うといろいろと幸せになれるかもしれない話

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

みなさん、railsに入っているactivesupportというgemをご存知でしょうか?👀

blank?とかpresent?とかinquiryとかpluralizeとか、いい感じのメソッドが使えるようになるやつです🙌

railsguides.jp

このactivesupportは、個別にgemとして公開されていて、別にrailsじゃなくても使えるんですよ!

rubygems.org

使い方は普通のgemと一緒でGemfileに下記を追記して、bundle installを実行して、、、

gem 'activesupport'

使いたい部分で、requireしてあげればOKです🙆‍♂️

# 'active_support/all'だと全ての機能が有効になる
require 'active_support/all'

⚠gemはactivesupportだけど、requireするときはactive_supportにする点に注意です。

これで、blank?等の便利メソッドが有効になります🙌

require 'active_support/all'

''.blank?
=> true
'a'.blank?
=> false

gemとかツールとか、ちょっとしたAPIとか、railsを使うまでも無いけど、このへんの便利メソッドが使いたいという場合は、activesupportだけ使うと良さそうですね!💡

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

それでは👋