天泣記

2007-08-01 (Wed)

#1

「ボタンを消せ」――Appleのジョブズ氏が追求する命題

「東京のAppleストアのエレベータには階数ボタンがない」そうな。いってみるか?

それはそれとして、ボタンに限らず、ユーザの入力を極力減らすのは基本だろうと思う。

ただ、入力しなければならない情報が同じなら、その通り道をどのような広さ・狭さに設計するかは問題だろう。

すべての情報を 2進にエンコードして、ボタンふたつ (あるいはメソッドふたつ) で伝えたいというひとはいない。

マウスのボタンをひとつにするかふたつにするかというのは、通り道に関する問題だと思う。

ユーザインターフェースのメニューに関する知見や、Ruby の API の経験では、通り道は、広く、浅くするのがよいという見識がある。API でいえば、やりたいことを直接表現するたくさんのメソッドがあったほうがいいし、やりたいことがひとつしかないのであれば、インスタンスを生成した上でメソッドでやるよりはクラスメソッドでやったほうが使いやすい。

しかし、ボタンを少なく、というのは通り道を狭くするということであるから、上記の見識には反する。

にもかかわらずそれがうまくいっているように思えるのは、なぜだろうか。

考えてみると、通り道を狭くすることにより、狭く、浅い通り道をデザインさせようとしているのかもしれない。

通り道が深いと (ひとつのことをやるのに何ステップも操作が必要だと) 明らかにそれは使いにくいので、どうにかして操作の削減をしようという気になるかもしれない。

エレベータから階数ボタンをなくすというのは、ボタンを減らすという問答無用な方針があったからこそ、階数の情報が不要であるということに気がついて、入力しなければならない情報を削減するという結果を得られたのかもしれない。

2007-08-04 (Sat)

#1

LL

2007-08-12 (Sun)

#1

暑いので「温度計」を検索してみたところ、ガリレオ温度計なるものを知る。

2007-08-15 (Wed)

#1
% echo -n a | wc -l  
0

へー、wc って不完全な行を数えないんだ。

POSIX を調べると... おぉ、newline の数なのか。なるほど。

2007-08-16 (Thu)

#1

Ruby のブロックパラメータの挙動は長いあいだ謎であった。

だが、ついに、もはや謎ではない、と言うことができる。

この「謎」はメソッド呼び出しと微妙に動作が異なり、さらにその違いが理解できない、ということである。

この謎に挑戦したひとは (私を含め) 過去に何人もいて、だいたいにおいてメソッド呼び出しに挙動をあわせようとして議論をするのだが、全面的に受け入れられることはなく、しかしながらそのたびに微妙に動作が変わることもあって、謎の成長に貢献してきた。

しかし、現在 (1.9) ではメソッド呼び出しとの違いは以下に集約され、かつ、その理由がわかる。

なお、lambda な場合はメソッド呼び出しとまったく同じである。(lambda なブロックというのは lambda および define_method についたブロックである。1.9 では Proc オブジェクトを inspect や p してわかる)

上記の違いには理由がある。忘れても後から調べられるよう以下にまとめておく。

引数の数の違いを無視する理由は 10.times { puts "hello" } などを許容するためである。Integer#times はブロックにひとつ引数を渡すが、このブロックでは受け取っていない。このような用法は非常に多く、現時点で ArgumentError にすることは非現実的である。(ただ、ここはメソッド呼び出しにさらに近づける余地があるかもしれない)

配列の展開の理由は、大きな原因は標準添付されているライブラリの rss である。rss には以下のようなコードが大量にある。

[
  ['title', nil, :text],
  ['link', nil, :text],
  ['description', nil, :text],
  ['image', '?', :have_child],
  ['items', nil, :have_child],
  ['textinput', '?', :have_child],
].each do |tag, occurs, type|
  ...
end

配列の配列を each して内側の配列を引数の並びとして受け取るというコードである。このような用法は rss だけにみられるものではないが、rss にはとくに大量にある。

ここで、Array#each が配列の要素をひとつ yield するとすると、yield されたひとつの実引数に対し 3つの仮引数で数があわない。もしブロックの呼び出しがメソッド呼び出しと同じ挙動であるとすれば、これは ArgumentError である。ArgumentError を避けるためには、以下のようにブロックパラメータを括弧で括り、ひとつの引数として受け取ってそれを配列として 3つに分解することができる。

[
  ['title', nil, :text],
  ...
  ['textinput', '?', :have_child],
].each do |(tag, occurs, type)|
  ...
end

[ruby-dev:29616] を実装したとき、最初は上のように括弧で括ればいいと考えて、配列の展開は行わなかった。しかし、テストを通すために必要なところに括弧を挿入していったところ、最終的に rss でめげたのである。多すぎてあきらめたのである。めげてしまい、また、配列の展開は使い勝手がいいということを認めざるを得なかったため、配列の展開は行うことにした。

ここで、配列の展開は呼び出し側でなく呼び出される側で行われるが、引数が配列ひとつであっても必ず行われるわけではない。

たとえば、{|*r|} では決して展開が行われず、引数の並びが配列としてそのまま得られるので、ブロックの引数の並びをそのまま転送することができる、つまりラッパーを実現することができる。

では、展開が行われる条件はなにかというと、引数が配列ひとつであったときで、さらにブロックパラメータが以下の形式の場合である。

なお、展開が行われないブロックパラメータの形式は何があるかというと、以下がある。

一般に、ブロックに対するブロック引数 (上記の &b) は展開の有無には関係しないが、構文上、&b があると {|x,|} という余計なカンマがある形式は記述できないということだけには関係する。

あと、上記の「普通の引数」は括弧で括られた配列分解の指定でもよい。つまり {|x|} の x は (y,z) でもよくて、{|(y,z)|} でも展開は行われない。

ブロックパラメータの形式によって展開されるかどうかが異なるのにはそれぞれに理由がある。

あと、{|x,*r|} でも展開を行うが、これについては十分に用法が確立しているかどうかは自信がない。ただ、配列ひとつを展開せずに {|x,*r|} に適用した場合、x にその配列が束縛され、r に空配列が束縛されることになる。空配列に固定される引数というのは意味がないので、イテレータが常に配列ひとつを yield する場合は展開されるほうがまだ有用であろう。意味がないものに比べて有用である、というのはずいぶんと弱い根拠ではあるが。展開されないほうが有用である場合があるとすれば、イテレータが引数ひとつの配列を yield することもあれば、ふたつ以上の引数で yield することもある、という引数の数が可変の場合である。

なお、{|x,*r|} で展開を行うことにより、{|x,|} は {|x,*|} と等価となる。(ついでにいえば、{|x,|} は、lambda では {|x,*|} でなく {|x|} と等価になる。)

上記の展開を行わない場合の無用さに関する議論は {|x,y|} にも適用できる。配列を展開しなければ、x にその配列が束縛され、y には nil が束縛される。nil 固定というのは意味がない。

{|x,|} についても、無用さは説明できる。配列を展開しなければ、{|x,|} は {|x|} と等価であるが、そうであるならば {|x,|} を使う意味がない。

2007-08-17 (Fri)

#1

もはや謎ではない、と言えば、多重代入もそうである。

「Rubyで一番複雑な仕様はどこだ、と問われれば筆者は即座に多重代入と答える」(*) とまでいわれた多重代入も 1.9 での挙動は難しくない。

(*) 青木峰郎, Ruby ソースコード完全解説, p.414

まず、1.9 では多重代入とブロックパラメータは別物である。以前はそうではなく、上記の発言は以前のものであり、両方を含めた話に対するものともとれる。現在は別物なので、少なくともブロックパラメータの部分の複雑さは多重代入には及ばない。

さて、多重代入は、代入の左辺および右辺にカンマとアスタリスクを使った配列の中身を記述できるものである。

一般に代入 (単純代入・多重代入) の左辺・右辺は以下の形式である。

左辺:

右辺:

これらの左辺・右辺は任意に組み合わせられるが、x = u の形を単純代入といい、それ以外を多重代入という。

また、左辺・右辺は両方ともネストできる。

右辺のネストは、u, v, w が任意の式であることから、それらに配列も書けるというだけの事である。

左辺のネストは、以前述べたように、角括弧でなく丸括弧を用いカンマの扱いが異なるという文法で行える。具体的には x とかのところに (y,z) などと配列を分解する指定を記述できる。右辺の配列と記法が揃っていないが、文法が自然でないのはパーサの都合ということで、謎ではない。(謎ではないが、望ましいという話でもないので、修正する方向に持っていける可能性もあるとは思う。)

なお、x = u で x が (y,z) の場合、つまり (y,z) = u のように、左辺が丸括弧で括られた単一の要素からなるものは、慣習的には多重代入の一種と認識されているが、ここでは説明の都合上単純代入として扱う。

代入では、右辺と左辺を対応づけるわけであるが、左辺と右辺が両方とも単純な場合、つまり x = u の形式であれば自明に x と u を対応づければ良い。(x が (y,z) とかで、u が配列でなかったときにどうするか、という話はある。これは後述する)

また、左辺と右辺が両方とも単純でない場合、つまり、x, y, *z = *u, *v, w などの場合でも悩むことはない。これは (x, y, *z) = [*u, *v, w] というように、両辺それぞれを括弧で括って解釈すれば、単純代入に帰着できる。

問題は、左辺・右辺のどちらかだけが単純でない場合である。たとえば x, y = u とか x = *u とか x, = u とかである。この場合、左右の対応が自明でないのでなんらかの細工が必要になる。

1.9 におけるここでの細工は簡単なものである。左辺・右辺それぞれについて単純でない場合は括弧で括ったものとして解釈するのである。つまり、(x, y) = u とか x = [*u] とか (x,) = u とかである。これにより両辺が単純になるので全ての代入は単純代入に帰着できることになる。

したがって、あとは配列の分解の話を定義すれば、代入の意味を定義できる。

配列の分解を配列に対して行うやりかたは明らかである。配列でないものに対して行うときは、事前にそれ自身を唯一の要素とする配列に変換してから行う。これが良い挙動であるかどうかは議論があるだろうが、謎というほどではない。

2007-08-18 (Sat)

#1

最近、chkbuild での、btest 部分で、failed to allocate memory が出たり出なかったりしていた。

<URL:http://www.rubyist.net/~akr/chkbuild/debian-sarge/ruby-trunk/log/20070818T233500.txt.gz> とか。

まず、chkbuild で、datasize と addressspace を 300M に制限してあったのを、両方 600M にしてみる。

だが、それでも足りない。

測ってみると、900M とか使うこともあるようだ。

テストしているマシンの実メモリは 1G なので、あんまり増やすのも迷惑である。

問題のテストを見てみる。

#517 test_thread.rb:172: 

   begin
     1000.times do |i|
       begin
         Thread.start(Thread.current) {|u| u.raise }
         raise
       rescue
       ensure
       end
     end
   rescue
     1000
   end
  #=> "" (expected "1000")  [ruby-dev:31371]

んー、スレッドをたくさん生成するのか。

スレッドはスタックとしてけっこうな量のメモリ (アドレス空間) を割り当てるだろうから、それが 1000個というのはでかそうである。

しかし、考えてみると、実際にはスタックはほとんど延びないわけである。とすると、addressspace は大きい必要があるが、datasize を増やす必要はないかもしれない。

とりあえず datasize を 300M に戻し、addressspace を 1G にしたら失敗しないようになった感じがする。

2007-08-22 (Wed)

#1

valgrind の massif でちょっとメモリ量を測定してみる。

#2

さらに、以下のようにして、st まわりを呼び出しているところを集計したところ、rb_ivar_set がトップに踊りでてくれやがりました。(r13128で)

% valgrind --tool=massif --format=html \
--alloc-fn=ruby_xmalloc --alloc-fn=ruby_xrealloc \
--alloc-fn=ruby_xrealloc2 --alloc-fn=ruby_xmalloc2 \
--alloc-fn=ruby_xcalloc \
--alloc-fn=st_init_table \
--alloc-fn=st_init_table_with_size \
--alloc-fn=st_add_direct \
--alloc-fn=st_insert \
./ruby ./bin/rdoc --all --ri --op .ext/rdoc .

add_heap よりも多い。(r13123 でやれば多くはならないだろうが、匹敵するものにはなるであろう)

うぅむ。とすると空を特別扱いしてもしょうがないか。

図ばかりでなく、テキストの方も読んでみると、Range で使ってるのがそれなりにある? そういえば、Range が T_OBJECT で、st を使ってインスタンス変数を管理しているのは無駄だよなぁ。どうせ 3つしかないんだし、T_STRUCT で済むだろう。

でも、やはり普通のオブジェクトの普通のインスタンス変数で消費しているようだから、インスタンス変数がひとつなら RObject に埋め込むのはどうだろうか。

2007-08-25 (Sat)

#1

POSIX の posix_spawn まわりを読む。

RATIONALE がたくさん書いてあって、なんで、posix_spawn_file_actions_t がああも操作的なのかの理由も載っていた。自分で (Ruby で) やる場合には同意できない理由であったが。

2007-08-26 (Sun)

#1

posix_spawn ぽいものを書いてみる。

require 'fcntl'
require 'io/fd'

# spawn2("cmd", "arg1", "arg2", ..., optkey1 => optval1, optkey2 => optval2, ...) #=> pid
#
# options:
# * :resetids => bool
# * :setpgroup => 0 or pid
# * :sigdefault => [signame, ...]
# * :chdir => str
# * :umask => int
# * :rlimit_foo => int, [int] or [int, int]
# * :env => hash
# * "ENV:foo" => str or nil
# * io/int => redirect-target
# * array-of-io/ints => redirect-target
#
# redirect:
#  optkey:
#  * io                  corresponding file descriptor
#  * int                 file descriptor
#  * array of io/ints    list of file descriptors
# 
#  optval (redirect-target):
#  * io                  corresponding file descriptor
#  * int                 file descriptor
#  * str                 filename (open mode (read/write/rdwr) depends on corresponding optkeys includes 1 or 2.)
#  * array               IO.sysopen arguments
#  * :close or nil       close corresponding key file descriptors

def spawn2(cmd, *args)
  opts = nil
  opts = args.pop if Hash === args.last
  redirects = {}
  resetids = false
  setpgroup = nil
  sigdefault = nil
  #sigmask = nil # Ruby cannot set sigmask.
  chdir = nil
  umask = nil
  rlimit = {}
  env = nil
  envmod = {}
  if opts
    opts.each {|k,v|
      k = [k] if Integer === k || IO === k
      if Array === k && k.all? {|elt| Integer === elt || IO === elt }
        k = k.map {|elt| IO === elt ? elt.fileno : elt }
        if Integer === v || Array === v
          # v = v
        elsif v == :close || v == nil
          v = :close
        elsif IO === v
          v = v.fileno
        elsif Array === v
          # v = v
        elsif String === v
          has_in = !(k - [1,2]).empty?
          has_out = !(k & [1,2]).empty?
          if has_in && has_out
            rw = "r+"
          elsif has_in
            rw = "r"
          elsif has_out
            rw = "w"
          else
            rw = "r"
          end
          v = [v, rw]
        else
          raise "unexpected redirect target: #{v.inspect}"
        end
        redirects[k] = v
      elsif k == :resetids
        resetids = v
      elsif k == :setpgroup
        setpgroup = v
      elsif k == :sigdefault
        sigdefault = v
      elsif k == :chdir
        chdir = v
      elsif k == :umask
        umask = v
      elsif Symbol === k && /\Arlimit_/ =~ k.to_s && Process.const_defined?(k.to_s.upcase)
        rlimit[Process.const_get(k.to_s.upcase)] = v
      elsif k == :env
        env = v
      elsif String === k && /\AENV:/ =~ k.to_s
        envmod[$'] = v
      else
        raise "unexpected option: #{k.inspect}"
      end
    }
  end

  specified_fd = {}
  redirects.each_key {|fds|
    fds.each {|fd|
      if specified_fd[fd]
        raise ArgumentError, "fd specified twice: #{fd}"
      end
      specified_fd[fd] = true
    }
  }

  errpipe_r, errpipe_w = IO.pipe
  errpipe_w.fcntl(Fcntl::F_SETFD, errpipe_w.fcntl(Fcntl::F_GETFD)|Fcntl::FD_CLOEXEC)

  pid = fork {
    begin
      errpipe_r.close
      if resetids
        Process.egid = Process.gid
        Process.euid = Process.uid
      end
      sigdefault.each {|s| Signal.trap(s, "SYSTEM_DEFAULT") } if sigdefault
      Process.setpgid($$, setpgroup) if setpgroup
      rlimit.each {|k, v| Process.setrlimit(k, *v) }
      ENV.replace env if env
      envmod.each {|k, v| if v then ENV[k] = v else ENV.delete k end }
      Dir.chdir(chdir) if chdir
      File.umask(umask) if umask
      extra_fd = nil
      until redirects.empty?
        nil while redirects.reject! {|fds,target|
                    if (fds & redirects.values).empty?
                      target_fd, need_close = redirect_target(target)
                      included = false
                      fds.each {|fd|
                        if target_fd != fd
                          dup2c(target_fd, fd)
                        else
                          included = true
                        end
                      }
                      IO::FD.close(target_fd) if need_close && !included
                      true
                    else
                      false
                    end
                  }
        if !redirects.empty?
          IO::FD.close(extra_fd) if extra_fd
          some_fds, some_target_fd = redirects.find {|fds, target| Integer === target }
          some_target_fd2 = IO::FD.dup(some_target_fd).to_i
          extra_fd = some_target_fd2
          redirects.keys.each {|fds|
            redirects[fds] = some_target_fd2 if redirects[fds] == some_target_fd
          }
        end
      end
      IO::FD.close(extra_fd) if extra_fd
      exec cmd, *args # xxx: invoked via shell if args is empty.
    ensure
      Marshal.dump($!, errpipe_w) if $!
      exit! 127
    end
  }
  errpipe_w.close
  err = errpipe_r.read
  if !err.empty?
    raise Marshal.load(err)
  end

  return pid
end

# pipeline(cmdline1, cmdline2, ...) => [pid1, pid2, ...]
#
# cmdline is an array used for spawn2 arguments. 
#
def pipeline(*commands)
  if commands.length == 1
    pid = spawn2(*commands[0])
    [pid]
  else
    r1 = nil
    pids = []
    commands.each_with_index {|c, i|
      h = Hash === c.last ? h = c.pop.dup : {}
      c += [h]
      if i == 0
        r1, w1 = IO.pipe
        h.update({STDOUT=>w1, r1=>:close, w1=>:close})
        pids << spawn2(*c)
        w1.close
      elsif i < commands.length-1
        r2, w2 = IO.pipe
        h.update({STDIN=>r1, STDOUT=>w2, r1=>:close, r2=>:close, w2=>:close})
        pids << spawn2(*c)
        r1.close
        w2.close
        r1 = r2
      else
        h.update({STDIN=>r1, r1=>:close})
        pids << spawn2(*c)
        r1.close
      end
    }
    pids
  end
end

def dup2c(oldfd, newfd)
  newfd = newfd.fileno if IO === newfd
  if oldfd == :close
    IO::FD.close(newfd)
  else
    oldfd = oldfd.fileno if IO === oldfd
    IO::FD.dup2(oldfd, newfd)
  end
end

def redirect_target(target)
  if Integer === target
    return target, false
  elsif target == :close
    return :close, false
  elsif Array === target
    fd = IO.sysopen(*target)
    return fd, true
  else
    raise "unexpected redirect target: #{target.inspect}"
  end
end

posix_spawn に似ているが、sigmask は Ruby ではいじれないので省き、chdir, umask, rlimit を足してある。

そして、fd は、posix_spawn とは違って、子プロセスの fd と外界との対応を宣言するだけで、操作的ではない。(posix_spawn は dup2, close などをどういう順序でやるかを指定する)

POSIX でも最初の案は子プロセスと親プロセスの fd の対応を指定するとその通りになる、というものだったそうなので、それに近いかもしれない。

POSIX でそれをあきらめたのは、fd を限界まで使っているとできないことがある、というのと、実装に複雑な戦略があるという理由だそうである。

余計な fd が必要というのはその通りで、上記のコードでの extra_fd がそれにあたる。fd の対応にサイクルがあると、それを処理するためにはいったん他の fd に dup しないといけない。たとえば、spawn2(..., STDOUT=>STDERR, STDERR=>STDOUT) とか。まぁ、swap にテンポラリ変数を使うのと同じ理由である。

しかし、どうせ例外を呼び出し側に戻すためにパイプを使うので、余計な fd は必要になる。毒を食らわば皿まで、というわけで、もうひとつ余計な fd を使ってもいけなくはあるまい。(posix_spawn ではパイプの使用は考えられていなくて、exit status での情報の受渡しの制限がいろいろと書いてある)

複雑な戦略、というのは、まぁ、上記の実装程度の話である。たしかに fd をいじくる順序をそれなりに考えないといけないが、一回実装すればいいんだしそれ以上の話ではない。POSIX の立場だと、一回実装すればいい、とはいかないからこの件の重みはちょっと違うかも?

2007-08-27 (Mon)

#1

プログラムを中断する、というのはなかなか難しい。

穏やかに終了するには後始末が必要だからである。

誰かがプログラムの中断を望み、プログラムに要請するのは勝手である。(適切な権限があるのであれば)

しかし、要請されたタイミングがプログラムの終了に都合がいいとは限らない。

たとえば、Web サーバの graceful shutdown は、処理中のリクエストがぜんぶ終わってから終了するというものである。graceful shutdown を要請されたときに処理中のリクエストがなければいいが、あったらしばらく終了を遅延しなければならない。

もちろん、Web サーバに graceful でない shutdown を要請することも考えられる。処理中のリクエストを放り出して終了するというのもありうる話である。ただ、そのときにも、そういうことをしたというログくらいは書いてほしいとしよう。

こういういろいろな中断をするために、プログラムにそういう要請を伝える機構には例えば signal がある。kill system call で要請を伝え、signal handler で要請を検出する。

ここで、要請を伝えるほうはいいのだが、要請を検出するのは難しい。

C の signal handler は、基本的には、プログラムがどういう状態であろうが即座に起動する。ユーザレベルの機械語で、任意の命令と命令のあいだで起動する可能性がある。OS からみれば、要請は可能な限り早く伝えたのだから後はプログラム側の責任で勝手にやってくれ、というわけである。しかし、malloc の処理中に動くなど、プログラムの一貫性が保たれていない可能性があるため、勝手にやれといわれても安全にできることはほとんどない。

昔の Perl は C の signal handler の中で Perl レベルで設定した signal handler を起動していた。そのため、安全にできることはほとんどなく、その状況で Perl インタプリタを動かすというのはかなり無謀である。これは安全ではない。

signal handler の中でインタプリタを動かすのは剣呑なので、Python などでは signal handler の中では flag を設定するだけにしておいて、インタプリタの命令間で flag を検査してそのタイミングで処理を行う。つまり、インタプリタが安全になるところまで処理を遅延するわけである。Perl 5.7.3 以降や、Ruby や Gauche も同様な仕掛けである。なお、ブロックする操作の周辺も含めて遅延しすぎないようにこれをやるのは大変である。完璧にはやってはいないかもしれない。

これはインタプリタが壊れないという点では安全性を保証できるが、それ以上ではない。つまりインタプリタは、その上で動くプログラムに対して要請は可能な限り早く伝えたのだから後はプログラム側の責任で勝手にやってくれ、という立場で、先ほどの OS の立場をインタプリタがとっているわけである。

しかしこれだと、プログラムは C で記述するプログラムと同じ苦労ををしなければならない。インタプリタでの signal の遅延を実装するのに、かなり苦労しているというのに、それと同じ苦労をしなければならない。しかも、signal mask の類などの必要な仕掛けが足りず、実現不能な場合もある。

どうせ安全なところまで遅延しなければならないのなら、もっと遅延しようと考えたものもある。

ではどこまで遅延するかが問題となるが、ブロックする操作まで遅延する、という考え方がある。これは POSIX (signal を 全部 mask しておいて pselect, sigwaitinfo, sigtimedwait だけで signal を受ける)、FreeBSD (signal handler を全部 SIG_IGN にしておいて kevent だけで signal を受ける)、sigsafe、pthread の Thread Cancellation (signal じゃないが、cancellation point というのはブロックするところである)、Java (signal じゃないが、java.lang.Thread#interrupt) あたりで実現できる。

この考え方には、中断する意図を伝えたのにプログラムが依然としてブロックして止まっているというのは我慢できない、という点と、ブロックする操作のあたりは race condition が非常に起きやすくて支援が必要という理由がある。

しかし、この理由はタイミングが安全であることを保証するわけではなく、ブロックする操作がすべて中断するのに安全なタイミングかというとそうとは限らない。

安全でないタイミングのブロックする操作で遅延された signal が検出されても、結局、アプリケーション側でさらに遅延することになる。

とすると、結局、アプリケーション側で遅延の範囲をいかに記述するか、というのが問題であろう。どうせシステム側には遅延機構が必要なのだから、それをもうちょっとアプリケーションの都合にあわせて制御できればかなり幸せになるのではないだろうか。

#2

Web サーバの graceful shutdown と graceful でない shutdown の例を考えると、「安全なタイミング」というのは中断の意図によって異なるので、中断の意図それぞれについて遅延の範囲を記述できる必要がある。


[latest]


田中哲