読者です 読者をやめる 読者になる 読者になる

Pandas Cheatsheet

python pandas

数ヶ月ぶりにpandasを使うと基本すら忘れていることが多いので、チートシートを残しておきます。 なお、pandas (0.18.1) です。

データの読み込み

csvから読み込む。 先頭行のヘッダは自動で読み込んでくれます。

import pandas as pd

df_train = pd.read_csv('train.csv', index_col='id')

numpyからDataFrameに変換する。

import numpy as np
import pandas as pd

pd.DataFrame(np.random.rand(3,2), index=[10, 11, 12], columns= ['x', 'y'])

Index(行)とColumns(列)

indexとcolumnsを設定する。

df.index = [1, 2, 3]
df.columns = ['a', 'b']

columnでsliceする。

new_df = df['b']

新しいcolumnを追加する。

df['c'] = np.array(3)

columnを削除する。

df.drop('c', axis = 1)

データの書き出し

csv

df.to_csv('output.csv')

numpy

df.values

capistrano3でshared_pathにディレクトリを作成する

capistrano rails

いつも忘れるので、メモっておく。

namespace :tmp_dir do
  task :create do
    on roles(:all) do
      within shared_path do
        unless test "[ -e tmp/subdir]"
          execute :mkdir, "-p", "tmp/subdir"
        end
      end
    end
  end
end
after "deploy:check", "tmp_dir:create"

Rails Good Parts

rails

Railsにまつわる便利機能・デザインパターンはたくさんありすぎて、正直覚えきれないので、 個人的に役立ったものまとめていこうと思います。

ActiveModel

validation

バリデーションを特定のコンテキストでのみ実行する方法。

ActiveModel::Validations

class Person
  include ActiveModel::Validations

  attr_accessor :name
  validates_presence_of :name, on: :new
end

person = Person.new
person.valid?       # => true
person.valid?(:new) # => false

ActionView

disable_with

二重サブミット防止するために、Javascriptを書く必要なし!

http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-submit_tag

submit_tag "Complete sale", data: { disable_with: "Submitting..." }

Patterns

Form Object

特定のコンテキストでしか利用しないバリデーション(例: 特定のユーザロールのみ、入力を必須としたいテキストフィールド)をModelレイヤで実装すると、複雑化することが多いように思います。 このようなケースでは、Form Object の導入は一つの選択肢になります。 blog.sundaycoding.com

PackerのAnsible RemoteでSSH ForwardAgentする

packer ansible

PackerのAnsible Remote ProvisionerでAMIをビルドしていますが、 SSH ForwardAgentできずにだいぶハマったので、備忘のためにメモしておきます。

やりたいこと

githubのプライベートリポジトリをansibleでcloneしたい。 公開鍵認証する必要あるが、private key はアップロードしたくないので、forward agent で解決したい。

なお、packerのバージョンは0.11.0です。

やったこと

packer.json

{
  "variables": {
    "aws_access_key": "",
    "aws_secret_key": ""
  },
  "builders": [{
    "type": "amazon-ebs",
    "access_key": "{{user `aws_access_key`}}",
    "secret_key": "{{user `aws_secret_key`}}",
    "region": "ap-northeast-1",
    "source_ami": "ami-xxxxxxx",
    "instance_type": "t2.small",
    "ssh_username": "ec2-user"
  }],
  "provisioners": [{
    "type": "ansible",
    "user" : "ec2-user",
    "sftp_command" : "/usr/libexec/openssh/sftp-server -e",
    "playbook_file": "ansible/playbook.yml",
    "ansible_env_vars": [ "ANSIBLE_HOST_KEY_CHECKING=False", "ANSIBLE_SSH_ARGS='-o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s'"]
   }]
}

ここではANSIBLE_SSH_ARGS環境変数に -o ForwardAgent=yes をセットしています。

ansible.cfg

[defaults]
sudo_flags=-HE

このオプションによりsudoでも環境変数を引き継ぐようになります。 sudoのmanには以下のように書かれています。

       -E          The -E (preserve environment) option will override the env_reset option in sudoers(5).  It is only available when either the matching command has the SETENV tag or the setenv option is set in sudoers(5).  sudo will return an
                   error if the -E option is specified and the user does not have permission to preserve the environment.

これでANSIBLE_SSH_ARGSが有効になりました。

role

- name: git clone repository
  git:
    repo: 'git@github.com:organization/your-awesome-repo.git'
    dest: '/home/yourname/dir'
    accept_hostkey: yes
    version: master

あとはcloneするだけ。

References

Multithreaded Programming - Operating System Concepts Chapter 4

ちょっとしたメモ。

Operating System Concepts の Chapter 4 の冒頭に

A thread is a basic unit of CPU utilization; it comprises a thread ID, a program counter, a register set, and a stack. It shares with other threads belonging to the same process its code section, data section, and other operating-system resources, such as open files and signals.

https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/images/Chapter4/4_01_ThreadDiagram.jpg

出典: https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html

と書かれている。

ざっくりいうと、プログラムカウンタ、レジスタ、スタックはそれぞれで保持するけど、データセクション(含 グローバル変数)、オープンファイルやシグナルは共有するということ。

グローバル変数について実際に確認してみる。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int x = 0;
pthread_t tid[2];

void* incr()
{
  x = x + 1;
  printf("%d\n", x);
}

int main()
{
  int i = 0;
  int err;

  while(i < 10) {
    err = pthread_create(&(tid[i]), NULL, &incr, NULL);
    if (err != 0)
        printf("can't create thread\n");

    i++;
  }

  sleep(1);
}
$ gcc -pthread thread.c && ./a.out
1
2
3
4
5
6
7
8
9
10

確かに、スレッドで共有されていることが確認できる。

次に、fork(2)で同じようなコードを書いてみる。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int x = 0;

void* incr()
{
  x = x + 1;
  printf("%d\n", x);
}

int main()
{
  int i = 0;
  int err;

  pid_t pid;

  while(i < 10) {
    pid = fork();
    // child
    if (pid == 0) {
      incr();
      exit(0);
    }
    // parent
    i++;
  }

  sleep(1);
}
$ gcc fork.c && ./a.out
1
1
1
1
1
1
1
1
1
1

これは共有されない。

Action Mailerでfromフィールドに差出人名を表示したい

rails

よく見かけるAction Mailer のサンプルはこんな感じだと思います。

mail from: 'noreply@example.com',
     to: 'foobar@example.com',
     subject: 'Hi'

このケースでは差出人は noreply@example.com となるんですが、時にサービス名などを設定したくなることもあると思います。 今回はActionMailerでどうやるといいの? って話です。

ドキュメント

まずはrails/actionmailer at master · rails/rails · GitHubを探しますが、特に方法は書かれていません。

ActionMailerはMailのラッパーなので、GitHub - mikel/mail: A Really Ruby Mail Libraryに目を通すと、それらしきサンプルが見つかります。

mail = Mail.new do
  to      'nicolas@test.lindsaar.net.au'
  from    'Mikel Lindsaar <mikel@test.lindsaar.net.au>'
  subject 'First multipart email sent with Mail'
end

"Mikel Lindsaar"の部分を置き換える形で運用を始めたところ、ごく稀にエラーが発生してメールが送信できない問題にぶつかりました。

An ArgumentError occurred in *****#create:

  An SMTP From address is required to send a message. Set the message smtp_envelope_from, return_path, sender, or from address.
  app/controllers/*****.rb:123:in `****************'

原因

ArgumentErrorはmail/check_delivery_params.rb at df48a05a7fb5a4271e6df12da7afb26a53494a18 · mikel/mail · GitHubにてraiseされていました。smtp_envelope_from がblankだとこのエラーが発生します。

いろいろとテストした結果、少なくとも ドット「.」や括弧「(」「)」が表示名に含まれている場合にエラーが再現することが確認できました。

name = 'kotaroi.(test)'

mail = Mail.new do
  from    "#{name} <noreply@example.com>"
  to      'foobar@example.com'
  subject 'This is a test email'
end

mail.smtp_envelope_from # => nil

ということで、rfcを読み解きます。本来は運用前に調べておくべき話ですね(汗

RFC

obsoleteやupdateがたくさんあるので、どれを読むべきか把握するのが大変でした。。 今回読むべきは RFC 5322 - Internet Message Format で、FromフィールドはこのRFCで規定されています。

3.6.2.  Originator Fields

   The originator fields of a message consist of the from field, the
   sender field (when applicable), and optionally the reply-to field.
   The from field consists of the field name "From" and a comma-
   separated list of one or more mailbox specifications.  

と記載されており、セクションを少し遡ると、

3.4.  Address Specification

   Addresses occur in several message header fields to indicate senders
   and recipients of messages.  An address may either be an individual
   mailbox, or a group of mailboxes.

   address         =   mailbox / group

   mailbox         =   name-addr / addr-spec

   name-addr       =   [display-name] angle-addr

   angle-addr      =   [CFWS] "<" addr-spec ">" [CFWS] /
                       obs-angle-addr

   group           =   display-name ":" [group-list] ";" [CFWS]

   display-name    =   phrase

   mailbox-list    =   (mailbox *("," mailbox)) / obs-mbox-list

と書かれています。最終的に

   atext           =   ALPHA / DIGIT /    ; Printable US-ASCII
                       "!" / "#" /        ;  characters not including
                       "$" / "%" /        ;  specials.  Used for atoms.
                       "&" / "'" /
                       "*" / "+" /
                       "-" / "/" /
                       "=" / "?" /
                       "^" / "_" /
                       "`" / "{" /
                       "|" / "}" /
                       "~"

   atom            =   [CFWS] 1*atext [CFWS]

   dot-atom-text   =   1*atext *("." 1*atext)

   dot-atom        =   [CFWS] dot-atom-text [CFWS]

   specials        =   "(" / ")" /        ; Special characters that do
                       "<" / ">" /        ;  not appear in atext
                       "[" / "]" /
                       ":" / ";" /
                       "@" / "\" /
                       "," / "." /
                       DQUOTE

   qtext           =   %d33 /             ; Printable US-ASCII
                       %d35-91 /          ;  characters not including
                       %d93-126 /         ;  "\" or the quote character
                       obs-qtext

   qcontent        =   qtext / quoted-pair

   quoted-string   =   [CFWS]
                       DQUOTE *([FWS] qcontent) [FWS] DQUOTE
                       [CFWS]

(中略)


   word            =   atom / quoted-string

   phrase          =   1*word / obs-phrase

に辿り着きました。

これにより、表示名(display-name)は quoted-string でない場合は specials を含めてはならないことを確認できました。裏を返せば、specials を使いたい場合は quoted-string とすればよいです。

次は「Ascii以外はどうするの」という疑問が出てきますが、RFC 6532 - Internationalized Email Headers にてUTF-8が使えるよう拡張されています。

3.2.  Syntax Extensions to RFC 5322


   The preceding changes mean that the following constructs now allow
   UTF-8:

   1.  Unstructured text, used in header fields like "Subject:" or
       "Content-description:".

   2.  Any construct that uses atoms, including but not limited to the
       local parts of addresses and Message-IDs.  This includes
       addresses in the "for" clauses of "Received:" header fields.

   3.  Quoted strings.

   4.  Domains.

コード

specials が含まれるか否かを判定し、含まれる場合は quoted-string(つまりダブルクォートで囲む)とし、さらにダブルクォートとバックスラッシュをエスケープすればよさそうです。

def envelope_display_name(display_name)   
  name = display_name.dup

  # Special characters
  if name && name =~ /[\(\)<>\[\]:;@\\,\."]/
    # escape double-quote and backslash
    name.gsub!(/\\/, '\\')
    name.gsub!(/"/, '\"')

    # enclose
    name = '"' + name + '"'
  end

  name
end


name = envelope_display_name('display name here')

mail from: "#{name} <noreply@example.com>",
     to: 'foobar@example.com',
     subject: 'Hi'

日本語(UTF8-non-ascii)が含まれていても、問題なく動作することを確認済みです。

まとめ

ActionMailerで差出人名を変更する方法を調べました。 本当は mail gem の実装まで確認しておいたほうがいいんですが、時間が限られてるのでまた今度。。

bundler環境で動いてるunicornでgem が更新されない話

TL;DR

Unicornをpreload_app=falseで運用してる場合は、

before_exec do |server|
  ENV["BUNDLE_GEMFILE"] = File.join(project_home, "Gemfile")
end

の設定をした上で、SIGUSR2 を使いましょう。 capistrano3-unicornを利用しているなら、下記の通りです。

after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:restart'
  end
end

発生していた問題

新しいgemをGemfileに追加してcapistranoでデプロイするも、なぜか新しいgemを認識してくれない。

## unicorn.log

E, [2016-09-08T15:02:36.583698 #5571] ERROR -- : uninitialized constant ExceptionNotifier::Rake (NameError)
/home/myapp/myapp/releases/20160908060023/config/initializers/exception_notification.rb:10:in `<top (required)>'

(以下略)

設定

preload_appはfalseで運用しているため、capistrano3-unicornのREADMEに従って、config/deploy.rbには下記の通り記述しています。

after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:reload'
  end
end

この設定により cap production deploy を実行すると、SIGHUP をunicornに送ることになります。

Running /usr/bin/env kill -s HUP `cat /home/myapp/myapp/current/tmp/pids/unicorn.pid` on ***.***.***.***

原因

2つほど誤解(というか理解不足)がありました。

誤解その1

http://unicorn.bogomips.org/Sandbox.html に従って、before_exec で ENV["BUNDLE_GEMFILE"] をセットしていました。

before_exec do |server|
  ENV["BUNDLE_GEMFILE"] = File.join(project_home, "Gemfile")
end

が、これがうまく動作しません。

UnicornSIGHUPをハンドリングしている箇所を読んで見ると、preload_app=false では before_exec を実行しないことが判明。。。

誤解その2

そうは言っても https://unicorn.bogomips.org/SIGNALS.html のHUPには

When reloading the application, Gem.refresh will be called so updated code for your application can pick up newly installed RubyGems.

と書かれているじゃないか、と思うわけです。

実際、unicornコードやログを見ても、Gem.refresh を呼び出しているようにも見えます。

I, [2016-09-09T11:49:21.648309 #5967]  INFO -- : worker=0 spawned pid=5967
I, [2016-09-09T11:49:21.667362 #5967]  INFO -- : Refreshing Gem list
I, [2016-09-09T11:49:23.931829 #30636]  INFO -- : reaped #<Process::Status: pid 14112 exit 0> worker=1
I, [2016-09-09T11:49:23.933808 #5970]  INFO -- : worker=1 spawned pid=5970
I, [2016-09-09T11:49:23.934109 #5970]  INFO -- : Refreshing Gem list

が、どうも疑わしいので Bundler のコードを追いかけてみました。

    # Because Bundler has a static view of what specs are available,
    # we don't #refresh, so stub it out.
    def replace_refresh
      gem_class = (class << Gem; self; end)
      redefine_method(gem_class, :refresh) {}
    end

空っぽに置き換えられていることが判明。。。

どうすればよいか?

USR2 シグナルを使えばOKです。

Unicornのmasterプロセスは USR2 シグナルを受取ると、自身をforkして unicornコマンドをexecします。

I, [2016-09-09T16:54:40.831653 #10698]  INFO -- : executing ["/home/myapp/myapp/shared/bundle/ruby/2.2.0/bin/unicorn", "-c", "/home/myapp/myapp/current/config/unicorn/sandbox.rb", "-E", "deployment", "-D", {12=>#<Kgio::TCPServer:fd 12>}] (in /home/myapp/myapp/releases/20160909075156)

コードを見ると、execの直前でbefore_execを実行しているので、

before_exec do |server|
  ENV["BUNDLE_GEMFILE"] = File.join(project_home, "Gemfile")
end

でBUNDLE_GEMFILEをセットしておけばよいです。これで新しいGemfileをexecするプロセスで認識できます(この時点でcurrent symlinkは切り替え済み)。

なお、execで実行されるunicornのbinはGem.bin_pathを呼び出すのですが、BundlerがこのコードによりGem.bin_pathを書き換えているので新しいgemを認識できるということのようです。(詳しく追ってないので、もしかしたら間違っているかも)