Summary#
app Pumpworks {
use Osysharp.Assignments;
use Osysharp.Assignments.Allocations;
}
policy KeepsSharedPools => IsOperations; // says what there is; decides who gives way
policy ClaimsFromSharedPools => IsOperations || IsRegionLead; // sees the pools; claims for a job they act on
var stock = OpenPool(new NewPool { Key = "malmo-housings", Title = "Housings in the Malmö warehouse", Unit = "housings", Available = 120 });
Claim(stock, nordics, 80, "Nordic orders due this month"); // held: nobody else can take these 80
Claim(stock, dach, 60, "DACH orders due this month"); // refused — 40 are left, and it says what the rest is held forSignature#
SharedPool OpenPool(NewPool what); // KeepsSharedPools — or the pool already open under the key
SharedPool PoolBy(string key);
decimal LeftIn(SharedPool pool);
Allocation Claim(SharedPool pool, Assignment a, decimal amount, string why, string key = ""); // held, or throws NotEnoughLeft
Allocation AskFor(SharedPool pool, Assignment a, decimal amount, string why, string key = ""); // held, or waits its turn
void ReleaseAllocation(Allocation claim, string why); // given back; a waiting claim is withdrawn
void UseAllocation(Allocation claim, string what); // shipped, spent, worked: gone from the pool for good
void CorrectPool(SharedPool pool, decimal available, string why); // KeepsSharedPools — what there is now
void CutAllocation(Allocation claim, decimal to, string why); // KeepsSharedPools — who gives way; 0 takes all of it
PoolView PoolOf(Guid pool);
List<AllocationLine> AllocationsOf(Guid assignment);
component PoolCard(Guid pool);Description#
What is a pool, and what is a claim?#
A pool is something there is a counted amount of: Available, in a Unit a person reads ("housings", "hours",
"EUR"). A claim is one assignment's share of it: an amount, what it is for, and where it stands — held, waiting,
given back, used, taken away.
Claim either holds the amount at once or throws NotEnoughLeft, whose message says how much is left and what the
rest is held for:
40 housings of Housings in the Malmö warehouse are left, and “DACH” asked for 60 housings. Held: 80 housings for Nordic orders due this month.
Claimed again under the same key, the claim already made is answered — a process step that is run again after a
crash holds nothing twice.
Why does a claim say what it is for?#
Whoever sees a pool need not be on the assignments that draw on it: a region's lead reads her own job and not her
neighbour's. So a claim is named, to everybody who sees the pool, by what its claimant said it is for — why — and
an assignment's title is shown only to somebody who may read that assignment. Write why for those readers.
What if it does not fit, and the assignment can wait?#
AskFor holds the amount when there is room. Otherwise the claim waits its turn: the assignment carries a piece
of blocked work — "Wait for 30 housings of Housings in the Malmö warehouse" — which is what makes its page say it
cannot move, and why. Waiting claims are served in the order they were made, when room appears: something is given
back, used up elsewhere, or a delivery is counted in. Nobody jumps the queue — with somebody already waiting, a new
claim waits behind them even if it would fit.
Whoever waits is served by the app itself, never by whoever made the room: they are seldom on the assignment that waited.
What happens when there is less than was thought?#
CorrectPool(pool, available, why) records what there is now. With more room, whoever waits is served. With
less than is held, the pool is overdrawn, and the kit takes nothing from anybody:
- every assignment holding some of it gets a line on its activity: how much there is now, how much is held, and that its own share is in question;
- a share that is in question cannot be used (
UseAllocationrefuses); PoolOverdrawnis raised, for the app to tell whoever decides.
Who gives way is a person's decision: CutAllocation(claim, to, why), by whoever keeps the pool. The assignment is
told, with the reason, and AllocationCut is raised — which is where an app puts that assignment's plan back in
question and plans again. When no more is held than there is, nobody's share is in question any more.
[On] void ARegionGaveWay(AllocationCut e) {
var region = SalesRegion.Single(r => r.Job == e.Allocation.Assignment);
ReopenCriterion(CriterionOf(region.Job, "plan"), "Its share of the stock was cut: " + e.Why);
}What happens when an assignment closes?#
It holds nothing. Completed, called off or failed, what it held goes back to the pool, what it waited for is withdrawn — with the reason the assignment gave — and whoever was waiting is served in turn.
Who may do what?#
| Open a pool, say what there is now, decide who gives way | KeepsSharedPools — nobody until the app says |
| See the pools; claim, give back and use, for an assignment they act on | ClaimsFromSharedPools — nobody until the app says |
| Read a claim | whoever sees the pools, and whoever is on its assignment |
| Write a pool's or a claim's numbers by hand | nobody — the keeper included |
Two claims made at the same moment cannot both pass: the pool's held amount is checked on every write
([ConcurrencyCheck]), and the second is refused as a conflict.
What is on screen?#
PoolCard(pool) draws the pool: how much there is, how much is held and how much is left; each holder, with what
it holds and whether its share is in question; who waits their turn; and what was given back, used or taken away,
with why. Overdrawn, it says so first, and by how much.
Every assignment's page (AssignmentView) gains a section, "Shared resources", with what that assignment holds and
waits for.
Examples#
The kit's test app is a pump maker whose supplier of housings shuts down — A supplier shuts down — three regions, one stock, a plan refused by the stock, and a plan made again tells it. Its tests show each rule:
| A claim is held, and one that does not fit is refused saying what the rest is held for | a_claim_is_held_and_one_that_does_not_fit_is_refused_saying_who_holds_the_rest |
| Claimed again under the same key, it is the claim already made | a_claim_made_again_under_the_same_key_is_the_claim_already_made |
| A claim that does not fit can wait, and is served in the order it asked | a_claim_that_does_not_fit_can_wait_and_is_served_in_the_order_it_asked |
| A job that is closed holds nothing, and whoever waited is served | a_job_that_is_closed_holds_nothing_and_whoever_waited_is_served |
| Less than was thought overdraws the pool, and its keeper decides who gives way | less_than_was_thought_overdraws_the_pool_and_its_keeper_decides_who_gives_way |
| Each person does what is theirs with a pool, and nothing else | each_person_does_what_is_theirs_with_a_pool_and_nothing_else |
app Pumpworks {
use Osysharp.Assignments;
use Osysharp.Assignments.Allocations;
use Osysharp.Llm;
use Osysharp.Identity;
use Osysharp.Observability;
use Osysharp.Workflow;
use Osysharp.Ui;
model "model/**/*.osy";
}
using Osysharp.Assignments;
using Osysharp.Assignments.Allocations;
using Osysharp.Identity;
[Role] enum PlantRole { Authenticator, Operations, RegionLead }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsOperations => RoleGrant.Any(g => g.Member == user && g.Role == PlantRole.Operations);
policy IsRegionLead => RoleGrant.Any(g => g.Member == user && g.Role == PlantRole.RegionLead);
// The kits' questions, answered by this app.
policy OpensAssignments => IsOperations;
policy OverseesAssignments => IsOperations;
policy AcceptsAssignmentExceptions => IsOperations;
policy KeepsSharedPools => IsOperations;
policy ClaimsFromSharedPools => IsOperations || IsRegionLead;
entity Person : Principal {
security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}
[TestFixture]
void Plant() {
var astrid = new Person { Email = "[email protected]", DisplayName = "Astrid" };
var bram = new Person { Email = "[email protected]", DisplayName = "Bram" };
var clara = new Person { Email = "[email protected]", DisplayName = "Clara" };
new RoleGrant { Member = astrid, Role = PlantRole.Operations };
new RoleGrant { Member = bram, Role = PlantRole.RegionLead };
new RoleGrant { Member = clara, Role = PlantRole.RegionLead };
}
principal Astrid => Person.Single(p => p.Email == "[email protected]");
principal Bram => Person.Single(p => p.Email == "[email protected]");
principal Clara => Person.Single(p => p.Email == "[email protected]");
SharedPool Stock() { return PoolBy("malmo-housings"); }
Assignment Job(string title) { return Assignment.Single(a => a.Title == title); }
Allocation ClaimFor(string title) { return Allocation.Single(c => c.Assignment.Title == title); }
[Test(Plant)]
void Two_jobs_share_one_stock_and_the_keeper_decides_who_gives_way() {
runas (Astrid) {
OpenPool(new NewPool { Key = "malmo-housings", Title = "Housings in the Malmö warehouse", Unit = "housings", Available = 120 });
OpenAssignment(new NewAssignment { Title = "Nordics", Goal = "Nordic orders ship.", Criteria = ["Shipped"], Owner = Bram });
OpenAssignment(new NewAssignment { Title = "DACH", Goal = "DACH orders ship.", Criteria = ["Shipped"], Owner = Clara });
}
runas (Bram) {
Assert.Equal(AllocationState.Held, Claim(Stock(), Job("Nordics"), 80, "Nordic orders due this month").State);
}
runas (Clara) {
// 60 does not fit: the refusal says what is left and what the rest is held for.
var refused = Assert.Throws<NotEnoughLeft>(() => Claim(Stock(), Job("DACH"), 60, "DACH orders due this month"));
Assert.Equal(40, refused.Left);
// Asked for instead, the claim waits its turn.
Assert.Equal(AllocationState.Waiting, AskFor(Stock(), Job("DACH"), 60, "DACH orders due this month").State);
}
// A count finds less than is held. Nothing is taken from anybody; the keeper decides.
runas (Astrid) {
CorrectPool(Stock(), 70, "The count found 50 housings damaged by damp.");
Assert.True(PoolOf(Stock().Id).Overdrawn);
}
runas (Bram) { Assert.Throws<ValidationException>(() => UseAllocation(ClaimFor("Nordics"), "Shipped")); }
runas (Astrid) { CutAllocation(ClaimFor("Nordics"), 10, "Most Nordic orders can ship in June."); }
Background.Settle();
// No more is held than there is, and the room the cut made went to whoever waited.
runas (Astrid) {
Assert.False(PoolOf(Stock().Id).Overdrawn);
Assert.Equal(AllocationState.Held, ClaimFor("DACH").State);
Assert.Equal(70, Stock().Held);
}
// A job that closes holds nothing: what it held goes back to the pool.
runas (Bram) { CancelAssignment(Job("Nordics"), "The Nordic customers took another pump."); }
Background.Settle();
runas (Astrid) {
Assert.Equal(AllocationState.Released, ClaimFor("Nordics").State);
Assert.Equal(60, Stock().Held);
}
}See also#
- Assignments — a piece of work owned until its outcome is reached (Assignments kit) — the assignment a claim is made for; jobs opened under a job, and links between them
- What an assignment asks of people — requests as an assignment's work and as inbox items (Assignments.WorkQueues kit) — what an assignment asks of people
- A supplier shuts down — three regions, one stock, a plan refused by the stock, and a plan made again — three regions, one stock, and a plan made again