ubicloud/spec/prog/base_spec.rb
Daniel Farina 5a688b5c72 Add Prog::Base.current_prog to find prog information in the stack
It'd be helpful for my Sshable session conflict assessment to know
what progs and labels are involved in contention, but as the relevant
code is down-stack, it's not immediately obvious how to get access to
the current prog and label information.

Searching the stack is a way to do the job.
2025-09-24 20:14:15 -07:00

387 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(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(prog: "Test", label: "reap_exit_no_children")
child = Strand.create(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(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(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(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(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
describe Prog::Base, :current_prog do
it "returns nil if Progs are not in the call stack" do
expect(described_class.current_prog).to be_nil
end
it "can have its label located deeper in the call stack by Prog.current_prog" do
st = Strand.create(prog: "Test", label: "callee_find_current_prog")
expect(described_class).to receive(:current_prog).and_wrap_original do |om, *args, **kwargs, &blk|
om.call(*args, **kwargs, &blk).tap { expect(it).to eq("Prog::Test#callee_find_current_prog") }
end
st.run
end
end
it "calls before_run if it is available" do
st = Strand.create(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(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(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(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(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(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(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(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(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_with_id(vm.id, prog: "Test", label: :napper, stack: [{"deadline_at" => Time.now - 1, "deadline_target" => "start"}])
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_with_id(vm.id, prog: "Test", label: :napper, stack: [{"deadline_at" => Time.now - 1, "deadline_target" => "start"}])
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_with_id(vmh.id, prog: "Test", label: :napper, stack: [{"deadline_at" => Time.now - 1, "deadline_target" => "start"}])
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_with_id(runner.id, prog: "Test", label: :napper, stack: [{"deadline_at" => Time.now - 1, "deadline_target" => "start"}])
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_with_id(runner.id, prog: "Test", label: :napper, stack: [{"deadline_at" => Time.now - 1, "deadline_target" => "start"}])
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