使用`ActiveRecord with_connection do`& ActionController的::直播

时间:2015-07-21 09:42:22

标签: ruby-on-rails ruby multithreading connection-pooling actioncontroller

主要编辑:由于最初发现此问题,我已将其缩减至下方。我认为现在这是一个略微更精确的问题描述。因此,对OP的评论可能无法完全相关。

在rails / puma项目中发布的

编辑轻微修改后的版本:https://github.com/rails/rails/issues/21209https://github.com/puma/puma/issues/758

编辑现在使用OS X和Rainbows重现

摘要: 当使用Puma并运行长时间运行的连接时,我一直收到与跨越线程的ActiveRecord连接相关的错误。这表现在消息中,如 message type 0x## arrived from server while idle 和锁定(崩溃)的服务器。

设置:

  • Ubuntu 15 / OSX Yosemite
  • PostgreSQL(9.4)/ MySQL(mysqld 5.6.25-0ubuntu0.15.04.1
  • Ruby - MRI 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux] / Rubinius rbx-2.5.8
  • Rails(4.2.34.2.1
  • Puma(2.12.22.11
  • pg(pg-0.18.2)/ mysql2

请注意,并非所有上述版本的组合都已尝试过。首先列出的版本是我目前正在测试的版本。

  • rails new issue-test
  • 添加路线get 'events' => 'streaming#events'
  • 添加控制器streaming_controller.rb
  • 设置数据库内容(pool: 2,但看到不同的池大小)

代码:

class StreamingController < ApplicationController

  include ActionController::Live

  def events
    begin
      response.headers["Content-Type"] = "text/event-stream"
      sse = SSE.new(response.stream)
      sse.write( {:data => 'starting'} , {:event => :version_heartbeat})
      ActiveRecord::Base.connection_pool.release_connection
      while true do
        ActiveRecord::Base.connection_pool.with_connection do |conn|
          ActiveRecord::Base.connection.query_cache.clear
          logger.info 'START'
          conn.execute 'SELECT pg_sleep(3)'
          logger.info 'FINISH'
          sse.write( {:data => 'continuing'}, {:event => :version_heartbeat})
          sleep 0.5
         end
      end
    rescue IOError
    rescue ClientDisconnected
    ensure
      logger.info 'Ensuring event stream is closed'
      sse.close
    end
    render nothing: true
  end
end

Puma配置:

workers 1
threads 2, 2
#...
bind "tcp://0.0.0.0:9292"

#...
activate_control_app

on_worker_boot do
  require "active_record"
  ActiveRecord::Base.connection.disconnect! rescue ActiveRecord::ConnectionNotEstablished
  ActiveRecord::Base.establish_connection(YAML.load_file("#{app_dir}/config/database.yml")[rails_env])
end
  • 运行服务器puma -e production -C path/to/puma/config/production.rb

测试脚本:

#!/bin/bash

timeout 30 curl -vS http://0.0.0.0/events &
timeout 5 curl -vS http://0.0.0.0/events &
timeout 30 curl -vS http://0.0.0.0/events

这合理地始终导致应用程序服务器的完全锁定(在PostgreSQL中,请参阅注释)。可怕的消息来自libpq

message type 0x44 arrived from server while idle
message type 0x43 arrived from server while idle
message type 0x5a arrived from server while idle
message type 0x54 arrived from server while idle

在现实世界中&#39;我有很多额外的元素,这个问题随机出现。我的研究表明此消息来自libpq,并且是&#39;通信问题的潜台词,可能使用不同线程中的连接&#39; 。最后,在编写本文时,我将服务器锁定在任何日志中都没有单个消息。

所以,问题是:

  1. 我所遵循的模式是否在某种程度上不合法?我错了什么[sed |理解]?
  2. 什么是&#39;标准&#39;在这里使用数据库连接应该避免这些问题?
  3. 你能看到一种可靠再现的方法吗?
    1. 这里的根本问题是什么?如何解决?
    2. 的MySQL

      如果运行MySQL,消息会有所不同,应用程序会恢复(尽管我不确定它是否处于某种未定义状态):

      F, [2015-07-30T14:12:07.078215 #15606] FATAL -- : 
      ActiveRecord::StatementInvalid (Mysql2::Error: This connection is in use by: #<Thread:0x007f563b2faa88@/home/dev/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/actionpack-4.2.3/lib/action_controller/metal/live.rb:269 sleep>: SELECT  `tasks`.* FROM `tasks`  ORDER BY `tasks`.`id` ASC LIMIT 1):
      

1 个答案:

答案 0 :(得分:1)

警告:将“回答”视为“似乎有所作为”

如果我将控制器块更改为:

,我不会发现问题
//#define OLDSCHOOL_INVOKE

using System;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace AsyncTests
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }


        private async void LongTermOp()
        {
            int delay;
            int thisId;

            lock (mtx1)
            {
                delay  = rnd.Next(2000, 10000);
                thisId = firstCount++;
#if OLDSCHOOL_INVOKE
                Invoke(new Action(() =>
#endif
                label1Gen.Text = $"Generating first run delay #{thisId} of {delay} ms"
#if OLDSCHOOL_INVOKE
                ))
#endif
                ;
                ++firstPending;
            }

            await Task.Delay(delay);

            lock (mtx1)
            {
                --firstPending;
#if OLDSCHOOL_INVOKE
                Invoke(new Action(() =>
#endif
                label1Gen.Text = $"First run #{thisId} completed, {firstPending} pending..."
#if OLDSCHOOL_INVOKE
                ))
#endif
                ;
            }
        }


        private async Task LongTermOpAsync()
        {
            await Task.Run((Action)LongTermOp);
        }

        private readonly Random rnd  = new Random();
        private readonly object mtx1 = new object();
        private readonly object mtx2 = new object();
        private int firstCount;
        private int firstPending;
        private int secondCount;
        private int secondPending;

        private async void buttonRound1_Click(object sender, EventArgs e)
        {
            await LongTermOpAsync();
        }

        private async void buttonRound2_Click(object sender, EventArgs e)
        {
            await Task.Run(async () => 
            {
                int delay;
                int thisId;

                lock (mtx2)
                {
                    delay = rnd.Next(2000, 10000); 
                    thisId = secondCount++;
#if OLDSCHOOL_INVOKE
                    Invoke(new Action(() =>
#endif
                    label2Gen.Text = $"Generating second run delay #{thisId} of {delay} ms"
#if OLDSCHOOL_INVOKE
                    ))
#endif
                    ;
                    ++secondPending;
                }
                await Task.Delay(delay);
                lock (mtx2)
                {
                    --secondPending;
#if OLDSCHOOL_INVOKE
                    Invoke(new Action(() =>
#endif
                    label2Gen.Text = $"Second run #{thisId} completed, {secondPending} pending..."
#if OLDSCHOOL_INVOKE
                    ))
#endif
                    ;
                }
            });            
        }

        private void buttonRound12_Click(object sender, EventArgs e)
        {
            buttonRound1_Click(sender, e);
            buttonRound2_Click(sender, e);
        }


        private bool isRunning = false;

        private async void buttonCycle_Click(object sender, EventArgs e)
        {
            isRunning = !isRunning;

            await Task.Run(() =>
            {
                while (isRunning)
                {
                    buttonRound12_Click(sender, e);
                    Application.DoEvents();
                }
            });
        }
    }
}

但我不知道这是否真的解决了这个问题,或者只是让它变得极不可能。我也无法理解为什么会有所作为。

将此作为解决方案发布,以防万一,但仍在挖掘问题。