aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Lib/test/support/interpreters/queues.py
blob: deb8e8613af731b476cdfa59bb9c41c2b75b9e0d (plain) (blame)
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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
"""Cross-interpreter Queues High Level Module."""

import pickle
import queue
import time
import weakref
import _interpqueues as _queues
from . import _crossinterp

# aliases:
from _interpqueues import (
    QueueError, QueueNotFoundError,
)
from ._crossinterp import (
    UNBOUND_ERROR, UNBOUND_REMOVE,
)

__all__ = [
    'UNBOUND', 'UNBOUND_ERROR', 'UNBOUND_REMOVE',
    'create', 'list_all',
    'Queue',
    'QueueError', 'QueueNotFoundError', 'QueueEmpty', 'QueueFull',
    'ItemInterpreterDestroyed',
]


class QueueEmpty(QueueError, queue.Empty):
    """Raised from get_nowait() when the queue is empty.

    It is also raised from get() if it times out.
    """


class QueueFull(QueueError, queue.Full):
    """Raised from put_nowait() when the queue is full.

    It is also raised from put() if it times out.
    """


class ItemInterpreterDestroyed(QueueError,
                               _crossinterp.ItemInterpreterDestroyed):
    """Raised from get() and get_nowait()."""


_SHARED_ONLY = 0
_PICKLED = 1


UNBOUND = _crossinterp.UnboundItem.singleton('queue', __name__)


def _serialize_unbound(unbound):
    if unbound is UNBOUND:
        unbound = _crossinterp.UNBOUND
    return _crossinterp.serialize_unbound(unbound)


def _resolve_unbound(flag):
    resolved = _crossinterp.resolve_unbound(flag, ItemInterpreterDestroyed)
    if resolved is _crossinterp.UNBOUND:
        resolved = UNBOUND
    return resolved


def create(maxsize=0, *, syncobj=False, unbounditems=UNBOUND):
    """Return a new cross-interpreter queue.

    The queue may be used to pass data safely between interpreters.

    "syncobj" sets the default for Queue.put()
    and Queue.put_nowait().

    "unbounditems" likewise sets the default.  See Queue.put() for
    supported values.  The default value is UNBOUND, which replaces
    the unbound item.
    """
    fmt = _SHARED_ONLY if syncobj else _PICKLED
    unbound = _serialize_unbound(unbounditems)
    unboundop, = unbound
    qid = _queues.create(maxsize, fmt, unboundop)
    return Queue(qid, _fmt=fmt, _unbound=unbound)


def list_all():
    """Return a list of all open queues."""
    return [Queue(qid, _fmt=fmt, _unbound=(unboundop,))
            for qid, fmt, unboundop in _queues.list_all()]


_known_queues = weakref.WeakValueDictionary()

class Queue:
    """A cross-interpreter queue."""

    def __new__(cls, id, /, *, _fmt=None, _unbound=None):
        # There is only one instance for any given ID.
        if isinstance(id, int):
            id = int(id)
        else:
            raise TypeError(f'id must be an int, got {id!r}')
        if _fmt is None:
            if _unbound is None:
                _fmt, op = _queues.get_queue_defaults(id)
                _unbound = (op,)
            else:
                _fmt, _ = _queues.get_queue_defaults(id)
        elif _unbound is None:
            _, op = _queues.get_queue_defaults(id)
            _unbound = (op,)
        try:
            self = _known_queues[id]
        except KeyError:
            self = super().__new__(cls)
            self._id = id
            self._fmt = _fmt
            self._unbound = _unbound
            _known_queues[id] = self
            _queues.bind(id)
        return self

    def __del__(self):
        try:
            _queues.release(self._id)
        except QueueNotFoundError:
            pass
        try:
            del _known_queues[self._id]
        except KeyError:
            pass

    def __repr__(self):
        return f'{type(self).__name__}({self.id})'

    def __hash__(self):
        return hash(self._id)

    # for pickling:
    def __getnewargs__(self):
        return (self._id,)

    # for pickling:
    def __getstate__(self):
        return None

    @property
    def id(self):
        return self._id

    @property
    def maxsize(self):
        try:
            return self._maxsize
        except AttributeError:
            self._maxsize = _queues.get_maxsize(self._id)
            return self._maxsize

    def empty(self):
        return self.qsize() == 0

    def full(self):
        return _queues.is_full(self._id)

    def qsize(self):
        return _queues.get_count(self._id)

    def put(self, obj, timeout=None, *,
            syncobj=None,
            unbound=None,
            _delay=10 / 1000,  # 10 milliseconds
            ):
        """Add the object to the queue.

        This blocks while the queue is full.

        If "syncobj" is None (the default) then it uses the
        queue's default, set with create_queue().

        If "syncobj" is false then all objects are supported,
        at the expense of worse performance.

        If "syncobj" is true then the object must be "shareable".
        Examples of "shareable" objects include the builtin singletons,
        str, and memoryview.  One benefit is that such objects are
        passed through the queue efficiently.

        The key difference, though, is conceptual: the corresponding
        object returned from Queue.get() will be strictly equivalent
        to the given obj.  In other words, the two objects will be
        effectively indistinguishable from each other, even if the
        object is mutable.  The received object may actually be the
        same object, or a copy (immutable values only), or a proxy.
        Regardless, the received object should be treated as though
        the original has been shared directly, whether or not it
        actually is.  That's a slightly different and stronger promise
        than just (initial) equality, which is all "syncobj=False"
        can promise.

        "unbound" controls the behavior of Queue.get() for the given
        object if the current interpreter (calling put()) is later
        destroyed.

        If "unbound" is None (the default) then it uses the
        queue's default, set with create_queue(),
        which is usually UNBOUND.

        If "unbound" is UNBOUND_ERROR then get() will raise an
        ItemInterpreterDestroyed exception if the original interpreter
        has been destroyed.  This does not otherwise affect the queue;
        the next call to put() will work like normal, returning the next
        item in the queue.

        If "unbound" is UNBOUND_REMOVE then the item will be removed
        from the queue as soon as the original interpreter is destroyed.
        Be aware that this will introduce an imbalance between put()
        and get() calls.

        If "unbound" is UNBOUND then it is returned by get() in place
        of the unbound item.
        """
        if syncobj is None:
            fmt = self._fmt
        else:
            fmt = _SHARED_ONLY if syncobj else _PICKLED
        if unbound is None:
            unboundop, = self._unbound
        else:
            unboundop, = _serialize_unbound(unbound)
        if timeout is not None:
            timeout = int(timeout)
            if timeout < 0:
                raise ValueError(f'timeout value must be non-negative')
            end = time.time() + timeout
        if fmt is _PICKLED:
            obj = pickle.dumps(obj)
        while True:
            try:
                _queues.put(self._id, obj, fmt, unboundop)
            except QueueFull as exc:
                if timeout is not None and time.time() >= end:
                    raise  # re-raise
                time.sleep(_delay)
            else:
                break

    def put_nowait(self, obj, *, syncobj=None, unbound=None):
        if syncobj is None:
            fmt = self._fmt
        else:
            fmt = _SHARED_ONLY if syncobj else _PICKLED
        if unbound is None:
            unboundop, = self._unbound
        else:
            unboundop, = _serialize_unbound(unbound)
        if fmt is _PICKLED:
            obj = pickle.dumps(obj)
        _queues.put(self._id, obj, fmt, unboundop)

    def get(self, timeout=None, *,
            _delay=10 / 1000,  # 10 milliseconds
            ):
        """Return the next object from the queue.

        This blocks while the queue is empty.

        If the next item's original interpreter has been destroyed
        then the "next object" is determined by the value of the
        "unbound" argument to put().
        """
        if timeout is not None:
            timeout = int(timeout)
            if timeout < 0:
                raise ValueError(f'timeout value must be non-negative')
            end = time.time() + timeout
        while True:
            try:
                obj, fmt, unboundop = _queues.get(self._id)
            except QueueEmpty as exc:
                if timeout is not None and time.time() >= end:
                    raise  # re-raise
                time.sleep(_delay)
            else:
                break
        if unboundop is not None:
            assert obj is None, repr(obj)
            return _resolve_unbound(unboundop)
        if fmt == _PICKLED:
            obj = pickle.loads(obj)
        else:
            assert fmt == _SHARED_ONLY
        return obj

    def get_nowait(self):
        """Return the next object from the channel.

        If the queue is empty then raise QueueEmpty.  Otherwise this
        is the same as get().
        """
        try:
            obj, fmt, unboundop = _queues.get(self._id)
        except QueueEmpty as exc:
            raise  # re-raise
        if unboundop is not None:
            assert obj is None, repr(obj)
            return _resolve_unbound(unboundop)
        if fmt == _PICKLED:
            obj = pickle.loads(obj)
        else:
            assert fmt == _SHARED_ONLY
        return obj


_queues._register_heap_types(Queue, QueueEmpty, QueueFull)