├── .gitignore ├── LICENSE ├── PSpec └── Specs.p ├── PSrc ├── Client.p ├── Modules.p ├── Promise.p ├── Timer.p └── Worker.p ├── PTst ├── TestDriver.p └── TestScript.p ├── Promises.pproj └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | PCheckerOutput 2 | PGenerated 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pavel Tcholakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PSpec/Specs.p: -------------------------------------------------------------------------------- 1 | event PromiseInit: (id: int); 2 | 3 | event PromiseStateChanged: TPromiseStateChanged; 4 | type TPromiseStateChanged = (id: int, status: PromiseState, value: TValue); 5 | 6 | // Promise safety property: once a promise result is set, it can never be set again 7 | spec PromiseResultNeverChangesOnceSet observes PromiseInit, ResolvePromiseRequest, RejectPromiseRequest { 8 | var promises: map[int, (status: PromiseState, value: TValue)]; 9 | 10 | start state Init { 11 | on PromiseInit goto ObservePromises with (config: (id: int)) { 12 | promises[config.id] = (status = PENDING, value = default(TValue)); 13 | } 14 | } 15 | 16 | state ObservePromises { 17 | on ResolvePromiseRequest do (request: TResolvePromiseRequest) { 18 | assert request.id in promises, 19 | format ("Unknown promise id {0}. Valid ids = {1}", request.id, keys(promises)); 20 | } 21 | 22 | on RejectPromiseRequest do (request: TRejectPromiseRequest) { 23 | assert request.id in promises, 24 | format ("Unknown promise id {0}. Valid ids = {1}", request.id, keys(promises)); 25 | } 26 | 27 | on PromiseStateChanged do (input: TPromiseStateChanged) { 28 | assert input.status == RESOLVED || input.status == REJECTED, 29 | format ("Invalid promise state {0}", input.status); 30 | assert promises[input.id].status == PENDING || (promises[input.id].status == input.status && promises[input.id].value == input.value), 31 | format ("Promise {0} already settled: {1}, but attempt to change to: {2}", input.id, promises[input.id], input); 32 | promises[input.id] = (status = input.status, value = input.value); 33 | } 34 | } 35 | } 36 | 37 | // Promise liveness property: an outstanding promise will eventually be resolved 38 | spec PendingPromisesAreEventuallySettled observes PromiseInit, PromiseStateChanged { 39 | var pendingPromises: set[int]; 40 | 41 | start state Init { 42 | on PromiseInit do (config: ( id: int )) { 43 | pendingPromises += (config.id); 44 | goto ObservePromises; 45 | } 46 | } 47 | 48 | hot state ObservePromises { 49 | on PromiseStateChanged do (input: TPromiseStateChanged) { 50 | assert input.status == RESOLVED || input.status == REJECTED || input.status == REJECTED_TIMED_OUT, 51 | format ("Invalid promise state {0}", input.status); 52 | assert input.id in pendingPromises, 53 | format ("Promise {0} is not pending", input.id); 54 | pendingPromises -= (input.id); 55 | if (sizeof(pendingPromises) == 0) { 56 | goto AllSettled; 57 | } 58 | } 59 | } 60 | 61 | cold state AllSettled { 62 | // Final state 63 | } 64 | } -------------------------------------------------------------------------------- /PSrc/Client.p: -------------------------------------------------------------------------------- 1 | event PromiseCompleted: TPromiseCompleted; 2 | type TPromiseCompleted = (result: TValue); 3 | 4 | // Downstream client waiting on a computation reflected in a promise 5 | machine Client 6 | { 7 | var awaitedResult: Promise; 8 | var result: TValue; 9 | 10 | start state Init { 11 | entry (promise: Promise) { 12 | awaitedResult = promise; 13 | goto Waiting; 14 | } 15 | } 16 | 17 | state Waiting { 18 | on PromiseCompleted do (promiseResult: TValue) { 19 | result = promiseResult; 20 | goto ContinueProcessing; 21 | } 22 | } 23 | 24 | state ContinueProcessing { 25 | entry { 26 | assert(result != null); 27 | 28 | // Get on with our work 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PSrc/Modules.p: -------------------------------------------------------------------------------- 1 | module Promise = { Promise }; 2 | 3 | module Worker = { Worker }; 4 | 5 | module Client = { Client }; 6 | 7 | module Timer = { Timer }; -------------------------------------------------------------------------------- /PSrc/Promise.p: -------------------------------------------------------------------------------- 1 | type TValue = data; 2 | 3 | enum PromiseState { PENDING, RESOLVED, REJECTED, REJECTED_CANCELED, REJECTED_TIMED_OUT } 4 | 5 | event ResolvePromiseRequest: TResolvePromiseRequest; 6 | type TResolvePromiseRequest = (id: int, value: TValue, worker: Worker); 7 | 8 | event RejectPromiseRequest: TRejectPromiseRequest; 9 | type TRejectPromiseRequest = (id: int, worker: Worker); 10 | 11 | // event CancelPromiseRequest: TCancelPromiseRequest; 12 | // type TCancelPromiseRequest =(id: int, worker: Worker); 13 | 14 | event GetPromiseRequest: TGetPromiseRequest; 15 | type TGetPromiseRequest = (id: int, client: machine); 16 | 17 | event GetPromiseResponse: TGetPromiseResponse; 18 | type TGetPromiseResponse = (id: int, status: PromiseState, value: TValue ); 19 | 20 | machine Promise 21 | { 22 | var id: int; 23 | var value: TValue; 24 | 25 | start state Init { 26 | entry (_id: int) { 27 | id = _id; 28 | goto Pending; 29 | } 30 | } 31 | 32 | state Pending { 33 | on ResolvePromiseRequest do (request: TResolvePromiseRequest) { 34 | if (request.id == id && value == null) { 35 | value = request.value; 36 | announce PromiseStateChanged, (id = id, status = RESOLVED, value = value); 37 | goto Resolved; 38 | } 39 | } 40 | 41 | on RejectPromiseRequest do (request: TRejectPromiseRequest) { 42 | if (request.id == id) { 43 | announce PromiseStateChanged, (id = id, status = REJECTED, value = value); 44 | goto Rejected; 45 | } 46 | } 47 | 48 | on eTimeOut goto Rejected with { 49 | announce PromiseStateChanged, (id = id, status = REJECTED_TIMED_OUT, value = value); 50 | } 51 | 52 | // on CancelPromiseRequest do (request: TCancelPromiseRequest) { 53 | // if (request.id == id) { 54 | // goto RejectedCanceled; 55 | // } 56 | // } 57 | } 58 | 59 | state Resolved { 60 | on GetPromiseRequest do (request: TGetPromiseRequest) { 61 | if (request.id == id) { 62 | send request.client, GetPromiseResponse, (id = id, status = RESOLVED, value = value); 63 | } 64 | } 65 | ignore RejectPromiseRequest, ResolvePromiseRequest, eTimeOut; 66 | } 67 | 68 | state Rejected { 69 | on GetPromiseRequest do (request: TGetPromiseRequest) { 70 | if (request.id == id) { 71 | send request.client, GetPromiseResponse, (id = id, status = REJECTED, value = value); 72 | } 73 | } 74 | ignore RejectPromiseRequest, ResolvePromiseRequest, eTimeOut; 75 | } 76 | 77 | // state RejectedCanceled { 78 | // on GetPromiseRequest do (request: TGetPromiseRequest) { 79 | // if (request.id == id) { 80 | // send request.client, GetPromiseResponse, (id = id, status = REJECTED_CANCELED, value = value); 81 | // } 82 | // } 83 | // } 84 | } 85 | -------------------------------------------------------------------------------- /PSrc/Timer.p: -------------------------------------------------------------------------------- 1 | event eStartTimer; 2 | event eCancelTimer; 3 | event eTimeOut; 4 | event eDelayedTimeOut; 5 | 6 | machine Timer { 7 | var client: machine; 8 | start state Init { 9 | entry (_client : machine) { 10 | client = _client; 11 | goto WaitForTimerRequests; 12 | } 13 | } 14 | 15 | state WaitForTimerRequests { 16 | on eStartTimer goto TimerStarted; 17 | ignore eCancelTimer, eDelayedTimeOut; 18 | } 19 | 20 | state TimerStarted { 21 | defer eStartTimer; 22 | entry { 23 | if($) { 24 | send client, eTimeOut; 25 | goto WaitForTimerRequests; 26 | } else { 27 | send this, eDelayedTimeOut; 28 | } 29 | } 30 | on eDelayedTimeOut goto TimerStarted; 31 | on eCancelTimer goto WaitForTimerRequests; 32 | } 33 | } 34 | 35 | fun CreateTimer(client: machine): Timer { 36 | return new Timer(client); 37 | } 38 | 39 | fun StartTimer(timer: Timer) { 40 | send timer, eStartTimer; 41 | } 42 | 43 | fun CancelTimer(timer: Timer) { 44 | send timer, eCancelTimer; 45 | } 46 | -------------------------------------------------------------------------------- /PSrc/Worker.p: -------------------------------------------------------------------------------- 1 | machine Worker 2 | { 3 | var resultHolder: Promise; 4 | var promiseId: int; 5 | var computedValue: TValue; 6 | 7 | start state Init { 8 | entry (config: (promiseId: int, promise: Promise)) { 9 | promiseId = config.promiseId; 10 | resultHolder = config.promise; 11 | goto Computing; 12 | } 13 | } 14 | 15 | state Computing { 16 | entry { 17 | var outcome: int; 18 | 19 | // Non-determinism: we could either succeed, encounter an error (and reject the promise), or just crash 20 | outcome = choose(3); 21 | if (outcome == 0) { 22 | computedValue = ""; 23 | goto Completed; 24 | } else if (outcome == 1) { 25 | goto Error; 26 | } else { 27 | goto Failure; 28 | } 29 | } 30 | } 31 | 32 | cold state Completed { 33 | entry { 34 | send resultHolder, ResolvePromiseRequest, (id = promiseId, value = computedValue, worker = this); 35 | } 36 | } 37 | 38 | cold state Error { 39 | entry { 40 | send resultHolder, RejectPromiseRequest, (id = promiseId, worker = this); 41 | } 42 | } 43 | 44 | cold state Failure { 45 | // Crashed state 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /PTst/TestDriver.p: -------------------------------------------------------------------------------- 1 | machine TestWithSinglePromise 2 | { 3 | start state Init { 4 | entry { 5 | SetupPromiseWorkers(1); 6 | } 7 | } 8 | } 9 | 10 | fun SetupPromiseWorkers(numWorkers: int) { 11 | var i: int; 12 | var promise: Promise; 13 | var worker: Worker; 14 | var timer: Timer; 15 | 16 | i = 1; 17 | while (i <= numWorkers) { 18 | promise = new Promise(i); 19 | announce PromiseInit, (id = i,); 20 | worker = new Worker((promiseId = i, promise = promise)); 21 | timer = CreateTimer(promise); 22 | StartTimer(timer); 23 | i = i + 1; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /PTst/TestScript.p: -------------------------------------------------------------------------------- 1 | test SinglePromise [main=TestWithSinglePromise]: 2 | assert PromiseResultNeverChangesOnceSet, PendingPromisesAreEventuallySettled in 3 | (union Promise, Worker, Client, Timer, { TestWithSinglePromise }); -------------------------------------------------------------------------------- /Promises.pproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Promises 4 | 5 | ./PSrc/ 6 | ./PSpec/ 7 | ./PTst/ 8 | 9 | ./PGenerated/ 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # p-promises 2 | 3 | A [P](https://p-org.github.io/P) model of [Durable Promises](https://github.com/resonatehq/durable-promise). 4 | 5 | **Usage:** 6 | 7 | - [Install P](https://p-org.github.io/P/getstarted/install/) 8 | - Check a test scenario: 9 | ``` 10 | p compile 11 | p check -tc SinglePromise 12 | ``` 13 | --------------------------------------------------------------------------------