Osy#betaRun it on Osyrin: osyrin.com

Reference / Agent

What several assignments compete for — shared pools, claims, and what happens when there is less than was thought (Assignments.Allocations kit)

use Osysharp.Assignments.Allocations · OpenPool(new NewPool { … }) → SharedPool · Claim(pool, assignment, amount, why, key:) → Allocation · AskFor(pool, assignment, amount, why, key:) · ReleaseAllocation(claim, why) · UseAllocation(claim, what) · CorrectPool(pool, available, why) · CutAllocation(claim, to, why) · PoolOf(pool) · AllocationsOf(assignment) · PoolCard(pool) · policy KeepsSharedPools · policy ClaimsFromSharedPools · events AllocationHeld, AllocationWaiting, AllocationReleased, PoolOverdrawn, AllocationCut, PoolChanged

Assignments opened under one outcome often draw on the same thing — the stock in one warehouse, a line's hours, a sum of money. A shared pool says how much there is. A claim on it is held for one assignment or refused, so two assignments never hold the same unit; a claim that does not fit can wait its turn. When a count finds less than is held, the pool is overdrawn: every holder is told its share is in question, nothing of it can be used, and whoever keeps the pool decides who gives way. An assignment that closes holds nothing. The pool has a card, and every assignment's page says what it holds of what is shared.

preview1 example compiled by CIassignmentskit

Source on GitHub: Osysharp.Assignments.Allocations — github.com/osysharp/assignments/allocations ↗

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 for

Signature#

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 (UseAllocation refuses);
  • PoolOverdrawn is 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 wayKeepsSharedPools — nobody until the app says
See the pools; claim, give back and use, for an assignment they act onClaimsFromSharedPools — nobody until the app says
Read a claimwhoever sees the pools, and whoever is on its assignment
Write a pool's or a claim's numbers by handnobody — 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 fora_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 madea_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 askeda_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 serveda_job_that_is_closed_holds_nothing_and_whoever_waited_is_served
Less than was thought overdraws the pool, and its keeper decides who gives wayless_than_was_thought_overdraws_the_pool_and_its_keeper_decides_who_gives_way
Each person does what is theirs with a pool, and nothing elseeach_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#

Related

Assignments — a piece of work owned until its outcome is reached (Assignments kit)

An assignment of the `Osysharp.Assignments` kit says who asked, the outcome wanted and what must be true for it to be…

What an assignment asks of people — requests as an assignment's work and as inbox items (Assignments.WorkQueues kit)

With the Assignments kit and the HumanRequests kit both in an app, a request put for an assignment is a piece of the…

A supplier shuts down — three regions, one stock, a plan refused by the stock, and a plan made again

A pump maker's supplier of housings shuts down. The response is one job with a job under it for each sales region. An…

Agents (a class with a job, its own account, a budget and a record)

An agent is a class of the `Osysharp.Agents` kit: a purpose, the skills it holds and what it may spend. A skill is a…