※当サイトの記事には、広告・プロモーションが含まれます。

Oxidizedの環境変数CONFIG_RELOAD_INTERVAL有効時のリロードで処理の不整合が起こる時があるかも

gigazine.net

毎年12月24日の夜には、「子どもたちのもとにサンタクロースが訪れてプレゼントを贈る」といわれています。「サンタクロースが世界中の子どもにプレゼントを配るにはどうすればいいのか」の量子論的解釈を、科学系ニュースサイトのPhys.orgが紹介しています。

サンタクロースを量子論的に解釈する、世界中に爆速でプレゼントを配るサンタが燃え尽きて消滅しないためには? - GIGAZINE

しかし、サンタクロースが実在したとして、世界中の子どもに一晩でプレゼントを配るのは至難の業。

サンタクロースを量子論的に解釈する、世界中に爆速でプレゼントを配るサンタが燃え尽きて消滅しないためには? - GIGAZINE

ドイツ・ドルトムント大学の物理学研究者であるメティン・トラン氏は自著「Silent Night, Hasty Night.(きよしこの夜、はやい夜)」の中で、「サンタクロースが、仮に『その1年でいい子だった10歳までのキリスト教徒の子どもたち』だけを対象にプレゼントを配達したとしても、24時間で2億3400万世帯を訪問する必要がある」と論じています。

サンタクロースを量子論的に解釈する、世界中に爆速でプレゼントを配るサンタが燃え尽きて消滅しないためには? - GIGAZINE

24時間で2億3400万世帯ということは、サンタクロースは1秒あたり約2700件の家を訪問しなければならないことを意味します。仮に地球の自転に逆らって移動したとしても、制限時間は合計32時間で、配達ノルマは1秒あたり約2000件。

サンタクロースを量子論的に解釈する、世界中に爆速でプレゼントを配るサンタが燃え尽きて消滅しないためには? - GIGAZINE

これは人間が見ることができる最大フレームレートをはるかに越えているため、「だからこそ我々は、サンタクロースが働いている姿を観察することができない」とトラン氏は述べています。

サンタクロースを量子論的に解釈する、世界中に爆速でプレゼントを配るサンタが燃え尽きて消滅しないためには? - GIGAZINE

トラン氏は「サンタクロースは空間全体に存在し、すべての状態を記述する波動状態に置き換えられる必要があります」と述べています。つまり、サンタクロースは1人ではなく、サンタクロースのすべての状態を重ね合わせた「サンタクロース波」を考えるというのがトラン氏のアイデアです。

サンタクロースを量子論的に解釈する、世界中に爆速でプレゼントを配るサンタが燃え尽きて消滅しないためには? - GIGAZINE

ただし、サンタクロースが目撃されれば、その時点でサンタクロースが確定してしまい、サンタクロースの波動状態は崩壊してしまいます。

サンタクロースを量子論的に解釈する、世界中に爆速でプレゼントを配るサンタが燃え尽きて消滅しないためには? - GIGAZINE

トラン氏は「このサンタクロース波がある限り、宇宙の終わりまでサンタクロースが生き延びることを示します。サンタクロースは20×1048年という永遠の時間をずっと旅をしているのです」と述べています。

サンタクロースを量子論的に解釈する、世界中に爆速でプレゼントを配るサンタが燃え尽きて消滅しないためには? - GIGAZINE

⇧ 何と言うか、「空想科学読本」を彷彿とさせますな...

まぁ、「サンタクロース波」の状態で活動できるとは思えませんが...

最早、「ホラー(Horror)」の領域という感じになって来ており、狂気が入り混じってるとしか思えないのよ...

Oxidizedの環境変数CONFIG_RELOAD_INTERVAL有効時のリロードで処理の不整合が起こる時があるかも

例の如く、「Ruby」製の「ライブラリ」である「oxidized」のバグであろうと思われる事象についての原因の推測を備忘録として。

公式の「ドキュメント」によりますと、

github.com

⇧ 上記にありますように、「CONFIG_RELOAD_INTERVAL」という「環境変数」を利用すると、「oxidized」の「設定」ファイルを再読み込みしてくれるとありますと。

なのだが、「oxidized」では「Ruby」の「Thread」を利用しているらしく、且つ、「リロード」の処理が「node」の「状態」の更新処理を考慮できていないため、タイミングによって正常に動作しないことがありますと。

残念ながら、「oxidized」の「設計」が破綻していることによる「バグ」っぽいので、おそらく、改修はされない気がする...

仕組みとしては、

github.com

■https://github.com/ytti/oxidized/blob/master/extra/auto-reload-config.runit

#!/bin/bash

if [ -z "$CONFIG_RELOAD_INTERVAL" ]; then
    # Just stop and do nothing
    sleep infinity
fi

while true; do
    sleep $CONFIG_RELOAD_INTERVAL
    echo "Reloading config..."
    curl -s http://localhost:8888/reload?format=json -O /dev/null
done    

⇧ 上記にあるように、無限ループしている中で、「CONFIG_RELOAD_INTERVAL」に指定した「秒数」のタイミングで、「oxidized-web」の「エンドポイント」に対してリクエストを実施している。

で、「oxidized-web」の該当の処理はというと、

github.com

■https://github.com/ytti/oxidized-web/blob/master/lib/oxidized/web/webapp.rb

...省略
      get '/reload.?:format?' do
        node = params[:node]
        node ? (nodes.load node) : nodes.load
        @data = node ? "reloaded #{node}" : 'reloaded list of nodes'
        out
      end

…省略

      def out(template = :text)
        if @json || (params[:format] == 'json')
          if @data.is_a?(String)
            json @data.lines
          else
            json @data
          end
        elsif (template == :text) || (params[:format] == 'text')
          content_type :text
          @data
        else
          haml template, layout: true
        end
      end

…省略

      def nodes
        settings.nodes
      end

…省略

⇧ とあり、「oxidized-web」側で「nodes」の起点はというと、

github.com

■https://github.com/ytti/oxidized-web/blob/master/lib/oxidized/web.rb

...省略

      def initialize(nodes, configuration)
        require 'oxidized/web/webapp'
        @configuration = self.class.parse_configuration(configuration)
        WebApp.set :nodes, nodes
        WebApp.set :configuration, @configuration
        WebApp.set :host_authorization, {
          permitted_hosts: @configuration[:vhosts]
        }
        uri_prefix = @configuration[:uri_prefix]
        @app = Rack::Builder.new do
          map uri_prefix do
            run WebApp
          end
        end
      end

...省略

⇧ 上記の「Web」クラスのインスタンス化の際に、引数として受け取った「nodes」になるのだが、「Web」クラスのインスタンス化は、「oxidized」側で実施されているらしく、

■https://github.com/ytti/oxidized/blob/master/lib/oxidized/core.rb

...省略

    def initialize(_args)
      Oxidized.mgr = Manager.new
      Oxidized.hooks = HookManager.from_config(Oxidized.config)
      nodes = Nodes.new
      raise NoNodesFound, 'source returns no usable nodes' if nodes.empty?

      @worker = Worker.new nodes
      @need_reload = false

      # If we receive a SIGHUP, queue a reload of the state
      reload_proc = proc do
        @need_reload = true
      end
      Signals.register_signal('HUP', reload_proc)

      # Load extensions, currently only oxidized-web
      # We have different namespaces for oxidized-web, which needs to be
      # adressed if we need a generic way to load extensions:
      # - gem: oxidized-web
      # - module: Oxidized::API
      # - path: oxidized/web
      # - entrypoint: Oxidized::API::Web.new(nodes, configuration)

      # Initialize oxidized-web if requested
      if Oxidized.config.has_key? 'rest'
        logger.warn(
          'configuration: "rest" is deprecated. Migrate to ' \
          '"extensions.oxidized-web" and remove "rest" from the configuration'
        )
        configuration = Oxidized.config.rest
      elsif Oxidized.config.extensions &&
            Oxidized.config.extensions['oxidized-web']&.load?
        # This comment stops rubocop complaining about Style/IfUnlessModifier
        configuration = Oxidized.config.extensions['oxidized-web']
      end

      if configuration
        begin
          require 'oxidized/web'
        rescue LoadError
          raise OxidizedError,
                'oxidized-web not found: install it or disable it by ' \
                'removing "rest" and "extensions.oxidized-web" from your ' \
                'configuration'
        end
        @rest = API::Web.new nodes, configuration
        @rest.run
      end
      run
    end

...省略

⇧ 上記の処理で、

  1. oxidized
  2. oxidized-web

の両方から、更新されてしまう状況になってしまいますと...

しかも、お互い「非同期」での処理になるので、変なタイミングで更新処理が実施された場合、表題の件のような処理の不整合が発生することになると思われる。

ちなみに、「node」の処理は「Thread」として管理される模様。

github.com

module Oxidized
  class Job < Thread
    include SemanticLogger::Loggable

    attr_reader :start, :end, :status, :time, :node, :config

    def initialize(node)
      @node = node
      @start = Time.now.utc
      self.name = "Job '#{@node.name}'"
      super do
        logger.debug "Starting fetching process for #{@node.name}"
        begin
          Timeout.timeout(Oxidized.config.timelimit) do
            @status, @config = @node.run
          end
          logger.debug "Config fetched for #{@node.name}"
        rescue Timeout::Error
          logger.warn "Job timelimit reached for #{@node.name}"
          @status = :timelimit
        ensure
          @end  = Time.now.utc
          @time = @end - @start
        end
      end
    end
  end
end

 

そして、「node」の「状態」は「Worker」クラスで管理されるのだが、

github.com

■https://github.com/ytti/oxidized/blob/master/lib/oxidized/worker.rb

...省略

    def work
      ended = []
      @jobs.delete_if { |job| ended << job unless job.alive? }
      ended.each      { |job| process job }
      @jobs.work

      while @jobs.size < @jobs.want
        logger.debug "Jobs running: #{@jobs.size} of #{@jobs.want} - ended: " \
                     "#{@jobs_done} of #{@nodes.size}"
        # ask for next node in queue non destructive way
        nextnode = @nodes.first
        unless nextnode.last.nil?
          # Set unobtainable value for 'last' if interval checking is disabled
          last = Oxidized.config.interval.zero? ? Time.now.utc + 10 : nextnode.last.end
          break if last + Oxidized.config.interval > Time.now.utc
        end
        # shift nodes and get the next node
        node = @nodes.get
        node.running? ? next : node.running = true

        @jobs.push Job.new node
        logger.debug "Added #{node.group}/#{node.name} to the job queue"
      end

      if cycle_finished?
        run_done_hook
        exit 0 if Oxidized.config.run_once
      end
      logger.debug("#{@jobs.size} jobs running in parallel") unless @jobs.empty?
    end

    def process(job)
      node = job.node
      node.last = job
      node.stats.add job
      @jobs.duration job.time
      node.running = false
      if job.status == :success
        process_success node, job
      else
        process_failure node, job
      end
    rescue NodeNotFound
      logger.warn "#{node.group}/#{node.name} not found, removed while collecting?"
    end

...省略

⇧ 上記の「node」の「状態」の「更新」のタイミングと「リロード」処理のタイミングがバッティングすると、「last」の判定が意図したものではなくなり、処理の整合性が崩れることになると思われますと。

ただでさえ、厄介な「ミュータブル(Mutable)」な構造になってしまっているにもかかわらず、「状態」の「更新」に影響し得るのが、

  1. oxidized
  2. oxidized-web

の2か所になり、「処理」の「排他制御」もされていないので、意図せぬ「更新」が発生し得るリスクを抱えていると...

とりあえず、

  1. oxidized
  2. oxidized-web

の「設計」の「思想」が分からないので推測するしか無いのだが、何となくだが、「メッセージキューイング(MQ:Message Queueing)」的な「機能」を導入したかったのかは謎だが、おそらく、「処理」を「疎結合」にしたかったのかもしれないが、見事に「設計」が破綻してはいそうなのよね...

まぁ、「Oxidized」の他に代替できそうな「ライブラリ」が無いので、芳しくない「設計」の「ライブラリ」と理解した上で利用せざるを得ないのだが、せめて、公式の「ドキュメント」に「情報」を載せて欲しい気はする...

今日も今日とて、ストレスが蓄積されていきますな...

毎度モヤモヤ感が半端ない…

今回はこのへんで。