Engineering from Scratch

エンジニア目指してます

2022/06/12

Unix Processes

Diving into Rack

Rackのrackupコマンドは,プロセスをデーモン化できるため,そのコードを読んでいく。

    exit if fork
    Process.setsid
    exit if fork
    Dir.chdir "/" 
    STDIN.reopen "/dev/null"
    STDOUT.reopen "/dev/null", "a" 
    STDERR.reopen "/dev/null", "a" 

Daemonizing a Process, Step by Step

exit if fork

forkメソッドは値を,親プロセス・子プロセスで計2回返す。親プロセスでは子プロセスのpidを,子プロセスではnilを返す。

そのため,親プロセスは終了し,子プロセスは存在したままになる。親プロセスの存在しない子プロセス(orphaned process)のppidは1となる。

Process.setsid

以下の3つのことを行う。

  1. プロセスが新しいセッションのセッションリーダーとなる
  2. プロセスが新しいプロセスグループのグループリーダーとなる
  3. プロセスはコントロールできるターミナルを持たない

Process Groups and Session Groups

全てのプロセスは一つのグループに属し,それぞれのグループは一意のIDを持つ。代表的なプロセスグループは,親プロセスとその子プロセス群。

普通,プロセスグループのidは,プロセスグループリーダーのpidと同じになる。

irb(main):005:0> puts Process.getpgrp
16852
=> nil                                                        
irb(main):006:0> puts Process.pid
16852
=> nil 

また,プロセスグループの集合がセッショングループ。

exit if fork

forkされ,プロセスグループとセッショングループのリーダーとなった子プロセス自身は終了し,その子プロセスがforkされる。

新しくforkされたプロセスは,プロセスグループのリーダーでもなく,セッションリーダーでもなく,支配できるターミナルも持っていない。

DIr.chdir "/"

カレントディレクトリを,ルートディレクトリに変更する。必ずしも必要な処理ではないが,デーモンのカレントディレクトリが実行中に消えないようにする。

STDIN.reopen "/dev/null"
STDOUT.reopen "/dev/null", "a"
STDERR.reopen "/dev/null", "a"

デーモンプロセスの出力先を変更する。

参考

workingwithruby.com

Rails の select メソッド

カラムを指定することができるメソッド。以下のような,users テーブルを考える。

irb(main):008:0> User
=> User(id: integer, name: string, department_id: integer, company_id: integer, created_at: datetime, updated_at: datetime)

上記の User モデルの一つ目のデータを取得した結果は以下となる。

irb(main):009:0> User.first
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=>
#<User:0x000000010b861bc0
 id: 1,
 name: "ユーザー_0",
 department_id: 1,
 company_id: 1,
 created_at: Sun, 12 Jun 2022 08:06:02.012826000 UTC +00:00,
 updated_at: Sun, 12 Jun 2022 08:06:02.012826000 UTC +00:00>

SELECT 句で"users".*が指定され,全てのカラムが取得されている。しかし,idnameカラムのみが欲しい時は,selectメソッドを使うことで指定したカラムのみを取得できる。

irb(main):010:0> User.select("id", "name").first
  User Load (0.3ms)  SELECT "users"."id", "users"."name" FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x000000010ae98580 id: 1, name: "ユーザー_0">

SELECT 句では,"users"."id", "users"."name"が指定されており,idnameカラムのみを取得できる。

次に,usersテーブルが以下のようなdepartmentsテーブルに属しているとする。

irb(main):011:0> Department
=> Department(id: integer, name: string, department_code: string, created_at: datetime, updated_at: datetime)

usersテーブルとdepartmentsテーブルを内部結合し,selectメソッドを使ってusersテーブルのidnameカラム,departmentsテーブルのnameカラムを取得する。以下では,json 形式で出力する。

    render json: User
      .joins(:department)
      .select(
        "name",
        "id",
        "departments.name"
      )

出力は以下となる。

[{"name":"部門_0","id":1},{"name":"部門_1","id":2},{"name":"部門_2","id":3},{"name":"部門_0","id":4},{"name":"部門_1","id":5},{"name":"部門_2","id":6},{"name":"部門_0","id":7},{"name":"部門_1","id":8},{"name":"部門_2","id":9},{"name":"部門_0","id":10},{"name":"部門_1","id":11},{"name":"部門_2","id":12}]

nameカラムがdepartmentsテーブルのnameとなってしまっている。usersテーブルとdepartmentsテーブルのnameカラムの区別がされず,上書きされてしまっていることが原因。

以下のように,asを使ってdepartmentsテーブルのnameカラムを区別する。

    render json: User
      .joins(:department)
      .select(
        "name",
        "id",
        "departments.name as department"
      )

以下のエラーが出てしまった。

missing attribute: department_id

どうやらasに,テーブル名の単数形は使えないよう。修正したコード及びその結果は以下となる。

    render json: User
      .joins(:department)
      .select(
        "name",
        "id",
        "departments.name as department_name"
      )
[{"name":"ユーザー_0","id":1,"department_name":"部門_0"},{"name":"ユーザー_1","id":2,"department_name":"部門_1"},{"name":"ユーザー_2","id":3,"department_name":"部門_2"},{"name":"ユーザー_3","id":4,"department_name":"部門_0"},{"name":"ユーザー_4","id":5,"department_name":"部門_1"},{"name":"ユーザー_5","id":6,"department_name":"部門_2"},{"name":"ユーザー_6","id":7,"department_name":"部門_0"},{"name":"ユーザー_7","id":8,"department_name":"部門_1"},{"name":"ユーザー_8","id":9,"department_name":"部門_2"},{"name":"ユーザー_9","id":10,"department_name":"部門_0"},{"name":"ユーザー_10","id":11,"department_name":"部門_1"},{"name":"ユーザー_11","id":12,"department_name":"部門_2"}]

また,SQL は以下のようになる。

SELECT "users"."name", "users"."id", departments.name as department_name FROM "users" INNER JOIN "departments" ON "departments"."id" = "users"."department_id"

参考

https://pikawaka.com/rails/select

eager_load と preload

joins

association をキャッシュしないため,join 先のテーブルで検索をかけたい時に使用する。

先ほど使用した,usersdepartmentsテーブルを考える。

usersのうち,departmentsname"部門_0"であるものを取得する。

irb(main):019:0> User.joins(:department).where(department: { name: "部門_0" }).count
  User Count (2.4ms)  SELECT COUNT(*) FROM "users" INNER JOIN "departments" "department" ON "department"."id" = "users"."department_id" WHERE "department"."name" = ?  [["name", "部門_0"]]
=> 4

しかし,departmentsはキャッシュしないため,departments.nameを取得しようとすると,N+1 となる。

irb(main):020:1* User.joins(:department).where(department: { name: "部門_0" }).each do |user|
irb(main):021:1*   puts user.department.name
irb(main):022:0> end
  User Load (0.3ms)  SELECT "users".* FROM "users" INNER JOIN "departments" "department" ON "department"."id" = "users"."department_id" WHERE "department"."name" = ?  [["name", "部門_0"]]
  Department Load (0.3ms)  SELECT "departments".* FROM "departments" WHERE "departments"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
部門_0
  Department Load (0.1ms)  SELECT "departments".* FROM "departments" WHERE "departments"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
部門_0
  Department Load (0.1ms)  SELECT "departments".* FROM "departments" WHERE "departments"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
部門_0
  Department Load (0.1ms)  SELECT "departments".* FROM "departments" WHERE "departments"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
部門_0

eager_load

指定した assocition を LEFT OUTER JOIN で引いてキャッシュする。JOIN しているため,JOIN 先のテーブルで絞り込みができる。

irb(main):024:0> User.eager_load(:department)
  SQL (0.5ms)  SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."department_id" AS t0_r2, "users"."company_id" AS t0_r3, "users"."created_at" AS t0_r4, "users"."updated_at" AS t0_r5, "departments"."id" AS t1_r0, "departments"."name" AS t1_r1, "departments"."department_code" AS t1_r2, "departments"."created_at" AS t1_r3, "departments"."updated_at" AS t1_r4 FROM "users" LEFT OUTER JOIN "departments" ON "departments"."id" = "users"."department_id"

先ほどと同様に,departmentsnameで絞り込みをして,departments.nameを出力する。

irb(main):036:1* User.eager_load(:department).where(department: { name: "部門_0" }).each do |user|
irb(main):037:1*   puts user.department.name
irb(main):038:0> end
  SQL (0.4ms)  SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."department_id" AS t0_r2, "users"."company_id" AS t0_r3, "users"."created_at" AS t0_r4, "users"."updated_at" AS t0_r5, "department"."id" AS t1_r0, "department"."name" AS t1_r1, "department"."department_code" AS t1_r2, "department"."created_at" AS t1_r3, "department"."updated_at" AS t1_r4 FROM "users" LEFT OUTER JOIN "departments" "department" ON "department"."id" = "users"."department_id" WHERE "department"."name" = ?  [["name", "部門_0"]]
部門_0
部門_0
部門_0
部門_0

association 先であるdepartmentsテーブルがキャッシュされているため,N+1 は発生しない。

preload

preloadを実行した結果は以下となる。

irb(main):006:0> User.preload(:department)
  User Load (8.8ms)  SELECT "users".* FROM "users"
  Department Load (3.3ms)  SELECT "departments".* FROM "departments" WHERE "departments"."id" IN (?, ?, ?)  [["id", 1], ["id", 2], ["id", 3]]

SQL 文が2回発行されている。usersテーブルのデータを全て取得した後に,その外部キーでdepartmentsテーブルを取得し,キャッシュしている。そのため,eager_loadと同様に N+1 が発生しない。

irb(main):007:1* User.preload(:department).each do |user|
irb(main):008:1*   puts user.department.name
irb(main):009:0> end
  User Load (0.9ms)  SELECT "users".* FROM "users"
  Department Load (0.4ms)  SELECT "departments".* FROM "departments" WHERE "departments"."id" IN (?, ?, ?)  [["id", 1], ["id", 2], ["id", 3]]
部門_0
部門_1
部門_2
部門_0
部門_1
部門_2
部門_0
部門_1
部門_2
部門_0
部門_1
部門_2

しかし,association テーブルで絞り込もうとするとエラーとなる。

irb(main):012:0> User.preload(:department).where(department: { name: "部門_0" }).first
  User Load (1.1ms)  SELECT "users".* FROM "users" WHERE "department"."name" = ? ORDER BY "users"."id" ASC LIMIT ?  [["name", "部門_0"], ["LIMIT", 1]]
/ `initialize': SQLite3::SQLException: no such column: department.name (ActiveRecord::StatementInvalid)

参考

https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58