With earlier changes, Prog::Base#donate is only called by Prog::Base#reap (outside a couple of test calls that don't need to call it). Moving donate into reap avoids a redundant child query. When we would call donate, we already know which children are active, because reap already selected all children, and separated them into reapable children and active children.
374 lines
14 KiB
Ruby
374 lines
14 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "../model/spec_helper"
|
|
|
|
RSpec.describe Prog::Base do
|
|
it "does not allow failure of one child strand inside donate to affect other strands" do
|
|
parent = Strand.create(prog: "Test", label: "reap_exit_no_children")
|
|
popper = Strand.create(parent_id: parent.id, prog: "Test", label: "popper")
|
|
failer = Strand.create(parent_id: parent.id, prog: "Test", label: "failer")
|
|
Strand.create(parent_id: parent.id, prog: "Test", label: "napper", lease: Time.now + 10)
|
|
|
|
expect(parent).to receive(:children_dataset).twice.and_wrap_original do
|
|
# Force popper before failer
|
|
it.call.order(Sequel.case({"popper" => 1}, 2, :label))
|
|
end
|
|
|
|
expect { parent.run(10) }.to raise_error(RuntimeError)
|
|
expect(popper.this.get(:exitval)).to eq("msg" => "popped")
|
|
expect(failer.this.get(:exitval)).to be_nil
|
|
end
|
|
|
|
it "can bud and reap" do
|
|
parent = Strand.create_with_id(prog: "Test", label: "budder")
|
|
expect {
|
|
parent.unsynchronized_run
|
|
}.to change { parent.children_dataset.empty? }.from(true).to(false)
|
|
|
|
child_id = parent.children.first.id
|
|
Semaphore.incr(child_id, :should_get_deleted)
|
|
|
|
expect {
|
|
# Execution donated to child sets the exitval.
|
|
parent.run
|
|
|
|
# Parent notices exitval is set and reaps the child.
|
|
parent.run
|
|
}.to change { parent.children_dataset.empty? }.from(false).to(true).and change {
|
|
Semaphore.where(strand_id: child_id).empty?
|
|
}.from(false).to(true).and change {
|
|
parent.children.empty?
|
|
}.from(false).to(true).and change {
|
|
Strand[child_id].nil?
|
|
}.from(false).to(true)
|
|
end
|
|
|
|
it "keeps children array state in sync even in consecutive-run mode" do
|
|
parent = Strand.create_with_id(prog: "Test", label: "reap_exit_no_children")
|
|
child = Strand.create_with_id(parent_id: parent.id, prog: "Test", label: "napper")
|
|
prg = parent.load
|
|
expect(parent).to receive(:load).twice.and_return(prg)
|
|
|
|
expect(prg).to receive(:nap).and_raise(Prog::Base::Nap.new(1))
|
|
expect(parent.run(10)).to be_a Prog::Base::Nap
|
|
expect(parent.associations).to be_empty
|
|
|
|
child.destroy
|
|
expect(parent.run(10)).to be_a Prog::Base::Exit
|
|
expect(parent.associations).to be_empty
|
|
end
|
|
|
|
describe "#pop" do
|
|
it "can reject unanticipated values" do
|
|
expect {
|
|
Strand.new(prog: "Test", label: "bad_pop").unsynchronized_run
|
|
}.to raise_error RuntimeError, "BUG: must pop with string or hash"
|
|
end
|
|
|
|
it "crashes is the stack is malformed" do
|
|
expect {
|
|
Strand.new(prog: "Test", label: "popper", stack: [{}] * 2).unsynchronized_run
|
|
}.to raise_error RuntimeError, "BUG: expect no stacks exceeding depth 1 with no back-link"
|
|
end
|
|
end
|
|
|
|
it "can push prog and frames on the stack" do
|
|
st = Strand.create_with_id(prog: "Test", label: "pusher1")
|
|
expect {
|
|
st.run
|
|
}.to change { st.label }.from("pusher1").to("pusher2")
|
|
expect(st.retval).to be_nil
|
|
|
|
expect {
|
|
st.run
|
|
}.to change { st.label }.from("pusher2").to "pusher3"
|
|
expect(st.retval).to be_nil
|
|
|
|
expect {
|
|
st.run
|
|
}.to change { st.label }.from("pusher3").to "pusher2"
|
|
expect(st.retval).to eq({"msg" => "3"})
|
|
|
|
expect {
|
|
st.run
|
|
}.to change { st.label }.from("pusher2").to "pusher1"
|
|
expect(st.retval).to eq({"msg" => "2"})
|
|
|
|
st.run
|
|
expect(st.exitval).to eq({"msg" => "1"})
|
|
|
|
expect { st.run }.to raise_error "already deleted"
|
|
expect { st.reload }.to raise_error Sequel::NoExistingObject
|
|
end
|
|
|
|
it "can nap" do
|
|
st = Strand.create_with_id(prog: "Test", label: "napper")
|
|
ante = st.schedule
|
|
st.run
|
|
post = st.schedule
|
|
expect(post - ante).to be > 121
|
|
end
|
|
|
|
it "can push new subject_id" do
|
|
st = Strand.create_with_id(prog: "Test", label: "push_subject_id")
|
|
st.run
|
|
expect(st.stack.first["subject_id"]).not_to eq(st.id)
|
|
end
|
|
|
|
it "requires a symbol for hop" do
|
|
expect {
|
|
Strand.new(prog: "Test", label: "invalid_hop").unsynchronized_run
|
|
}.to raise_error RuntimeError, "BUG: #hop only accepts a symbol"
|
|
end
|
|
|
|
it "requires valid label for hop" do
|
|
expect {
|
|
Strand.new(prog: "Test", label: :invalid_hop_target).unsynchronized_run
|
|
}.to raise_error RuntimeError, "BUG: not valid hop target"
|
|
end
|
|
|
|
it "can manipulate semaphores" do
|
|
st = Strand.create_with_id(prog: "Test", label: "increment_semaphore")
|
|
expect {
|
|
st.run
|
|
}.to change { Semaphore.where(strand_id: st.id).any? }.from(false).to(true)
|
|
|
|
st.label = "decrement_semaphore"
|
|
expect {
|
|
st.unsynchronized_run
|
|
}.to change { Semaphore.where(strand_id: st.id).any? }.from(true).to(false)
|
|
end
|
|
|
|
it "calls before_run if it is available" do
|
|
st = Strand.create_with_id(prog: "Prog::Vm::Nexus", label: "wait")
|
|
prg = instance_double(Prog::Vm::Nexus)
|
|
expect(st).to receive(:load).and_return(prg)
|
|
expect(prg).to receive(:before_run)
|
|
expect(prg).to receive(:wait).and_raise(Prog::Base::Nap.new(30))
|
|
st.unsynchronized_run
|
|
end
|
|
|
|
context "when rendering FlowControl strings" do
|
|
it "can render hop" do
|
|
expect(
|
|
described_class::Hop.new("OldProg", "old_label",
|
|
Strand.new(prog: "NewProg", label: "new_label")).to_s
|
|
).to eq("hop OldProg#old_label -> NewProg#new_label")
|
|
end
|
|
|
|
it "can render nap" do
|
|
expect(described_class::Nap.new("10").to_s).to eq("nap for 10 seconds")
|
|
end
|
|
|
|
it "can render exit" do
|
|
expect(described_class::Exit.new(
|
|
Strand.new(prog: "TestProg", label: "exiting_label"), {"msg" => "done"}
|
|
).to_s).to eq('Strand exits from TestProg#exiting_label with {"msg" => "done"}')
|
|
end
|
|
end
|
|
|
|
context "with deadlines" do
|
|
it "registers a deadline only if the current deadline is nil, later or to a different target" do
|
|
st = Strand.create_with_id(prog: "Test", label: :set_expired_deadline)
|
|
|
|
# register if current deadline is nil
|
|
expect {
|
|
st.unsynchronized_run
|
|
}.to change { st.stack.first["deadline_at"] }
|
|
|
|
# register if current deadline is further in the time
|
|
st.label = :set_expired_deadline
|
|
st.stack.first["deadline_at"] = Time.now + 30
|
|
expect {
|
|
st.unsynchronized_run
|
|
}.to change { st.stack.first["deadline_at"] }
|
|
|
|
# register if the target is different
|
|
st.label = :set_expired_deadline
|
|
st.stack.first["deadline_target"] = "napper"
|
|
expect {
|
|
st.unsynchronized_run
|
|
}.to change { st.stack.first["deadline_at"] }
|
|
st.unsynchronized_run
|
|
|
|
# ignore if new deadline is further in the time and target is same
|
|
st.label = :set_expired_deadline
|
|
st.stack.first["deadline_at"] = Time.now - 60
|
|
st.stack.first["deadline_target"] = "pusher2"
|
|
expect {
|
|
st.unsynchronized_run
|
|
}.not_to change { st.stack.first["deadline_at"] }
|
|
|
|
# allow to explicitly extend deadline
|
|
st.label = :extend_deadline
|
|
st.stack.first["deadline_at"] = Time.now
|
|
st.stack.first["deadline_target"] = "pusher2"
|
|
expect {
|
|
st.unsynchronized_run
|
|
}.to change { st.stack.first["deadline_at"] }
|
|
end
|
|
|
|
it "triggers a page exactly once when deadline is expired" do
|
|
st = Strand.create_with_id(prog: "Test", label: :set_expired_deadline)
|
|
st.unsynchronized_run
|
|
expect {
|
|
st.unsynchronized_run
|
|
}.to change { Page.active.count }.from(0).to(1)
|
|
|
|
expect {
|
|
st.unsynchronized_run
|
|
}.not_to change { Page.active.count }.from(1)
|
|
end
|
|
|
|
it "resolves the page if the frame is popped" do
|
|
st = Strand.create_with_id(prog: "Test", label: :set_popping_deadline1)
|
|
st.unsynchronized_run
|
|
st.unsynchronized_run
|
|
expect {
|
|
st.unsynchronized_run
|
|
}.to change { Page.active.count }.from(0).to(1)
|
|
|
|
expect {
|
|
st.unsynchronized_run
|
|
|
|
page_id = Page.first.id
|
|
Strand[page_id].unsynchronized_run
|
|
Strand[page_id].unsynchronized_run
|
|
}.to change { Page.active.count }.from(1).to(0)
|
|
end
|
|
|
|
it "resolves the page of the budded prog when pop" do
|
|
st = Strand.create_with_id(prog: "Test", label: :set_popping_deadline_via_bud)
|
|
st.unsynchronized_run
|
|
st.unsynchronized_run
|
|
expect {
|
|
st.unsynchronized_run
|
|
}.to change { Page.active.count }.from(0).to(1)
|
|
|
|
expect {
|
|
st.unsynchronized_run
|
|
|
|
page_id = Page.first.id
|
|
Strand[page_id].unsynchronized_run
|
|
Strand[page_id].unsynchronized_run
|
|
}.to change { Page.active.count }.from(1).to(0)
|
|
end
|
|
|
|
it "resolves the page once the target is reached" do
|
|
st = Strand.create_with_id(prog: "Test", label: :napper)
|
|
page_id = Prog::PageNexus.assemble("dummy-summary", ["Deadline", st.id, st.prog, :napper], st.ubid).id
|
|
|
|
st.stack.first["deadline_target"] = "napper"
|
|
st.stack.first["deadline_at"] = Time.now - 1
|
|
|
|
expect {
|
|
st.unsynchronized_run
|
|
Strand[page_id].unsynchronized_run
|
|
Strand[page_id].unsynchronized_run
|
|
}.to change { Page.active.count }.from(1).to(0)
|
|
end
|
|
|
|
it "resolves the page once a new deadline is registered" do
|
|
st = Strand.create_with_id(prog: "Test", label: :start)
|
|
page_id = Prog::PageNexus.assemble("dummy-summary", ["Deadline", st.id, st.prog, :napper], st.ubid).id
|
|
|
|
st.stack.first["deadline_target"] = "napper"
|
|
st.stack.first["deadline_at"] = Time.now - 1
|
|
|
|
st.update(label: :set_popping_deadline2)
|
|
|
|
expect {
|
|
st.unsynchronized_run
|
|
|
|
page_id = Page.first.id
|
|
Strand[page_id].unsynchronized_run
|
|
Strand[page_id].unsynchronized_run
|
|
}.to change { Page.active.count }.from(1).to(0)
|
|
end
|
|
|
|
it "deletes the deadline information once the target is reached" do
|
|
st = Strand.create_with_id(prog: "Test", label: :napper)
|
|
st.stack.first["deadline_target"] = "napper"
|
|
st.stack.first["deadline_at"] = Time.now - 1
|
|
|
|
expect(st.stack.first).to receive(:delete).with("deadline_target")
|
|
expect(st.stack.first).to receive(:delete).with("deadline_at")
|
|
|
|
st.unsynchronized_run
|
|
end
|
|
|
|
it "can create pages for progs that are not on the top of the stack" do
|
|
st = Strand.create_with_id(prog: "Test2", label: "pusher1")
|
|
st.stack.first["deadline_target"] = "t1"
|
|
st.stack.first["deadline_at"] = Time.now + 1
|
|
st.unsynchronized_run
|
|
st.stack[1]["deadline_at"] = Time.now - 1
|
|
st.stack.first["deadline_target"] = "t2"
|
|
st.stack.first["deadline_at"] = Time.now - 1
|
|
|
|
expect {
|
|
st.unsynchronized_run
|
|
}.to change { Page.active.count }.from(0).to(2)
|
|
|
|
expect(Page.all.map(&:summary)).to include(
|
|
"#{st.ubid} has an expired deadline! Test2.pusher2 did not reach t1 on time",
|
|
"#{st.ubid} has an expired deadline! Test.pusher2 did not reach t2 on time"
|
|
)
|
|
end
|
|
|
|
it "can create a page with extra data from a vm" do
|
|
vm = create_vm
|
|
st = Strand.create(prog: "Test", label: :napper, stack: [{"deadline_at" => Time.now - 1, "deadline_target" => "start"}]) { it.id = vm.id }
|
|
st.unsynchronized_run
|
|
page = Page.active.first
|
|
expect(page).not_to be_nil
|
|
expect(page.details["location"]).to eq(vm.location.display_name)
|
|
expect(page.details["vcpus"]).to eq(vm.vcpus)
|
|
end
|
|
|
|
it "can create a page with extra data from a vm with a vm host" do
|
|
vm = create_vm(vm_host: create_vm_host(data_center: "FSN1-DC1"))
|
|
st = Strand.create(prog: "Test", label: :napper, stack: [{"deadline_at" => Time.now - 1, "deadline_target" => "start"}]) { it.id = vm.id }
|
|
st.unsynchronized_run
|
|
page = Page.active.first
|
|
expect(page).not_to be_nil
|
|
expect(page.details["vm_host"]).to eq(vm.vm_host.ubid)
|
|
expect(page.details["data_center"]).to eq(vm.vm_host.data_center)
|
|
end
|
|
|
|
it "can create a page with extra data from a vm host" do
|
|
vmh = create_vm_host(data_center: "FSN1-DC1")
|
|
create_vm(vm_host: vmh)
|
|
st = Strand.create(prog: "Test", label: :napper, stack: [{"deadline_at" => Time.now - 1, "deadline_target" => "start"}]) { it.id = vmh.id }
|
|
st.unsynchronized_run
|
|
page = Page.active.first
|
|
expect(page).not_to be_nil
|
|
expect(page.details["arch"]).to eq(vmh.arch)
|
|
expect(page.details["vm_count"]).to eq(1)
|
|
end
|
|
|
|
it "can create a page with extra data from a github runner" do
|
|
installation = GithubInstallation.create(installation_id: 123, name: "test-user", type: "User", project: Project.create(name: "test-project"))
|
|
runner = GithubRunner.create(label: "ubicloud-standard-2", repository_name: "my-repo", installation:)
|
|
st = Strand.create(prog: "Test", label: :napper, stack: [{"deadline_at" => Time.now - 1, "deadline_target" => "start"}]) { it.id = runner.id }
|
|
st.unsynchronized_run
|
|
page = Page.active.first
|
|
expect(page).not_to be_nil
|
|
expect(page.details["label"]).to eq("ubicloud-standard-2")
|
|
expect(page.details["installation"]).to eq(installation.ubid)
|
|
end
|
|
|
|
it "can create a page with extra data from a github runner with a vm" do
|
|
vm = create_vm(vm_host: create_vm_host(data_center: "FSN1-DC1"))
|
|
installation = GithubInstallation.create(installation_id: 123, name: "test-user", type: "User", project: Project.create(name: "test-project"))
|
|
runner = GithubRunner.create(label: "ubicloud-standard-2", repository_name: "my-repo", vm_id: vm.id, installation:)
|
|
st = Strand.create(prog: "Test", label: :napper, stack: [{"deadline_at" => Time.now - 1, "deadline_target" => "start"}]) { it.id = runner.id }
|
|
st.unsynchronized_run
|
|
page = Page.active.first
|
|
expect(page).not_to be_nil
|
|
expect(page.details["vm"]).to eq(vm.ubid)
|
|
expect(page.details["data_center"]).to eq(vm.vm_host.data_center)
|
|
end
|
|
end
|
|
end
|