Problem with large rails background process not completing - ruby-on-rails

I have a rails background process (using Sidekiq and Redis) that parses XML files and later makes modifications to it.
The background process works as intended but stays in processing and does not complete when the XML is really big. My two assumptions for this are:
My background process is storing large amounts of text from the XML into arrays old_texts & new_texts and it is causing issues
My background process is timing out
The problem occurs on both my development machine (& staging).
I'm not sure how to debug this problem. I don't think posting my code will help but I'll do it in case you need an idea of what I'm doing:
old_texts, new_texts = [], []
xml_no_includs = ["pctHeight","pctWidth","posOffset","delText","delInstrText","instrText"]
search = '//w:document//w:body//w:p'
ancestors_excluds = ['//mc:Fallback', '//w:tbl', '//wps:txbx', '//v:textbox']
old_texts, new_texts = get_texts(old_texts, new_texts, XML, xml_no_includs, ancestors_excluds, search)
search_param = '//w:document//w:body//w:p'
ancestors_excluds = ['//mc:Fallback', '//w:tbl', '//wps:txbx', '//v:textbox']
replace_texts(old_texts, new_texts, XML, search_param, ancestors_excluds)
-
def replace_texts(old_texts, new_texts, XML, search_param, ancestors_excluds)
text_params = './/text()[not(ancestor::wp14:pctHeight or ancestor::wp14:pctWidth or ancestor::wp:posOffset or ancestor::w:instrText or ancestor::w:delText or ancestor::w:delInstrText)]'
inc = 0
old_texts.each_with_index do |old_text, index|
accum_string = ''
double_break = false
XML.search(search_param).drop(inc).each do |line|
inc += 1
temp = true
line.search(text_params).each do |p|
temp2 = true
ancestors_excluds.each do |param|
temp2 = false if p.ancestors(param).present?
end
if temp2 == true
if accum_string.blank? && !p.content.blank?
accum_string += p.content
p.content = new_texts[index]
else
accum_string += p.content unless accum_string.blank?
p.content = ''
end
if accum_string.strip == old_text.strip
double_break = true
break
end
end
end
break if double_break == true
end
end
end
-
def get_texts(old_texts, new_texts, XML, xml_no_includs, ancestors_excluds, search_param)
XML.xpath(search_param).each do |p|
text = ''
temp = true
p.search('text()').each do |p2|
temp2 = true
temp2 = false if xml_no_includs.include?(p2.parent.name)
ancestors_excluds.each do |param|
temp2 = false if p2.ancestors(param).present?
end
text += p2.text if temp2 == true
end
unless text.blank?
old_texts.append(text)
new_texts.append(text.gsub(/(.)./, '\1*') )
end
end
old_texts.reject!(&:blank?)
new_texts.reject!(&:blank?)
return old_texts, new_texts
end

I ended up refactoring my code to manage each element that was originally going to be pushed into the array. Didn't speed it up more, but at least the background task completes after a few hours.

Related

Lua function constantly returning false out of SQL function

I have this code
function IsAuthorized(xPlayer, doorID, locked, usedLockpick)
local jobName, grade = {}, {}
jobName[1] = xPlayer.job.name
grade[1] = xPlayer.job.grade
if xPlayer.job2 then
jobName[2] = xPlayer.job2.name
grade[2] = xPlayer.job2.grade
end
local canOpen = false
if doorID.lockpick and usedLockpick then
count = xPlayer.getInventoryItem('lockpick').count
if count and count >= 1 then canOpen = true end
end
if not canOpen and doorID.authorizedJobs then
for job,rank in pairs(doorID.authorizedJobs) do
if (job == jobName[1] and rank <= grade[1]) or (jobName[2] and job == jobName[2] and rank <= grade[2]) then
canOpen = true
if canOpen then break end
end
end
end
if not canOpen and doorID.items then
local count
for k,v in pairs(doorID.items) do
count = xPlayer.getInventoryItem(v).count
if count and count >= 1 then
canOpen = true
local consumables = { ['ticket']=1 }
if locked and consumables[v] then
xPlayer.removeInventoryItem(v, 1)
end
break
end
end
if not count or count < 1 then canOpen = false end
end
if not canOpen then
local group = xPlayer.getGroup()
if group == 'management' or group == 'owner' then
print(group..' '..xPlayer.getName()..' was authorised to use a door')
canOpen = true
end
if doorID.authorizedgang then
print(xPlayer.identifier .. " | " .. doorID.authorizedgang)
MySQL.Async.fetchSingle('SELECT * FROM users WHERE identifier = ? AND gang = ?', {xPlayer.identifier, doorID.authorizedgang}, function(foundplayer)
if foundplayer then
print("found")
canOpen = true
print(canOpen)
end
----------------------------canopen returns true until here
end)
end
end
print(canOpen)
return canOpen
end
Essentially what is does is return a true or false value based on the if statements. I did not write this script myself, I simply added on. The code that I added to this function is
if doorID.authorizedgang then
print(xPlayer.identifier .. " | " .. doorID.authorizedgang)
MySQL.Async.fetchSingle('SELECT * FROM users WHERE identifier = ? AND gang = ?', {xPlayer.identifier, doorID.authorizedgang}, function(foundplayer)
if foundplayer then
print("found")
canOpen = true
print(canOpen)
end
----------------------------canopen returns true until here
end)
end
This code essentially makes a database call to check if it'll find a player with X identifier and Y gang, and this actually does work because it prints out "found" and canOpen returns true until the line that I marked, after this it returns false at the bottom of the function.
I've been trying other ways to get the data but ultimately I need the SQL DB call for this and im not sure why the variable isnt setting when in fact the database returns found. Any ideas what I could be doing wrong here?

how do i fix ')' (to close '(' at line 38), got '.'

for i, v in pairs(Buttons:GetChildren()) do
local NewItem = BoughtItems:FindFirstChild(v.Item.value)
if NewItem ~= nil then
Items[NewItem.Name] = NewItem:Clone()
NewItem:Destroy()
else
v.ButtonPart.Transparency = 1
v.ButtonPart.CanCollide = false
v.ButtonPart.BillBoardGui.Frame.Visible = false
end
if v:FindFirstChild("Dependency") then
coroutine.resume(coroutine.create(function(
v.ButtonPart.Transparency = 1
v.ButtonPart.CanCollide = false
v.ButtonPart.BillBoardGui.Frame.Visible = false
if BoughtItems:WaitForChild(v.Dependency.Value, 100000)then
v.ButtonPart.Transparency = 0
v.ButtonPart.CanCollide = true
v.ButtonPart.BillBoardGui.Frame.Visible = true
end
end))
end
v.ButtonPart.Touched:Connect(function(Hit)
if Hit.Parent:FindFirstChild("Humanoid")then
local Player = game.Players:GetPlayerFromCharacter(Hit.Parent)
if Values.OwnerValue.Value == Player then
if v.ButtonPart.CanCollide == true and v.ButtonPart.Transparency == 0 then
if Player:WaitForChild("leaderstats").Cash.Value >= v.Price.Value then
Player.leaderstats.Cash.Value -= v.Price.Value
Items[v.Item.Value].Parent = BoughtItems
v:Destroy()
end
end
end
end
end)
end
how do i fix this the header is the problem i dont knw what it means so is there a fix to it for my tycoon if anyone knows pls comment (heres some random stuff since my post is mostly code) A brief Introduction to Copypasta. Copypasta means to copy a text or a part of the text from an existing manuscript and inclusion of that very text in an under-process manuscript by pasting. At present, the societies are going to be expanded with an enormous pattern.
Parenthesis always come in pairs. The error message already strongly suggests that you are missing a closing parenthesis for an opening one in line 38.
1 2 3
coroutine.resume(coroutine.create(function(
v.ButtonPart.Transparency = 1
v.ButtonPart.CanCollide = false
v.ButtonPart.BillBoardGui.Frame.Visible = false
if BoughtItems:WaitForChild(v.Dependency.Value, 100000)then
v.ButtonPart.Transparency = 0
v.ButtonPart.CanCollide = true
v.ButtonPart.BillBoardGui.Frame.Visible = true
end
end)) <--- one is missing
1 2
Use a text editor that autocompletes pairs or even better one that hightlights pairs in matching colors to avoid errors like this.

<name> error while trying to do two or more loops

Last time I wanted to update my LUA code to be more automatic. Earlier had to turn on and off everything manual, by deleting phrases. But I met small problem name expected near '2' error and I'm not sure what's wrong is with it. I was looking in other thread about similar error, but they didn't help me.
local BOSS = GetModConfigData("BOSS") --return true or false
local KOAL = GetModConfigData("KOAL") --return true or false
local SCULP = GetModConfigData("SCULP") --return true or false
local BossList = {"BOSS", "KOAL", "SCULP"}
local BossCode = {"bosscode", "koal_old", "koal_new", "sculp_small", "sculp_med", "sculp_big"}
k = 1
for i,v in ipairs(BossList) do
if BossList[i] == true then
if BossList[i] == "KOAL" then
for i,2 do
Bosses[k] = BossCode[k]
k = k+1
end
elseif BossList[i] == "SCULP" then
for i,3 do
Bosses[k] = BossCode[k]
k = k+1
end
else
Bosses[k] = BossCode[k]
k = k+1
end
end
end
When I'm trying to use second loop, problem comes to me. When I pushed this code without second and third loop, it was working. But without save additional BossCodes.
for i,v in ipairs(BossList) do
if BossList[i] == true then
Bosses[k] = BossCode[k]
k = k+1
end
end

Aggregate an array of Merchant store hours that overlap to produce a single range

I have a list of store hours from a merchant that could potentially overlap stored in AvailableHours model. The model has a start_time and an end_time stored as a TimeOfDay object (https://github.com/JackC/tod).
I want to iterate through the list and compare the store hours so that I can just display 1 store hour. For example, if a store could had hours of Mon: 9am-2pm and Mon: 10am-5pm, I would want to display Mon: 9am-5pm. A store could also have two times in a single day, i.e. Fri: 9-5pm, 7pm-10pm. I have to do this for each day of the week.
The issue I am running into is that my code is becoming hard to manage - I started introducing a lot of nested if/else for manual checking (what a nightmare!), but my question is this:
Is there a way to use ActiveRecord to produce a hash with merged time ranges?
def ranges_overlap?(shift, current_shift)
shift.include?(current_shift.beginning) || current_shift.include?(shift.beginning)
end
def merge_ranges(shift, current_shift)
new_open_time = [shift.beginning, current_shift.beginning].min
new_close_time = [shift.ending, current_shift.ending].max
Shift.new(new_open_time, new_close_time)
end
def get_aggregate_merchant_hours
merchant = self
day = nil
shifts_array = []
output_shift_array = []
merchant_shifts = merchant_shifts = merchant.available_hours.order(day: :asc, open_time: :desc).map do |ah|
merged_shift = nil
show_output = false
# if it's a new day
if day.blank? || day != ah.day
output_shift_array = shifts_array
shifts_array = []
show_output = true if !day.blank?
output_day = day
day = ah.day
end
next if ah.open_time.blank? || ah.close_time.blank?
open_time = ah.open_time
close_time = ah.close_time
current_shift = Shift.new(open_time, close_time)
if !shifts_array.blank?
#compare and merge
shifts_array.each do |shift|
merged_shift = merge_ranges(shift, current_shift) if ranges_overlap?(shift, current_shift)
end
end
#replace old shift with merged shift
if merged_shift
delete_shift = shifts_array.find(beginning: current_shift.beginning, ending: current_shift.ending).first
shifts_array.delete(delete_shift) if delete_shift
shifts_array.push(merged_shift)
else
shifts_array.push(current_shift)
end
if show_output
store_hours_string = ""
if output_shift_array.blank?
store_hours_string = "Closed"
else
output_shift_array.each do |shift|
shift.beginning.strftime("%I:%M%p")
shift.ending.strftime("%I:%M%p")
if store_hours_string.blank?
store_hours_string << "#{shift.beginning.strftime("%I:%M%p").downcase} - #{shift.ending.strftime("%I:%M%p").downcase}"
else
store_hours_string << ", #{shift.beginning.strftime("%I:%M%p").downcase} - #{shift.ending.strftime("%I:%M%p").downcase}"
end
end
end
"#{Date::DAYNAMES[output_day][0..2]}: #{store_hours_string}"
end
end
#output last shift
store_hours_string = ""
if output_shift_array.blank?
store_hours_string = "Closed"
else
output_shift_array.each do |shift|
shift.beginning.strftime("%I:%M%p")
shift.ending.strftime("%I:%M%p")
if store_hours_string.blank?
store_hours_string << "#{shift.beginning.strftime("%I:%M%p").downcase} - #{shift.ending.strftime("%I:%M%p").downcase}"
else
store_hours_string << ", #{shift.beginning.strftime("%I:%M%p").downcase} - #{shift.ending.strftime("%I:%M%p").downcase}"
end
end
end
merchant_shifts << "#{Date::DAYNAMES[day][0..2]}: #{store_hours_string}"
merchant_shifts.compact
end

Rspec Ruby on Rails Test File System in model

I have a model that has a method that looks through the filesystem starting at a particular location for files that match a particular regex. This is executed in an after_save callback. I'm not sure how to test this using Rspec and FactoryGirl. I'm not sure how to use something like FakeFS with this because the method is in the model, not the test or the controller. I specify the location to start in my FactoryGirl factory, so I could change that to a fake directory created by the test in a set up clause? I could mock the directory? I think there are probably several different ways I could do this, but which makes the most sense?
Thanks!
def ensure_files_up_to_date
files = find_assembly_files
add_files = check_add_assembly_files(files)
errors = add_assembly_files(add_files)
if errors.size > 0 then
return errors
end
update_files = check_update_assembly_files(files)
errors = update_assembly_files(update_files)
if errors.size > 0 then
return errors
else
return []
end
end
def find_assembly_files
start_dir = self.location
files = Hash.new
if ! File.directory? start_dir then
errors.add(:location, "Directory #{start_dir} does not exist on the system.")
abort("Directory #{start_dir} does not exist on the system for #{self.inspect}")
end
Find.find(start_dir) do |path|
filename = File.basename(path).split("/").last
FILE_TYPES.each { |filepart, filehash|
type = filehash["type"]
vendor = filehash["vendor"]
if filename.match(filepart) then
files[type] = Hash.new
files[type]["path"] = path
files[type]["vendor"] = vendor
end
}
end
return files
end
def check_add_assembly_files(files=self.find_assembly_files)
add = Hash.new
files.each do |file_type, file_hash|
# returns an array
file_path = file_hash["path"]
file_vendor = file_hash["vendor"]
filename = File.basename(file_path)
af = AssemblyFile.where(:name => filename)
if af.size == 0 then
add[file_path] = Hash.new
add[file_path]["type"] = file_type
add[file_path]["vendor"] = file_vendor
end
end
if add.size == 0 then
logger.error("check_add_assembly_files did not find any files to add")
return []
end
return add
end
def check_update_assembly_files(files=self.find_assembly_files)
update = Hash.new
files.each do |file_type, file_hash|
file_path = file_hash["path"]
file_vendor = file_hash["vendor"]
# returns an array
filename = File.basename(file_path)
af = AssemblyFile.find_by_name(filename)
if !af.nil? then
if af.location != file_path or af.file_type != file_type then
update[af.id] = Hash.new
update[af.id]['path'] = file_path
update[af.id]['type'] = file_type
update[af.id]['vendor'] = file_vendor
end
end
end
return update
end
def add_assembly_files(files=self.check_add_assembly_files)
if files.size == 0 then
logger.error("add_assembly_files didn't get any results from check_add_assembly_files")
return []
end
asm_file_errors = Array.new
files.each do |file_path, file_hash|
file_type = file_hash["type"]
file_vendor = file_hash["vendor"]
logger.debug "file type is #{file_type} and path is #{file_path}"
logger.debug FileType.find_by_type_name(file_type)
file_type_id = FileType.find_by_type_name(file_type).id
header = file_header(file_path, file_vendor)
if file_vendor == "TBA" then
check = check_tba_header(header, file_type, file_path)
software = header[TBA_SOFTWARE_PROGRAM]
software_version = header[TBA_SOFTWARE_VERSION]
elsif file_vendor == "TBB" then
check = check_tbb_header(header, file_type, file_path)
if file_type == "TBB-ANNOTATION" then
software = header[TBB_SOURCE]
else
software = "Unified"
end
software_version = "UNKNOWN"
end
if check == 0 then
logger.error("skipping file #{file_path} because it contains incorrect values for this filetype")
asm_file_errors.push("#{file_path} cannot be added to assembly because it contains incorrect values for this filetype")
next
end
if file_vendor == "TBA" then
xml = header.to_xml(:root => "assembly-file")
elsif file_vendor == "TBB" then
xml = header.to_xml
else
xml = ''
end
filename = File.basename(file_path)
if filename.match(/~$/) then
logger.error("Skipping a file with a tilda when adding assembly files. filename #{filename}")
next
end
assembly_file = AssemblyFile.new(
:assembly_id => self.id,
:file_type_id => file_type_id,
:name => filename,
:location => file_path,
:file_date => creation_time(file_path),
:software => software,
:software_version => software_version,
:current => 1,
:metadata => xml
)
assembly_file.save! # exclamation point forces it to raise an error if the save fails
end # end files.each
return asm_file_errors
end
Quick answer: you can stub out model methods like any others. Either stub a specific instance of a model, and then stub find or whatever to return that, or stub out any_instance to if you don't want to worry about which model is involved. Something like:
it "does something" do
foo = Foo.create! some_attributes
foo.should_receive(:some_method).and_return(whatever)
Foo.stub(:find).and_return(foo)
end
The real answer is that your code is too complicated to test effectively. Your models should not even know that a filesystem exists. That behavior should be encapsulated in other classes, which you can test independently. Your model's after_save can then just call a single method on that class, and testing whether or not that single method gets called will be a lot easier.
Your methods are also very difficult to test, because they are trying to do too much. All that conditional logic and external dependencies means you'll have to do a whole lot of mocking to get to the various bits you might want to test.
This is a big topic and a good answer is well beyond the scope of this answer. Start with the Wikipedia article on SOLID and read from there for some of the reasoning behind separating concerns into individual classes and using tiny, composed methods. To give you a ballpark idea, a method with more than one branch or more than 10 lines of code is too big; a class that is more than about 100 lines of code is too big.

Resources