package affect

  1. Overview
  2. Docs

Source file affect__port.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
(*---------------------------------------------------------------------------
   Copyright (c) 2026 The affect programmers. All rights reserved.
   SPDX-License-Identifier: ISC
  ---------------------------------------------------------------------------*)

open Affect__base
open Affect__action.Private

(* The Parallel concurrent ML paper <https://doi.org/10.1145/1596550.1596588>
   is useful to understand better what is going on here.

   This implementation differs though. We follow the guile implementation
   which uses two atomics on queues instead of a spinlock to synchronize
   access to the two queues at the same time. This means that in the
   blocking phase we need to publish operations on the queues before we try
   to synchronize and we must peek the queues, not take and possibly untake,
   otherwise we may end up in states where matching actions end up
   missing each other.

   This should perhaps still be benchmarked one day w.r.t. spin lock protected
   queues. *)

type 'a blocked_offer = 'a * Action.Blocked.Value.t
type 'a blocked_take = 'a Action.Blocked.t
type 'a t =
  { offers : 'a blocked_offer Synchronized_queue.t;
    takes : 'a blocked_take Synchronized_queue.t; }

let make () =
  { offers = Synchronized_queue.make ();
    takes = Synchronized_queue.make (); }

let discard v ~seen q = (* We don't care if the state changed *)
  ignore (Synchronized_queue.try_drop' ~sat:(Repr.phys_equal v) ~seen q)

(* offer *)

let rec offer_poll p v tag ~continue =
  match Synchronized_queue.take p.takes with
  | None -> None
  | Some take ->
      let candidate = Action.Result.Value v in
      if Action.Blocked.synced_unblock_is_ours ~candidate take
      then Some (continue (Action.Result.Value tag))
      else offer_poll p v tag ~continue

let offer_block p v tag ~blocked:offer =
  let offer = Action.Blocked.Value.make tag offer in
  let and_keep (_, b) = Action.Blocked.Value.is_not_synced b in
  Synchronized_queue.add' (v, offer) ~and_keep p.offers;
  let rec loop p v offer =
    let is_not_me = Action.Blocked.Value.is_not offer in
    match Synchronized_queue.peek' ~sat:is_not_me p.takes with
    | None, _seen -> ()
    | Some take, seen ->
        if Action.Blocked.is_synced take
        then (discard take ~seen p.takes; loop p v offer) else
        let rec try_this_take ~backoff p v offer take seen =
          match Action.Blocked.Value.exchange_waiting_to_claimed offer with
          | Claimed -> assert false
          | Synced -> ()
          | Waiting ->
              match Action.Blocked.exchange_waiting_to_synced take with
              | Waiting ->
                  discard take ~seen p.takes;
                  Action.Blocked.Value.set_state offer Synced;
                  Action.Blocked.tricky_unblock take (Action.Result.Value v);
                  Action.Blocked.Value.tricky_unblock offer;
              | Synced ->
                  discard take ~seen p.takes;
                  Action.Blocked.Value.set_state offer Waiting;
                  loop p v offer
              | Claimed ->
                  Action.Blocked.Value.set_state offer Waiting;
                  try_this_take ~backoff:(Atomic.Backoff.once backoff)
                    p v offer take seen
        in
        try_this_take ~backoff:Atomic.Backoff.default p v offer take seen
  in
  loop p v offer

let offer_meta = Action.Meta.make ~name:"Port.offer" ()
let offer' p v tag =
  let poll = offer_poll p v tag and block = offer_block p v tag in
  Action.Primitive.make ~meta:offer_meta ~poll ~block

let offer p v = Action.invoke (offer' p v ())

(* take *)

let rec take_poll p ~continue = match Synchronized_queue.take p.offers with
| None -> None
| Some (v, offer) ->
    if Action.Blocked.Value.synced_unblock_is_ours offer
    then Some (continue (Action.Result.Value v))
    else take_poll p ~continue

let take_block p ~blocked:take =
  let and_keep = Action.Blocked.is_not_synced in
  Synchronized_queue.add' take ~and_keep p.takes;
  let rec loop p take =
    let is_not_me (_, b) = Action.Blocked.Value.is_not b take in
    match Synchronized_queue.peek' ~sat:is_not_me p.offers with
    | None, _seen -> ()
    | Some (v, offer as o), seen ->
        if Action.Blocked.Value.is_synced offer
        then (discard o ~seen p.offers; loop p take) else
        let rec try_this_offer ~backoff p take ((v, offer) as o) seen =
          match Action.Blocked.exchange_waiting_to_claimed take with
          | Claimed -> assert false
          | Synced -> ()
          | Waiting ->
              match Action.Blocked.Value.exchange_waiting_to_synced offer with
              | Waiting ->
                  discard o ~seen p.offers;
                  Action.Blocked.set_state take Synced;
                  Action.Blocked.Value.tricky_unblock offer;
                  Action.Blocked.tricky_unblock take (Action.Result.Value v)
              | Synced ->
                  discard o ~seen p.offers;
                  Action.Blocked.set_state take Waiting;
                  loop p take
              | Claimed ->
                  Action.Blocked.set_state take Waiting;
                  try_this_offer ~backoff:(Atomic.Backoff.once backoff)
                    p take o seen
        in
        try_this_offer ~backoff:Atomic.Backoff.default p take o seen
  in
  loop p take

let take_meta = Action.Meta.make ~name:"Port.take" ()
let take' p =
  let poll = take_poll p and block = take_block p in
  Action.Primitive.make ~meta:take_meta ~poll ~block

let take p = Action.invoke (take' p)