package affect

  1. Overview
  2. Docs
Streamlined and natural concurrency model for OCaml

Install

dune-project
 Dependency

Authors

Maintainers

Sources

affect-0.0.0.tbz
sha512=b328f6696e60c489da8cc2e8fad0537ca6634a39fc0c5de5840d4f98561f110617ef79ba8285ee8427dd99908c6b3d39114973598cddc5ded91abcce3b91b9a6

doc/src/affect.unix/affect_unix__select.ml.html

Source file affect_unix__select.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
138
139
140
141
142
143
144
145
146
(*---------------------------------------------------------------------------
   Copyright (c) 2026 The affect programmers. All rights reserved.
   SPDX-License-Identifier: ISC
  ---------------------------------------------------------------------------*)

open Affect__base
open Affect.Action.Private
open Affect_unix__fd

(* Note this is a toy, it might work on Windows though. *)

type blocked = Action.Blocked.Value.t
type blocked_state = { readable : blocked list; writable : blocked list; }

let unblock_fd rset wset fd st =
  let readable =
    if List.mem fd rset
    then (List.iter Action.Blocked.Value.synced_unblock st.readable; [])
    else List.filter Action.Blocked.Value.is_not_synced st.readable (* gc *)
  in
  let writable =
    if List.mem fd wset
    then (List.iter Action.Blocked.Value.synced_unblock st.writable; [])
    else List.filter Action.Blocked.Value.is_not_synced st.writable (* gc *)
  in
  if List.is_empty readable && List.is_empty writable then None else
  Some { readable; writable }

type t = { fds : blocked_state Fd.Synchronized_map.t; block_bypass : Flagfd.t }

let make () =
  let fds = Fd.Synchronized_map.make () and block_bypass = Flagfd.make () in
  { fds; block_bypass }

let dispose u = Flagfd.dispose u.block_bypass

let gc_fd fd st =
  let is_not_synced = Action.Blocked.Value.is_not_synced in
  let readable = List.filter is_not_synced st.readable in
  let writable = List.filter is_not_synced st.writable in
  if List.is_empty readable && List.is_empty writable then None else
  Some { readable; writable }

let gc_ebadf fd st =
  (* There's two reasons why we may get an EBADF.
     1. We were part of a choice that we did not win and that ended up with
        the fd being closed. In this case we don't error but simply prune
        out the synced blocks with [gc_fd]. This could be checked when we
        call [fd_sets], but we try optimistcally.
     2. There is a use after close error, in this case we try to throw an
        error in the blocked value if it hasn't synced yet. *)
  match gc_fd fd st with
  | None -> None
  | Some st as keep ->
      try Unix.set_nonblock fd (* hack to find closed fd *); keep with
      | Unix.Unix_error (EBADF, _, _) ->
          let bt = Printexc.get_raw_backtrace () in
          let exn op = Unix.Unix_error (EBADF, op, "") in
          let unblock exn (Action.Blocked.Value.V (_, b)) =
            let candidate = Action.Result.Exn (exn, bt) in
            Action.Blocked.synced_unblock ~candidate b
          in
          List.iter (unblock (exn "Unix.wait_readable")) st.readable;
          List.iter (unblock (exn "Unix.wait_writable")) st.writable;
          None

let gc_synced u = Fd.Synchronized_map.update_all gc_fd u.fds
let gc_ebadf u = Fd.Synchronized_map.update_all gc_ebadf u.fds
let is_empty u = gc_synced u; Fd.Synchronized_map.is_empty u.fds
let fd_sets u =
  let add fd st (rset, wset) =
    let rset = if List.is_empty st.readable then rset else fd :: rset in
    let wset = if List.is_empty st.writable then wset else fd :: wset in
    (rset, wset)
  in
  Fd.Synchronized_map.fold add u.fds ([], [])

let add_wait_readable u fd ~blocked =
  let add_read r = function
  | None -> Some { readable = [r]; writable = [] }
  | Some st -> Some { st with readable = r :: st.readable }
  in
  Fd.Synchronized_map.update fd (add_read blocked) u.fds

let add_wait_writable u fd ~blocked =
  let add_write w = function
  | None -> Some { readable = []; writable = [w] }
  | Some st -> Some { st with writable = w :: st.writable }
  in
  Fd.Synchronized_map.update fd (add_write blocked) u.fds

let rec unblock u ~timeout_ns =
  let timeout_s = match timeout_ns with
  | None -> -1.
  | Some dur_s -> Affect_unix__timeline.mtime_span_ns_to_float_s dur_s
  in
  let blocking = timeout_s <> 0. in
  let rset, wset = fd_sets u in
  let rset = Affect_unix__signal.syscall_block_bypass_fd :: rset in
  let rset = if blocking then Flagfd.fd u.block_bypass :: rset else rset in
  let did_unblock = match Unix.select rset wset [] timeout_s with
  | exception Unix.Unix_error (EBADF, _, _) ->
      gc_ebadf u; unblock u ~timeout_ns
  | exception Unix.Unix_error (EINTR, _, _) ->
      Affect_unix__signal.clear_syscall_block_bypass ();
      if blocking then Flagfd.clear u.block_bypass;
      false
  | rset, wset, _eset ->
      Affect_unix__signal.clear_syscall_block_bypass ();
      if blocking then Flagfd.clear u.block_bypass;
      (* XXX Except for performance there is no harm in doing the operation
         more than once if there is an atomic retry. But we should try to
         eventually avoid this. A first step would perhaps to do it atomically
         on each fd with an iter, we don't really need the whole batch to be
         consistent. It's still a good idea to go over all of them for
         gc, though we could move that to [fd_sets]. *)
      Fd.Synchronized_map.update_all (unblock_fd rset wset) u.fds;
      not (List.is_empty rset) || not (List.is_empty wset)
      (* XXX formally we may not have unblocked anything because we are not
         using [sync_unblock_is_ours] in [blocked_synced_unblock] or rather
         it's complicated given our simplistic datastructures to get it out.
         So we just use this which is good enough, there may be false
         positives (also the signal and block bypass may show up) but it's
         harmless. *)
  in
  did_unblock

let set_block_bypass u = Flagfd.set u.block_bypass

(* Domain local fd unblocker

   Each domain may know about an unblocker via domain local storage.
   This allows APIs calls in Unix to register readability and writability
   request on the appropriate unblocker. Note that the current scheme
   could experience high-contention, one idea would be to have
   one synchronized map per domain and the domain that calls [unblocks]
   merges them before calling select. *)

let nil = let nil = make () in dispose nil; nil
let err_unblocker_not_set () = invalid_arg "Fd unblocker not set"
let key : t Domain.DLS.key = Domain.DLS.new_key (fun () -> nil)
let set_domain_local u = Domain.DLS.set key u
let clear_domain_local () = set_domain_local nil
let get_domain_local () =
  let u = Domain.DLS.get key in
  if Repr.phys_equal u nil then err_unblocker_not_set () else u