简体   繁体   English

Worker 劫持 Active Record 模型

[英]Worker hijacking Active Record model

I am a ruby-junior.我是一个红宝石初级。 My app allows a user to enter Contacts and/or upload a CSV file.我的应用程序允许用户输入联系人和/或上传 CSV 文件。

  • Running on my local branch, if I add a Contact - the Contact gets added from View, Controller & dBase and there it works great.在我的本地分支上运行,如果我添加一个联系人 - 联系人是从视图、控制器和 dBase 添加的,它在那里工作得很好。
  • If I then allow the user to Import CSV file.如果我允许用​​户导入 CSV 文件。 It starts to import the file.它开始导入文件。 However, the User is now unable to Add a Contact via the App.但是,用户现在无法通过应用程序添加联系人。 It essentially hangs until the CSV import is completed.它基本上会挂起,直到 CSV 导入完成。

I am using the following versions:我正在使用以下版本:

  ruby "2.3.0"
  gem "rails", "4.2.5.1" gem "pg", "0.17.1" # postgresql database 
  gem "delayed_job_active_record", ">= 4.0.0.beta1" # background job
  processing gem "delayed_job_web", ">= 1.2.0" # web interface for delayed job

Also using:还使用:

> class CsvUploader < CarrierWave::Uploader::Base

def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end

Here is the worker:这是工人:

class ImportCsvFileWorker类 ImportCsvFileWorker

def self.perform(csv_file_id) csv_file = CsvFile.find(csv_file_id) def self.perform(csv_file_id) csv_file = CsvFile.find(csv_file_id)

 csv_file.import! csv_file.send_report! end

end结尾

I am using smarecsv parsing service我正在使用 smarecsv 解析服务

def process_csv parser = ::ImportData::SmartCsvParser.new(csv_file.file_url) def process_csv 解析器 = ::ImportData::SmartCsvParser.new(csv_file.file_url)

 parser.each do |smart_row| csv_file.increment!(:total_parsed_records) begin self.contact = process_row(smart_row) rescue => e row_parse_error(smart_row, e) end end rescue => e # parser error or unexpected error csv_file.save_import_error(e) end

Does delayed_job lock the dbase for the User/Contact so i can't add any Contacts via the App? delay_job 是否锁定了用户/联系人的数据库,因此我无法通过应用程序添加任何联系人?

Locally, the app is frozen/hanging or seems locked until background delayed_job is completed (BTW if i run on Heroku, it causes H12 errors but figure I need to fix the issue locally first).在本地,应用程序被冻结/挂起或似乎被锁定,直到后台延迟作业完成(顺便说一句,如果我在 Heroku 上运行,它会导致 H12 错误,但我需要先在本地解决问题)。 Just trying to understand - what is causing it to be locked?只是想了解 - 是什么导致它被锁定? Should it be doing this?应该这样做吗? Is it code (the business logic of the CSV file and the View of Adding a Contact both work independent)?它是代码(CSV 文件的业务逻辑和添加联系人的视图都独立工作)吗? But the App side will not work if there is a background job running or is it the way Active Record handles it.但是,如果有后台作业正在运行或 Active Record 处理它的方式,则 App 端将无法工作。 Is there a way around this?有没有解决的办法?

I have not isolated it but am suspicious that if any background job is running, the app becomes unavailable.我没有隔离它,但我怀疑如果任何后台作业正在运行,该应用程序将变得不可用。

I have tried to include all the relevant facts - let me know if any further details needed.我已尝试包括所有相关事实 - 如果需要任何进一步的细节,请告诉我。 many thanks for help.非常感谢您的帮助。

UPDATE - i discovered i have a ContactMergingService that seems to locking all the contacts.更新 - 我发现我有一个 ContactMergingService 似乎锁定了所有联系人。 If i comment out this service below, my application does not hang.如果我在下面注释掉这个服务,我的应用程序就不会挂起。

So my question is what are other options = Before adding a Contact, what I am trying to do is find all existing same email address (if I find it, I append contact details).所以我的问题是还有哪些其他选项 = 在添加联系人之前,我想要做的是找到所有现有的相同电子邮件地址(如果我找到了,我会附加联系方式)。 how do i do this without locking dbase?如何在不锁定 dbase 的情况下执行此操作?

is it because I am using 'find' method?是因为我使用的是“查找”方法吗? is there a better way?有没有更好的办法?

> class ContactMergingService
> 
>   attr_reader :new_contact, :user
> 
>   def initialize(user, new_contact, _existing_records)
>     @user = user
>     @new_contact = new_contact
>     @found_records = matching_emails_and_phone_numbers   
>   end
> 
>   def perform
>     Rails.logger.info "[CSV.merging] Checking if new contact matches existing contact..."
>     if (existing_contact = existing_contact())
>       Rails.logger.info "[CSV.merging] Contact match found."
>       merge(existing_contact, new_contact)
>       existing_contact
>     else
>       Rails.logger.info "[CSV.merging] No contact match found."
>       new_contact
>     end   end
> 
>   private
> 
>   def existing_contact
>     Rails.logger.info "[CSV.merging] Found records: #{@found_records.inspect}"
>     if @found_records.present?
>       @user.contacts.find @found_records.first.owner_id # Fetch first owner
>     end   end
> 
>   def merge(existing_contact, new_contact)
>     Rails.logger.info "[CSV.merging] Merging with existing contact (ID: #{existing_contact.id})..."
>     merge_records(existing_contact, new_contact)   end
> 
>   def merge_records(existing_relation, new_relation)
>     existing_relation.attributes do |field, value|
>       if value.blank? && new_relation[field].present?
>         existing_relation[field] = new_relation[field]
>       end
>     end
>     new_relation.email_addresses.each do |email_address|
>       Rails.logger.info "[CSV.merging.emails] Email: #{email_address.inspect}"
>       if existing_relation.email_addresses.find_by(email: email_address.email)
>         Rails.logger.info "[CSV.merging.emails] Email address exists."
>       else
>         Rails.logger.info "[CSV.merging.emails] Email does not already exist. Saving..."
>         email_address.owner = existing_relation
>         email_address.save!
>       end
>     end
>     new_relation.phone_numbers.each do |phone_number|
>       Rails.logger.info "[CSV.merging.phone] Phone Number: #{phone_number.inspect}"
>       if existing_relation.phone_numbers.find_by(number: phone_number.number)
>         Rails.logger.info "[CSV.merging.phone] Phone number exists."
>       else
>         Rails.logger.info "[CSV.merging.phone] Phone Number does not already exist. Saving..."
>         phone_number.owner = existing_relation
>         phone_number.save!
>       end
>     end   end
> 
>   def matching_emails_and_phone_numbers
>     records = []
>     if @user
>       records << matching_emails
>       records << matching_phone_numbers
>       Rails.logger.info "[CSV.merging] merged records: #{records.inspect}"
>       records.flatten!
>       Rails.logger.info "[CSV.merging] flattened records: #{records.inspect}"
>       records.compact!
>       Rails.logger.info "[CSV.merging] compacted records: #{records.inspect}"
>     end
>     records   end
> 
>   def matching_emails
>     existing_emails = []
>     new_contact_emails = @new_contact.email_addresses
>     Rails.logger.info "[CSV.merging] new_contact_emails: #{new_contact_emails.inspect}"
>     new_contact_emails.each do |email|
>       Rails.logger.info "[CSV.merging] Checking for a match on email: #{email.inspect}..."
>       if existing_email = @user.contact_email_addresses.find_by(email: email.email, primary: email.primary)
>         Rails.logger.info "[CSV.merging] Found a matching email"
>         existing_emails << existing_email
>       else
>         Rails.logger.info "[CSV.merging] No match found"
>         false
>       end
>     end
>     existing_emails   end
> 
>   def matching_phone_numbers
>     existing_phone_numbers = []
>     @new_contact.phone_numbers.each do |phone_number|
>       Rails.logger.info "[CSV.merging] Checking for a match on phone_number: #{phone_number.inspect}..."
>       if existing_phone_number = @user.contact_phone_numbers.find_by(number: phone_number.number)
>         Rails.logger.info "[CSV.merging] Found a matching phone number"
>         existing_phone_numbers << existing_phone_number
>       else
>         Rails.logger.info "[CSV.merging] No match found"
>         false
>       end
>     end
>     existing_phone_numbers   end
> 
>   def clean_phone_number(number)
>     number.gsub(/[\s\-\(\)]+/, "")   end
> 
> end

You can try something like:您可以尝试以下操作:

 Thread.new do
    ActiveRecord::Base.transaction do   
      User.import(user_data)
    end
    ActiveRecord::Base.connection.close
 end

In your CVS importing code.在您的 CVS 导入代码中。

We have concluded that the cause of the issue was when CsvParsingService#perform runs, it puts AccessShareLocks on certain tables in the database (we think Contacts, EmailAddresses, PhoneNumbers, and maybe Users).我们得出的结论是,问题的原因是当 CsvParsingService#perform 运行时,它将 AccessShareLocks 放在数据库中的某些表上(我们认为是联系人、电子邮件地址、电话号码,也许还有用户)。

The locks persist until the method finishes.锁会一直持续到方法完成。 Any other request that tried to access one of these locked tables will just sit and wait until the database becomes unlocked.试图访问这些锁定表之一的任何其他请求将只是等待,直到数据库解锁。 Because the method parses each row of a given uploaded csv_file, it takes as long as 90 minutes to run.由于该方法解析给定上传的 csv_file 的每一行,因此运行时间长达 90 分钟。

Any request to the app that tries to access one of these locked tables will stop and wait until the tables are unlocked.对尝试访问这些锁定表之一的应用程序的任何请求都将停止并等待,直到这些表被解锁。 Because Heroku will cut off a request after 30 seconds, this is what was generating the H12 errors (on the application side).因为 Heroku 会在 30 秒后切断请求,这就是产生 H12 错误的原因(在应用程序端)。

The cause of the issue was that the gem state-machine_active record wraps every state transition inside a transaction by default.问题的原因是 gem state-machine_active 记录默认将每个状态转换包装在事务中。

The worker was calling the parsing service by running csv_file.import!, which triggered a transition on the csv_file state machine, which then called CsvParsingService and parsed each row.工作人员通过运行 csv_file.import! 调用解析服务,这触发了 csv_file 状态机上的转换,然后调用 CsvParsingService 并解析每一行。 Since the state machine was wrapping everything inside a transaction, nothing committed until the state transition was complete.由于状态机将所有内容都包装在事务中,因此在状态转换完成之前不会提交任何内容。

By updating the gem to version 0.4.0pre, and adding the option use_transactions: false to the state machine in the CsvFile model, it no longer locks the database when calling .import!通过将 gem 更新到 0.4.0pre 版本,并将选项 use_transactions: false 添加到 CsvFile 模型中的状态机,它在调用 .import 时不再锁定数据库! and processing.和处理。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM