Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions lib/bitclust/app.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'bitclust'
require 'bitclust/interface'
require 'bitclust/reloadable_request_handler'

module BitClust

Expand Down Expand Up @@ -28,10 +29,6 @@ def initialize(options)
when String
# @type var viewpath: String
dbpath = File.expand_path(dbpath)
db = BitClust::MethodDatabase.new(dbpath)
if capi
db = [db, BitClust::FunctionDatabase.new(dbpath)] #: [MethodDatabase, FunctionDatabase]
end
manager = BitClust::ScreenManager.new(
:base_url => baseurl,
:cgi_url => File.join(baseurl, viewpath),
Expand All @@ -40,7 +37,7 @@ def initialize(options)
:theme => options[:theme],
:encoding => encoding
)
handler = request_handler_class.new(db, manager)
handler = BitClust::ReloadableRequestHandler.new(dbpath, capi, manager, request_handler_class)
@interfaces[viewpath] = BitClust::Interface.new { handler }
when Array
# @type var viewpath: String?
Expand All @@ -56,10 +53,6 @@ def initialize(options)
else
version_viewpath = version
end
db = BitClust::MethodDatabase.new(dbpath)
if capi
db = [db, BitClust::FunctionDatabase.new(dbpath)] #: [MethodDatabase, FunctionDatabase]
end
manager = BitClust::ScreenManager.new(
:base_url => baseurl,
:cgi_url => File.join(baseurl, version_viewpath),
Expand All @@ -68,7 +61,7 @@ def initialize(options)
:theme => options[:theme],
:encoding => encoding
)
handler = request_handler_class.new(db, manager)
handler = BitClust::ReloadableRequestHandler.new(dbpath, capi, manager, request_handler_class)
@interfaces[version_viewpath] = BitClust::Interface.new { handler }
$bitclust_context_cache = nil # clear cache
end
Expand Down
20 changes: 19 additions & 1 deletion lib/bitclust/database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,29 @@ def transaction
save_properties 'properties', (@properties || raise)
@properties_dirty = false
end
commit if dirty?
if dirty?
commit
# 更新完了の合図として properties の mtime を進める(bitclust#275)。
# server の自動リロード(ReloadableRequestHandler)は properties の
# mtime で新鮮さを判定するため、init 直後〜update 完了前の途中状態を
# 読んでしまったプロセスも、ここで mtime が進むことで次のリクエスト
# から完成した DB を読み直して収束できる
touch_properties
end
ensure
@in_transaction = false
end

# properties の内容は変えず mtime だけ現在時刻に進める。
# ファイルが無い場合(異常系)は何もしない
def touch_properties
path = realpath('properties')
now = Time.now
File.utime(now, now, path)
rescue Errno::ENOENT
end
private :touch_properties

# abstract dirty?
# abstract clear_dirty
# abstract commit
Expand Down
4 changes: 1 addition & 3 deletions lib/bitclust/interface.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ def main

# for rack
def call(env)
# @type var handler: BitClust::RackRequestHandler
handler = _ = @handler
handler.handle(Rack::Request.new(env)).rack_finish
@handler.handle(Rack::Request.new(env)).rack_finish
end

private
Expand Down
72 changes: 72 additions & 0 deletions lib/bitclust/reloadable_request_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true
#
# bitclust/reloadable_request_handler.rb
#

require 'bitclust/methoddatabase'
require 'bitclust/functiondatabase'
require 'bitclust/requesthandler'

module BitClust

# `bitclust server` を再起動しなくても `rake generate` で再生成された DB を
# 読み直せるようにするラッパー(bitclust#275)。
#
# MethodDatabase/FunctionDatabase はエントリを一度読むとメモリ上にメモ化
# し続けるため、App#initialize で一度だけ生成した RequestHandler をずっと
# 使い回す従来の作りでは、DB をディスク上で再生成してもプロセスは古い内容
# を返し続けてしまう。
#
# 新鮮さの判定は DB ディレクトリ直下の "properties" ファイルの mtime で行う。
# `rake generate` は対象を rm_rf してから MethodDatabase#init(properties を
# touch) → update という手順を踏むため、再生成のたびに properties は必ず
# 書き直される。#handle は毎リクエストその mtime を stat するだけなので
# コストは無視できる。
#
# 再生成の途中(rm_rf 直後〜init 前)は properties が一時的に存在しない。
# その間は新鮮さを判定できないので、古い RequestHandler をそのまま使い
# 続けてリクエストを失敗させない。次のリクエストで改めて判定する。
class ReloadableRequestHandler

def initialize(dbpath, capi, manager, request_handler_class)
@dbpath = dbpath
@capi = capi
@manager = manager
@request_handler_class = request_handler_class
@properties_mtime = properties_mtime
@handler = build_handler
end

def handle(req)
reload_if_stale
@handler.handle(req)
end

private

def reload_if_stale
mtime = properties_mtime
return unless mtime # 再生成中で properties が一時的に無い: 判定を諦めて継続
return if @properties_mtime == mtime

@properties_mtime = mtime
@handler = build_handler
$bitclust_context_cache = nil # clear cache
end

def properties_mtime
File.mtime("#{@dbpath}/properties")
rescue Errno::ENOENT
nil
end

def build_handler
db = BitClust::MethodDatabase.new(@dbpath)
if @capi
db = [db, BitClust::FunctionDatabase.new(@dbpath)] #: [MethodDatabase, FunctionDatabase]
end
@request_handler_class.new(db, @manager)
end

end
end
2 changes: 2 additions & 0 deletions sig/bitclust/database.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ module BitClust

private

def touch_properties: () -> void

def realpath: (::String rel) -> ::String
end
end
16 changes: 8 additions & 8 deletions sig/bitclust/interface.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ module BitClust
type webrick_conf = {}
@webrick_conf: webrick_conf

@handler: BitClust::RequestHandler
@handler: BitClust::ReloadableRequestHandler

$bitclust_context_cache: ::BitClust::RequestHandler?
$bitclust_context_cache: ::BitClust::ReloadableRequestHandler?

def initialize: (?webrick_conf webrick_conf) { () -> BitClust::RequestHandler } -> void
def initialize: (?webrick_conf webrick_conf) { () -> BitClust::ReloadableRequestHandler } -> void

# for WEBrick servlet
def get_instance: (WEBrick::HTTPServer server) -> WEBrickServlet
Expand All @@ -25,23 +25,23 @@ module BitClust
def mod_ruby?: () -> false

class CGI < ::WEBrick::CGI
@handler: BitClust::RequestHandler
@handler: BitClust::ReloadableRequestHandler

def main: (BitClust::RequestHandler handler) -> void
def main: (BitClust::ReloadableRequestHandler handler) -> void

def do_GET: (WEBrick::HTTPRequest wreq, WEBrick::HTTPResponse wres) -> void

alias do_POST do_GET
end

class FCGI < CGI
@handler: BitClust::RequestHandler
@handler: BitClust::ReloadableRequestHandler

def main: (BitClust::RequestHandler handler) -> void
def main: (BitClust::ReloadableRequestHandler handler) -> void
end

class WEBrickServlet < ::WEBrick::HTTPServlet::AbstractServlet
@options: [BitClust::RequestHandler]
@options: [BitClust::ReloadableRequestHandler]

def do_GET: (WEBrick::HTTPRequest wreq, WEBrick::HTTPResponse wres) -> void

Expand Down
46 changes: 46 additions & 0 deletions sig/bitclust/reloadable_request_handler.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module BitClust
# `bitclust server` を再起動しなくても `rake generate` で再生成された DB を
# 読み直せるようにするラッパー(bitclust#275)。
#
# MethodDatabase/FunctionDatabase はエントリを一度読むとメモリ上にメモ化
# し続けるため、App#initialize で一度だけ生成した RequestHandler をずっと
# 使い回す従来の作りでは、DB をディスク上で再生成してもプロセスは古い内容
# を返し続けてしまう。
#
# 新鮮さの判定は DB ディレクトリ直下の "properties" ファイルの mtime で行う。
# `rake generate` は対象を rm_rf してから MethodDatabase#init(properties を
# touch) → update という手順を踏むため、再生成のたびに properties は必ず
# 書き直される。#handle は毎リクエストその mtime を stat するだけなので
# コストは無視できる。
#
# 再生成の途中(rm_rf 直後〜init 前)は properties が一時的に存在しない。
# その間は新鮮さを判定できないので、古い RequestHandler をそのまま使い
# 続けてリクエストを失敗させない。次のリクエストで改めて判定する。
class ReloadableRequestHandler
type request_handler_class = singleton(RequestHandler) | singleton(RackRequestHandler)

@dbpath: String

@capi: bool

@manager: ScreenManager

@request_handler_class: request_handler_class

@properties_mtime: Time?

@handler: RequestHandler | RackRequestHandler

def initialize: (String dbpath, bool capi, ScreenManager manager, request_handler_class request_handler_class) -> void

def handle: (untyped req) -> Response

private

def reload_if_stale: () -> void

def properties_mtime: () -> Time?

def build_handler: () -> (RequestHandler | RackRequestHandler)
end
end
Loading
Loading