Files
ubicloud/spec/prog/base_spec.rb
Jeremy Evans 835f334d97 Combine donate into reap
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.
2025-06-27 01:40:47 +09:00

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