20220127のRubyに関する記事は11件です。

M1 Mac の Docker で gem install grpc に失敗するときの対策

docker-compose で bundle install したり、ビルドしようとするとエラーが発生。 色々試していたところ、grpc をインストールするステップで必ず失敗していること確認。 docker-compose run web gem install grpc #=> Installing grpc 1.43.1 with native extensions Gem::Ext::BuildError: ERROR: Failed to build gem native extension. ... 省略 ... ERROR: 137 alpine Linuxが悪さをしている?という記事を見てビルドイメージを変更するが改善せず。 結論、docker-compose.yml で platform を明示することで動作するようになった。 以下、実行時のファイル内容を残しておきます。 # ディレクトリ構成 root/ ├ Gemfile ├ Gemfile.lock ├ Dockerfile └ docker-compose.yml # Dockerfile FROM ruby:3.0.1 build-essential \ libpq-dev \ nodejs \ default-mysql-client \ yarn WORKDIR 'app' COPY ./Gemfile* ./ RUN bundle install # docker-compose.yml version: '3.8' services: ruby: platform: linux/x86_64 #=> 追加 build: context: ./ dockerfile: Dockerfile volumes: - ./:/app - ruby-bundle:/usr/local/bundle stdin_open: true tty: true volumes: ruby-bundle: # Gemfile gem 'google-cloud-pubsub' gem 'grpc' gem 'google-protobuf' gem 'pristine'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsとVue.js3のSPAアプリにDeepL apiを導入する

ポートフォリオ作りの過程でRails×Vue.js3のSPAアプリにDeepL apiを導入したので過程を初学者視点で紹介します。 開発環境にはdockerを使用しています。 #version Ruby 3.0.2 Rails 6.0.4 Vue.js3 #開発環境 Docker docker-compose APIで実現したいこと 上記の画像の「AI翻訳の結果を表示する」ボタンを押すと上部に表示された日本語文を英訳して表示する。 DeepL apiの登録 こちらから登録します。 50,000文字/1ヶ月の制約はありますが無料プランもあります。 登録すると自身のアカウントに認証キーが表示されるのでそれをコピーしておきます。 Rails側の実装 gem httpclient 上記gemをインストールします。DeepL apiは"https://api-free.deepl.com/v2/translate" というurlにget(もしくはpost)リクエストを認証キー、翻訳したいテキスト、変換結果の言語の3点をパラメーターとして含んだ上で送信する必要があります。httpclientはrailsのcontrollerからそのリクエストを送信するための記述を提供してくれるgemになります。 $ docker-compose build --no-cache 今回はdockerを使用しているので上記コマンドでgemをインストールします。 $ rails g controller api/translations apiを叩くためのコントローラーを用意します。既存のコントローラーを使用しても問題ありません。 $ EDITOR="vi" bin/rails credentials:edit deepl_api: api_key: DeepL apiの認証キーを貼り付け 認証キーを直打ちしなくて済むようにcredentials.ymlに秘匿します。 translations_controller.rb class Api::TranslationsController < ApplicationController require 'httpclient' #httpclientを使用できるようにします def translate @question = Question.find(params[:id]) api_key = Rails.application.credentials.deepl[:api_key] uri = "https://api-free.deepl.com/v2/translate" client = HTTPClient.new params = { auth_key: api_key, text: @question.content, target_lang: "EN" } @response = client.get(uri, query: params) render json: @response.body end end 前述の「実現したいこと」の画像に表示されている翻訳したい文章はQuestio モデルのcontentカラムに保存されているものになるのでparamsのtextには@question.contentを代入しています。 routes.rb namespace :api, format: 'json' do get '/translate/:id', to: 'translations#translate' end questionモデルのidをパラメーターとして送信するため/:idを付け加えていますが'/translate'の部分は自由に書き換えてください(例えば'/aaa/:id'でも'translations#translate'によってapi/translations_controllerのtranslateアクションを実行してくれます)。 Vue側の実装 <template> <button @click="translateWithDeepL"> AI翻訳の結果を確認する </button> <h1>翻訳結果: {{result}}</h1> </template> <script> methods: { translateWithDeepL: function() { const question_id = this.question.id axios.get('/api/translate/'+ question_id) .then(response => { this.result = response.data.translations[0].text }) } } </script> axios.get('/api/translate/' + question_id)でroute.rbに設定したget '/translate/:id'にアクセスし、translations_controllerのtranslateアクションを実行という流れになります。 具体的には仮に翻訳したいテキストが[hello]で翻訳結果の言語を日本語に設定すると https://api-free.deepl.com/v2/translate?auth_key=認証キー&text=hello&target_lang=JA というURLにgetリクエストが送信され、 { "translations": [{ "detected_source_language":"JA", "text":"こんにちは" }] } というresponseがjson形式でかえってきます。 このレスポンスのtextの部分だけ表示したいので response.data.translations[0].text という形でフロントエンド上では受け取っているわけです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

splitメソッドとそれに関連するメソッドの解説

ruby silverの勉強をしていく中で間違えたところがあったのでその解説記事を挙げていきます。 splitメソッド 指定した文字列で分割し、それを配列で返すメソッドです。 基本構文は、 "文字列".split(区切り文字, 分割数) 論より証拠ということで、実際にコードを見ていきましょう 'Yamada, Satou, Itou, Suzuki'.split(',') => ["Yamada", " Satou", " Itou", " Suzuki"] 'Yamada, Satou, Itou, Suzuki'.split('u') => ["Yamada, Sato", ", Ito", ", S", "z", "ki"] 1個目の例でいえば、対象の文字列には、「,」があるので、,を目印に区切っていくメソッドなんですね 2個目の例でいえば、「u」を目印に文字列を区切っていくようなメソッドです。 分割数の指定 分割数を指定することもできます。 'Yamada, Satou, Itou, Suzuki'.split(',', 2) => ["Yamada", " Satou, Itou, Suzuki"] 'Yamada, Satou, Itou, Suzuki'.split('u', 2) => ["Yamada, Sato", ", Itou, Suzuki"] また、分割数を制限することもできるんです。 'Yamada, Satou, Itou, Suzuki'.split(',').first => "Yamada" 正規表現とも組み合わせることが可能 'Yamada, Satou, Itou, Suzuki'.split(/[a | u ]/) => ["Y", "m", "d", ",", "S", "to", ",", "Ito", ",", "S", "z", "ki"] 正規表現に関しては、こちらのサイトを参照してみてください! 1バイトの空白文字がある場合 1バイトの空白文字とは、文字列の半角スペースのことをいいます。 この場合、その文字列の先頭と末尾の空白文字を除いてくれて、さらに、空白文字に一致する部分で分割します。 補足でいうと、改行(\n)、タブ(\t)、nilも対象になります。 'Yama da Sat ou It ou Su zuki'.split(' ') => ["Yama", "da", "Sat", "ou", "It", "ou", "Su", "zuki"] 今回の問題の正解 ()で囲われたものを含んだ結果を返すんですね。なので、 ["Apple", "-", "Banana", "-", "Lemon"] が正解ですね ちなみに、以下のようにすると答えはまた変わりますね p "Apple-Banana-Lemon".split('-') => ["Apple", "Banana", "Lemon"] 類似メソッド String#partitionメソッド [最初のセパレータより前の部分, セパレータ, それ以降の部分] の 3 要素の配列を返します。 by公式ドキュメント セパレータって何?って方は、こちらの記事を読んでみてください 要は、[最初にヒットした場所より前の場所、最初にヒットした場所、それ以降の部分]ってことです。 実際にコードを見ていきましょう p 'Ishida Keisuke'.partition('s') => ["I", "s", "hida Keisuke"] p 'Ishida Keisuke'.partition('x') => ["Ishida Keisuke", "", ""] ヒットしなかった場合は、第2要素、第3要素は空文字になる。 あまり、partitionメソッドは使われていないような気がしますね 今回はこの辺で終わりにします! 何か間違いがございましたら、ご教示いただけますと幸いです。 【参考文献】
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails7でspring起因でrails consoleが立ち上がらなかった問題の解決方法

はじめに 現在個人プロジェクトで M1 Mac Ruby3.1.0 Rails7.0.1 という環境での開発を試しています。今までは Intel Core i7 Ruby2.x Rails6.x という環境だったので、やはり動くところ、動かないところが出てきます。 (特に、Docker周りは変なバグを踏まないか戦々恐々としています) 今回はRails7のプロジェクトにて、springのバージョンのせいでrails consoleの実行が失敗した件についてです。 事象 発生したエラーはこんな感じで、 bundle exec rails c を実行した時に発生しました。 /usr/local/bundle/gems/spring-2.1.1/lib/spring/application.rb:103:in `block in preload': undefined method `mechanism=' for ActiveSupport::Dependencies:Module ActiveSupport::Dependencies.mechanism = :load ^^^^^^^^^^^^ (NoMethodError) 原因 rails newをした時にはspring 4.0.0でプロジェクトが作成されていたのですが、gemを整理してbundleし直した時にspringのバージョンが2.1.1に巻き戻っていたようです。 ↓該当の差分 対処方法 ググってたらRails Guideにてそのものズバリな記事を見つけました。 ここに「springは最低でも3.0.0にしてね!」と書いてあったおかげで差分があることに気づけました。 新規でプロジェクトを立ち上げた時には問題なくrails consoleが使えていたので、原因を見失っていましたが、springのバージョンを戻したことで問題なく動いています。 デフォルトでrails newした時には4.0.0でプロジェクトが作成されるため滅多に踏むバグではないかも知れませんが、 他のgemとの依存関係でたまたま自分のようにバージョンが巻き戻ったケース Rails6以前のプロジェクトをRails7に上げた際、springのバージョンを上げ忘れたケース で引っかかる可能性があります。 rails consoleだけでなく、rspecなどのspringを使うコマンド実行でも発生する可能性があるため、 Rails7プロジェクトで undefined method 'mechanism=' for ActiveSupport::Dependencies:Module というエラーを発見したらspringがバージョン2系以前になってないか確認してみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Jbuilder】部分テンプレートでハッシュのオブジェクトを使いたい

前提 @book_managerが配列ではなくハッシュのため、json.array!が使えない。 今まで ファイル名とインスタンス変数名が同じだと Railsが部分テンプレートを自動的に予測してレンダリングしてくれていたので、 ファイル名など指定しなくても機能していた。 api/v1/book_managers/show.json.jbuilder json.book_manager do json.partial! @book_manager end 訳あってファイル名変更 ファイル名を変更した際、下記の様に部分テンプレートを指定してみたがうまく行かない... api/v1/book_infos/show.json.jbuilder json.book_info do json.partial! 'api/v1/book_infos/book_info', collection: @book_manager, as: :book_info end うまく行った例 ローカル変数book_infoとして@book_managerを渡す。 api/v1/book_infos/show.json.jbuilder json.book_info do json.partial! 'api/v1/book_infos/book_info', book_info: @book_manager end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Hamlの基本

はじめに RailsでWebアプリにPay.jpを用いた決済機能を実装しようとしていました。 実装フローを検索していたところ、Hamlを使ってビューが実装されているドキュメントが多く、興味本位で学習しました。 これから学習しようとしている方の参考になればと学習したことをまとめてみました。 TL;DL ・RubyをベースとしたHTMLのテンプレートエンジンのこと ・HTMLよりもシンプルに記述できる ・Ruby / Ruby on Railsでも使われている 前提知識 ・HTML ・Ruby(Rubyがベースになった記法だから) ・Ruby on Rails(Railsで使いたい場合) 大枠の使い方 ① 拡張子がhamlのファイルを作成する(Ex.index.haml) ② 作成した①をhamlコマンドに通す(Ex. haml index.haml index.html ) ③ htmlファイルが生成される(Ex.index.html) ※ hamlのコマンドオプション(よく使うもの) -f format → アウトプットのフォーマットを指定(html5) -q → ダブルクオーテーションを出力 具体的な記述の仕方 【POINT】hamlの記法 1. 接頭辞でコンテンツの内容を表現 2. インデントで階層構造を表現 この二つの概念と接頭辞の種類さえ抑えれば、基本的な表現であれば、hamlで表現できないことはなくなるはずです。 接頭辞 接頭辞 接頭辞の意味 !!! DOCTYPE % 開始タグと終了タグ {:属性 => 値} タグに属性を追加する(Rubyっぽい) (属性 = 値) タグに属性を追加する(HTMLっぽい) %p テキスト pタグに囲われたテキスト < タグ内部の改行を除去 > タグ外部の改行を除去 %p{:id=>"id", :class=>"class"} <p id="id" class="class"></p> %p(id="id" class="class") <p id="id" class="class"></p> %p.class <p class="class名"></p> %p#id <p id="id名"></p> #id.class <div id="id" class="class"></div> :css styleタグ :javascript scriptタグ #{〜} 〜にRubyのコードを実行して表示 %p=〜 〜のRubyのコードを実行して表示 - 〜 〜のRubyのコードを実行して表示しない / HTMLとHaml両方に残したいコメント -# Hamlにだけ残したいコメント インデント HTMLの入れ子構造を作るために使用する ※ 同じ階層の要素は同じインデントの数に統一すること コメント / → HTMLでも残したいコメント -# → Hamlにだけ残したいコメント フォーマット HTMLと同様、最初に記載する型は決まっているので、自分用に念の為記載します。 ①hamlファイルを作成する !!! %html{:lang => "ja"} %head %meta(charset="UTF-8") %body / 一般的なタグの生成 %p hellow world! / ul, liタグの生成 %ul %li< item / divの生成 %div#id.class %div{:id=>"id", :class=>"class"} %div(id="id" class="class") #id.class / Rubyの実行と表示 #{ 3 + 5 } %p= 3 + 5 - 3 + 5 ②作成したファイルをhamlコマンドに通す haml index.haml index.html ③htmlファイルが生成される <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> </head> <body> <!-- 一般的なタグの生成 --> <p>hellow world!</p> <!-- ul, liタグの生成 --> <ul> <li>item</li> </ul> <!-- divの生成 --> <div id="id" class="class"></div> <div id="id" class="class"></div> <div id="id" class="class"></div> <div id="id" class="class"></div> <!-- Rubyの実行と表示 --> 8 <p>8</p> </body> </html> 上のhamlファイルをhtmlに変換すると、下のhtmlファイルのような記載になります。 参考文献 ドットインストール haml入門(全8回) ※ hamlをRailsで使う方法は以下の記事が参考になります。 - 【rails】htmlからhamlに一括で変換するコマンド - Railsでhamlを使う
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails 7.00における "cannot load such file --sassc"の解決法

rails7.0.0の影響によるcssの読み込みができなくなるというエラーに遭遇したので、同じ症状で悩まされている方のためになるかと思い、この記事を執筆しています。 railsの7.0.0が昨年に公開されましたが、以前のようにcssを適用させようとすると書き画像のようなエラーが出ることがあります。 このエラーの解決方法は Gemfile 46 # gem "sassc-rails" のコメントアウトを削除して、bundle installをすることで解決することができます。 rails7.00以前はこのgemが環境構築の際に自動でinstallされていましたが、7.00以降では標準でインストールされないようです.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[py2rb] 多重継承の3

はじめに 移植やってます。 ( from python 3.7 to ruby 2.7 ) 多重継承 (Python) 移植するにあたり、多重継承はRubyのミックスインで行こうと思っていました。 __init__のないクラスをモジュールにする予定でしたが、これでは避けられないようですので、委譲により多重継承を表現できればと思います。 forwardable (Ruby) require 'forwardable' class A def initialize(s) @s = s end def putsa(o) puts "#{@s}は#{o}です" end def putsd() puts "呼び出されたクラスは#{self.class}です" end end class B def initialize(s) @s = s end def putsb(o) puts "#{@s}は#{o}です" end def putsd() puts "呼び出されたクラスは#{self.class}です" end end class C extend Forwardable def initialize(*args, **kwargs) @a = A.new(kwargs['A']) @b = B.new(kwargs['B']) end def_delegators :@a, :putsa, :putsd def_delegators :@b, :putsb, :putsd end c = C.new('A' => '好きな食べ物', 'B' => '好きな飲み物') c.putsa('みかん') c.putsb('コーヒー') c.putsd() p C.ancestors # output 好きな食べ物はみかんです 好きな飲み物はコーヒーです 呼び出されたクラスはBです [C, Object, Kernel, BasicObject] 学習したばかりのforwardableを使用した例です。 メソッドが重複した場合、後のdef_delegators :@b, :putsb, :putsdが優先されるようです。 また、ancestorsにクラスAもBも検索されませんので、superで呼び出したりとかはできないものと思われます。 delegate (Ruby) require 'delegate' class A def initialize(s) @s = s end def putsa(o) puts "#{@s}は#{o}です" end def putsd() puts "呼び出されたクラスは#{self.class}です" end end class B def initialize(s) @s = s end def putsb(o) puts "#{@s}は#{o}です" end def putsd() puts "呼び出されたクラスは#{self.class}です" end end class C def initialize(*args, **kwargs) @a = SimpleDelegator.new(A).new(kwargs['A']) @b = SimpleDelegator.new(B).new(kwargs['B']) end def method_missing(name, *args) [@b, @a].each do |x| if x.respond_to?(name) return x.method(name).call(*args) end end raise "メソッド #{name} が見つかりません" end end c = C.new('A' => '好きな食べ物', 'B' => '好きな飲み物') c.putsa('みかん') c.putsb('コーヒー') c.putsd() p C.ancestors # output 好きな食べ物はみかんです 好きな飲み物はコーヒーです 呼び出されたクラスはBです [C, Object, Kernel, BasicObject] もう一つのdeletagorを使用した例です。 メソッド数が多い場合は、こちらの方が楽かもしれません。 メモ Python の 多重継承の3 を学習した 百里を行く者は九十里を半ばとす
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[py2rb] 辞書クラスの拡張2

はじめに 移植やってます。 ( from python 3.7 to ruby 2.7 ) 辞書クラスの拡張 (Python) from collections import defaultdict class BasicComposition(defaultdict): def __init__(self, *args, **kwargs): defaultdict.__init__(self, int) for k, v in list(self.items()): if not v: del self[k] def __missing__(self, key): return 'nil' def __setitem__(self, key, value): if isinstance(value, float): value = int(round(value)) elif not isinstance(value, int): print('Error') if value: super(BasicComposition, self).__setitem__(key, value) elif key in self: del self[key] d = BasicComposition() d.update({'A': 1, 'B': 2.1, 'C': 0}) d['D'] = 0 d['E'] = 1.2 for k, v in d.items(): print(k, v) print(d['F']) # output A 1 B 2.1 C 0 E 1 nil コメント欄にてご指摘いただいた通り、非整数を代入することが可能です。 運用で逃げようとも思いましたが、別件の多重継承をどうするかという話もありますので、考えてみました。 継承案 (Ruby) class BasicComposition < Hash def initialize(*args, **kwargs) self.default = 0 if args args.each do |x| self[x] += 1 end end if kwargs kwargs.each do |k, v| self[k] = v end end end def []=(key, value) if value.instance_of?(Float) value = value.round elsif value.instance_of?(Integer).! raise TypeError, 'IntegerもしくはFloat以外が指定されました。' end if value != 0 # reject 0's self.merge!({key => value}) elsif self.include?(key) self.delete(key) end end end d = BasicComposition.new d.merge!({'A' => 1, 'B' => 2.1, 'C' => 0}) d['D'] = 0 d['E'] = 1.2 d.each do |k, v| p [k, v] end puts d['F'] # output ["A", 1] ["B", 2.1] ["C", 0] ["E", 1] 0 これは、単に移植しただけですので、非整数の代入が可能です。 委譲案1 (Ruby) class BasicComposition def initialize(*args, **kwargs) @h = Hash.new(0) if args args.each do |x| @h[x] += 1 end end if kwargs kwargs.each do |k, v| @h[k] = v end end end def []=(key, value) if value.instance_of?(Float) value = value.round elsif value.instance_of?(Integer).! raise TypeError, 'IntegerもしくはFloat以外が指定されました。' end if value != 0 # reject 0's @h.merge!({key => value}) elsif self.include?(key) @h.delete(key) end end def merge!(**kwargs) if kwargs kwargs.each do |k, v| self[k] = v end end end def include?(key) @h.include?(key) end def [](key) @h[key] end def h @h end end d = BasicComposition.new d.merge!('A' => 1, 'B' => 2.1, 'C' => 0) d['D'] = 0 d['E'] = 1.2 d.h.each do |k, v| p [k, v] end puts d['F'] # output ["A", 1] ["B", 2] ["E", 1] 0 独習Ruby 450p に委譲の説明があります。 # class BasicComposition < Hash @h = Hash.new(0) Hashを継承するのではなく、内部に保持(has)します。 d.h.eachのところが格好悪いのですが、'B'の値が整数となり、'C' => 0が削除されました。 委譲案2 (Ruby) def merge!(**kwargs) # if kwargs # kwargs.each do |k, v| # self[k] = v # end # end end # output ["E", 1] 0 委譲案1のコードで、def merge!を空のメソッドにします。 結果として、merge!メソッドからの値変更をスルーすることにより、非整数の代入を防ぎます。 forwardable (Ruby) require 'forwardable' class BasicComposition extend Forwardable def initialize(*args, **kwargs) @h = Hash.new(0) if args args.each do |x| @h[x] += 1 end end if kwargs kwargs.each do |k, v| @h[k] = v end end end def_delegators :@h, :[], :include?, :each def []=(key, value) if value.instance_of?(Float) value = value.round elsif value.instance_of?(Integer).! raise TypeError, 'IntegerもしくはFloat以外が指定されました。' end if value != 0 # reject 0's @h.merge!({key => value}) elsif self.include?(key) @h.delete(key) end end def merge!(**kwargs) if kwargs kwargs.each do |k, v| self[k] = v end end end end d = BasicComposition.new d.merge!('A' => 1, 'B' => 2.1, 'C' => 0) d['D'] = 0 d['E'] = 1.2 d.each do |k, v| p [k, v] end puts d['F'] # output ["A", 1] ["B", 2] ["E", 1] 0 require 'forwardable'つよいですね。 d.eachになってスッキリしています。 @Nabetani さん、ご指摘ありがとうございました。 メモ Python の 辞書クラスの拡張2 を学習した 百里を行く者は九十里を半ばとす
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

大量データ加工処理の高速化

はじめに 今まで一般的なWeb開発はしてきたけど高速化についてあまり考慮せずに実装してきたエンジニアの方に向けて、筆者の大量データ ( 7億-8億レコード ) 加工処理高速化の経験を基に、本番実行で何を目指すかということと、それまでに実際にどのようなアプローチがあるのかを提案できればと思います。 あくまで提案なので、「こうしたらもっと高速になるよ」的な箇所は多々あるかと思います。 コメントなどでご指摘いただければ幸いです。 本記事における前提条件 本記事における前提条件を筆者の経験した環境を参考に記載します。 本プロジェクトの背景及び要件 既存サービスで蓄積されたデータを外部のシステムに連携して有用活用するにあたってのPoVを実施する。 既存サービスで蓄積されたデータの一部を匿名化 (ハッシュ化) したものをCSVに書き出し、外部のシステムにPOSTする。 本番実行までの実装及び検証は短期間 (2-4週間程度) である。 加工処理はできるだけ早くする完了させる 採用したテクノロジー Ruby on Rails API mode AWS EC2, RDS, Elasticache Redis, CloudWatch データ加工処理高速化のアプローチ 本対応における極意 塵も積もれば山となる 加工処理は要件を満たす範囲内で最速のロジックを追求する 外部接続回数は最小限に抑える 使用するデータサイズをできるだけ小さくする 使いまわせるデータは使いまわす マシンスペックを最大限活用する 実装レベル 計測 高速化実装は実行時間を計測してロジックをチューニングしながら進めます。 今回はRuby標準の benchmark というライブラリを採用しました。 benchmarkの使用方法については細かく解説しないので、以下のような記事を参考にしていただければと思います。 https://qiita.com/scivola/items/c5b2aeaf7d67a9ef310a https://nishinatoshiharu.com/usage-benchmark/ 並列実行 今回のようなデータ量を直列で加工していてはいつまでたっても処理が完了しません。 複数スレッド/プロセスに作業を分担して並列実行します。 今回はRuby Gemsの Parallel を採用しました。 # Parallel.processor_countでCPUのコア数が取得できる csv_data += Parallel.map(records, in_processes: Parallel.processor_count) do |record| # some process end このような記述にすると、 records の要素が record としてブロックに渡され、各プロセスで加工処理できるようになります。 今回のケースにおいて in_thread と in_processes は計測の結果、プロセスに処理させる方が多少高速だったため in_processes を採用しました。 in_thread を使用し、 #some_process 内でDB接続する部分がある場合は、コネクションプールを使用して高速化することも検討できます。 加工処理の高速化 分割抽出 大量なデータを一度にDBから取得しようとするのは非常に非効率です。 find_in_bataches で細切れにデータを取得して高速化を図ります。 SomeModel.find_in_batches(batch_size: Constants::Batch::BATCH_SIZE) do |records| # some process csv_data += Parallel.map(records, in_processes: Parallel.processor_count) do |record| # some process end # some process end *Constants::Batch::BATCH_SIZE は独自に宣言した定数です。最終的に 100_000 で設定しました。 必要な属性のみ抽出 今回CSVには必要な属性のみ書き出すため、抽出する属性も最小限に絞ります。 また、加工対象のテーブルがパーティショニングされている場合は、 SomeModel の部分にパーティションが渡されるような実装になるかと思います。 SomeModel.select_csv_columns.find_in_batches(batch_size: Constants::Batch::BATCH_SIZE) do |records| # some process csv_data += Parallel.map(records, in_processes: Parallel.processor_count) do |record| # some process end # some process end scope :select_csv_columns, -> do select([ :some_colum, ... ]) end 加工に使用されるアルゴリズムなどのチューニング 今回特定の属性を匿名化するため、最初は SHA-512 でハッシュ化していました。 Digest::SHA512.hexdigest("#{string}") 今回ハッシュ化のセキュリティレベルに関して特段指定がなかったため、より高速な SHA-1 に変更しました。 Digest::SHA1.hexdigest("#{string}") 処理速度の比較.1 処理速度の比較.2 インフラレベル 全体像 全体のシーケンス図を描いてみました。 各EC2インスタンスにssh接続し、それぞれのインスタンス内でrakeタスクを実行する形で構成されています。(ここは正直時間が足りず...改善点は多々あると思っています) Rakeタスクのパラメータにデータ抽出のオフセット値を渡し、処理対象を分散するようになっています。 対象テーブルの加工時に、他テーブルへの参照が必要な場合ループ単位で外部参照のSQLが発行されてしまいます。 できる限りDB接続を減少させるため、一度参照したKVはRedisにストアし、値がセットされている場合はそちらを使用します。 また、rakeタスク実行時にはCloudWatchでメモリ使用率やIOPSを監視し、異常が発生したらすぐに対応できる体制を整えます。 さいごに 今回全体での処理経過時間は9時間程度でした。 (EC2 a1.4xlarge:8台, RDS db.t3.medium:1台, Elasticache t2.small:1台) 実行方法に関しての検討はタイムアップで脳筋実行になってしましたが、 処理単位を分割したタスクをキューイングし、各インスタンスがタスクを拾いにくる、という形を作ると処理効率を最大化できそう というアドバイスをいただいたので、次回このような機会があったときには試してみたいと思っています。 その他より良い方法がある方はご意見いただけると幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

binding.pryが止まらない!

railsのデバッグのために binding.pry を使ったが、ループの中に入れてしまったので exit をしてもずっと止まらない! こんな時に使えるコマンドがこちらです!↓ $ exit! $ !!! 上記のどちらかで止まります! 試してみてください^^
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む