From 78c878baaace21c75aa5a66badaf830d5acc371e Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 29 Nov 2023 12:36:06 -0700 Subject: [PATCH 01/14] Add the _xxinterpqueues module. --- Modules/Setup | 1 + Modules/Setup.stdlib.in | 3 + Modules/_xxinterpqueuesmodule.c | 3549 +++++++++++++++++++ PC/config.c | 2 + PCbuild/pythoncore.vcxproj | 1 + PCbuild/pythoncore.vcxproj.filters | 3 + Tools/build/generate_stdlib_module_names.py | 1 + configure | 29 + configure.ac | 2 + 9 files changed, 3591 insertions(+) create mode 100644 Modules/_xxinterpqueuesmodule.c diff --git a/Modules/Setup b/Modules/Setup index 1367f0ef4fa54a..8ad9a5aebbfcaa 100644 --- a/Modules/Setup +++ b/Modules/Setup @@ -273,6 +273,7 @@ PYTHONPATH=$(COREPYTHONPATH) #_xxsubinterpreters _xxsubinterpretersmodule.c #_xxinterpchannels _xxinterpchannelsmodule.c +#_xxinterpqueues _xxinterpqueuesmodule.c #_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c #_testbuffer _testbuffer.c #_testinternalcapi _testinternalcapi.c diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 54650ea9c1d4ac..8a65a9cffb1b9d 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -41,8 +41,11 @@ @MODULE__QUEUE_TRUE@_queue _queuemodule.c @MODULE__RANDOM_TRUE@_random _randommodule.c @MODULE__STRUCT_TRUE@_struct _struct.c + +# build supports subinterpreters @MODULE__XXSUBINTERPRETERS_TRUE@_xxsubinterpreters _xxsubinterpretersmodule.c @MODULE__XXINTERPCHANNELS_TRUE@_xxinterpchannels _xxinterpchannelsmodule.c +@MODULE__XXINTERPQUEUES_TRUE@_xxinterpqueues _xxinterpqueuesmodule.c @MODULE__ZONEINFO_TRUE@_zoneinfo _zoneinfo.c # needs libm diff --git a/Modules/_xxinterpqueuesmodule.c b/Modules/_xxinterpqueuesmodule.c new file mode 100644 index 00000000000000..b95de5dd05a72a --- /dev/null +++ b/Modules/_xxinterpqueuesmodule.c @@ -0,0 +1,3549 @@ +/* interpreters module */ +/* low-level access to interpreter primitives */ + +#ifndef Py_BUILD_CORE_BUILTIN +# define Py_BUILD_CORE_MODULE 1 +#endif + +#include "Python.h" +#include "interpreteridobject.h" +#include "pycore_crossinterp.h" // struct _xid +#include "pycore_pybuffer.h" // _PyBuffer_ReleaseInInterpreterAndRawFree() +#include "pycore_interp.h" // _PyInterpreterState_LookUpID() + +#ifdef MS_WINDOWS +#define WIN32_LEAN_AND_MEAN +#include // SwitchToThread() +#elif defined(HAVE_SCHED_H) +#include // sched_yield() +#endif + + +/* +This module has the following process-global state: + +_globals (static struct globals): + module_count (int) + channels (struct _channels): + numopen (int64_t) + next_id; (int64_t) + mutex (PyThread_type_lock) + head (linked list of struct _channelref *): + cid (int64_t) + objcount (Py_ssize_t) + next (struct _channelref *): + ... + chan (struct _channel *): + open (int) + mutex (PyThread_type_lock) + closing (struct _channel_closing *): + ref (struct _channelref *): + ... + ends (struct _channelends *): + numsendopen (int64_t) + numrecvopen (int64_t) + send (struct _channelend *): + interpid (int64_t) + open (int) + next (struct _channelend *) + recv (struct _channelend *): + ... + queue (struct _channelqueue *): + count (int64_t) + first (struct _channelitem *): + next (struct _channelitem *): + ... + data (_PyCrossInterpreterData *): + data (void *) + obj (PyObject *) + interpid (int64_t) + new_object (xid_newobjectfunc) + free (xid_freefunc) + last (struct _channelitem *): + ... + +The above state includes the following allocations by the module: + +* 1 top-level mutex (to protect the rest of the state) +* for each channel: + * 1 struct _channelref + * 1 struct _channel + * 0-1 struct _channel_closing + * 1 struct _channelends + * 2 struct _channelend + * 1 struct _channelqueue +* for each item in each channel: + * 1 struct _channelitem + * 1 _PyCrossInterpreterData + +The only objects in that global state are the references held by each +channel's queue, which are safely managed via the _PyCrossInterpreterData_*() +API.. The module does not create any objects that are shared globally. +*/ + +#define MODULE_NAME "_xxinterpqueues" + + +#define GLOBAL_MALLOC(TYPE) \ + PyMem_RawMalloc(sizeof(TYPE)) +#define GLOBAL_FREE(VAR) \ + PyMem_RawFree(VAR) + + +struct xid_class_registry { + size_t count; +#define MAX_XID_CLASSES 5 + struct { + PyTypeObject *cls; + } added[MAX_XID_CLASSES]; +}; + +static int +register_xid_class(PyTypeObject *cls, crossinterpdatafunc shared, + struct xid_class_registry *classes) +{ + return 0; + int res = _PyCrossInterpreterData_RegisterClass(cls, shared); + if (res == 0) { + assert(classes->count < MAX_XID_CLASSES); + // The class has refs elsewhere, so we need to incref here. + classes->added[classes->count].cls = cls; + classes->count += 1; + } + return res; +} + +static void +clear_xid_class_registry(struct xid_class_registry *classes) +{ + while (classes->count > 0) { + classes->count -= 1; + PyTypeObject *cls = classes->added[classes->count].cls; + _PyCrossInterpreterData_UnregisterClass(cls); + } +} + +#define XID_IGNORE_EXC 1 +#define XID_FREE 2 + +static int +_release_xid_data(_PyCrossInterpreterData *data, int flags) +{ + int ignoreexc = flags & XID_IGNORE_EXC; + PyObject *exc; + if (ignoreexc) { + exc = PyErr_GetRaisedException(); + } + int res; + if (flags & XID_FREE) { + res = _PyCrossInterpreterData_ReleaseAndRawFree(data); + } + else { + res = _PyCrossInterpreterData_Release(data); + } + if (res < 0) { + /* The owning interpreter is already destroyed. */ + if (ignoreexc) { + // XXX Emit a warning? + PyErr_Clear(); + } + } + if (flags & XID_FREE) { + /* Either way, we free the data. */ + } + if (ignoreexc) { + PyErr_SetRaisedException(exc); + } + return res; +} + + +static PyInterpreterState * +_get_current_interp(void) +{ + // PyInterpreterState_Get() aborts if lookup fails, so don't need + // to check the result for NULL. + return PyInterpreterState_Get(); +} + +static PyObject * +_get_current_module(void) +{ + PyObject *name = PyUnicode_FromString(MODULE_NAME); + if (name == NULL) { + return NULL; + } + PyObject *mod = PyImport_GetModule(name); + Py_DECREF(name); + if (mod == NULL) { + return NULL; + } + assert(mod != Py_None); + return mod; +} + +static PyObject * +get_module_from_owned_type(PyTypeObject *cls) +{ + assert(cls != NULL); + return _get_current_module(); + // XXX Use the more efficient API now that we use heap types: + //return PyType_GetModule(cls); +} + +static struct PyModuleDef moduledef; + +static PyObject * +get_module_from_type(PyTypeObject *cls) +{ + assert(cls != NULL); + return _get_current_module(); + // XXX Use the more efficient API now that we use heap types: + //return PyType_GetModuleByDef(cls, &moduledef); +} + +static PyObject * +add_new_exception(PyObject *mod, const char *name, PyObject *base) +{ + assert(!PyObject_HasAttrStringWithError(mod, name)); + PyObject *exctype = PyErr_NewException(name, base, NULL); + if (exctype == NULL) { + return NULL; + } + int res = PyModule_AddType(mod, (PyTypeObject *)exctype); + if (res < 0) { + Py_DECREF(exctype); + return NULL; + } + return exctype; +} + +#define ADD_NEW_EXCEPTION(MOD, NAME, BASE) \ + add_new_exception(MOD, MODULE_NAME "." Py_STRINGIFY(NAME), BASE) + +static PyTypeObject * +add_new_type(PyObject *mod, PyType_Spec *spec, crossinterpdatafunc shared, + struct xid_class_registry *classes) +{ + PyTypeObject *cls = (PyTypeObject *)PyType_FromModuleAndSpec( + mod, spec, NULL); + if (cls == NULL) { + return NULL; + } + if (PyModule_AddType(mod, cls) < 0) { + Py_DECREF(cls); + return NULL; + } + if (shared != NULL) { + if (register_xid_class(cls, shared, classes)) { + Py_DECREF(cls); + return NULL; + } + } + return cls; +} + +static int +wait_for_lock(PyThread_type_lock mutex, PY_TIMEOUT_T timeout) +{ + PyLockStatus res = PyThread_acquire_lock_timed_with_retries(mutex, timeout); + if (res == PY_LOCK_INTR) { + /* KeyboardInterrupt, etc. */ + assert(PyErr_Occurred()); + return -1; + } + else if (res == PY_LOCK_FAILURE) { + assert(!PyErr_Occurred()); + assert(timeout > 0); + PyErr_SetString(PyExc_TimeoutError, "timed out"); + return -1; + } + assert(res == PY_LOCK_ACQUIRED); + PyThread_release_lock(mutex); + return 0; +} + + +/* Cross-interpreter Buffer Views *******************************************/ + +// XXX Release when the original interpreter is destroyed. + +typedef struct { + PyObject_HEAD + Py_buffer *view; + int64_t interpid; +} XIBufferViewObject; + +static PyObject * +xibufferview_from_xid(PyTypeObject *cls, _PyCrossInterpreterData *data) +{ + assert(data->data != NULL); + assert(data->obj == NULL); + assert(data->interpid >= 0); + XIBufferViewObject *self = PyObject_Malloc(sizeof(XIBufferViewObject)); + if (self == NULL) { + return NULL; + } + PyObject_Init((PyObject *)self, cls); + self->view = (Py_buffer *)data->data; + self->interpid = data->interpid; + return (PyObject *)self; +} + +static void +xibufferview_dealloc(XIBufferViewObject *self) +{ + PyInterpreterState *interp = _PyInterpreterState_LookUpID(self->interpid); + /* If the interpreter is no longer alive then we have problems, + since other objects may be using the buffer still. */ + assert(interp != NULL); + + if (_PyBuffer_ReleaseInInterpreterAndRawFree(interp, self->view) < 0) { + // XXX Emit a warning? + PyErr_Clear(); + } + + PyTypeObject *tp = Py_TYPE(self); + tp->tp_free(self); + /* "Instances of heap-allocated types hold a reference to their type." + * See: https://docs.python.org/3.11/howto/isolating-extensions.html#garbage-collection-protocol + * See: https://docs.python.org/3.11/c-api/typeobj.html#c.PyTypeObject.tp_traverse + */ + // XXX Why don't we implement Py_TPFLAGS_HAVE_GC, e.g. Py_tp_traverse, + // like we do for _abc._abc_data? + Py_DECREF(tp); +} + +static int +xibufferview_getbuf(XIBufferViewObject *self, Py_buffer *view, int flags) +{ + /* Only PyMemoryView_FromObject() should ever call this, + via _memoryview_from_xid() below. */ + *view = *self->view; + view->obj = (PyObject *)self; + // XXX Should we leave it alone? + view->internal = NULL; + return 0; +} + +static PyType_Slot XIBufferViewType_slots[] = { + {Py_tp_dealloc, (destructor)xibufferview_dealloc}, + {Py_bf_getbuffer, (getbufferproc)xibufferview_getbuf}, + // We don't bother with Py_bf_releasebuffer since we don't need it. + {0, NULL}, +}; + +static PyType_Spec XIBufferViewType_spec = { + .name = MODULE_NAME ".CrossInterpreterBufferView", + .basicsize = sizeof(XIBufferViewObject), + .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | + Py_TPFLAGS_DISALLOW_INSTANTIATION | Py_TPFLAGS_IMMUTABLETYPE), + .slots = XIBufferViewType_slots, +}; + + +/* extra XID types **********************************************************/ + +static PyTypeObject * _get_current_xibufferview_type(void); + +static PyObject * +_memoryview_from_xid(_PyCrossInterpreterData *data) +{ + PyTypeObject *cls = _get_current_xibufferview_type(); + if (cls == NULL) { + return NULL; + } + PyObject *obj = xibufferview_from_xid(cls, data); + if (obj == NULL) { + return NULL; + } + return PyMemoryView_FromObject(obj); +} + +static int +_memoryview_shared(PyThreadState *tstate, PyObject *obj, + _PyCrossInterpreterData *data) +{ + Py_buffer *view = PyMem_RawMalloc(sizeof(Py_buffer)); + if (view == NULL) { + return -1; + } + if (PyObject_GetBuffer(obj, view, PyBUF_FULL_RO) < 0) { + PyMem_RawFree(view); + return -1; + } + _PyCrossInterpreterData_Init(data, tstate->interp, view, NULL, + _memoryview_from_xid); + return 0; +} + +static int +register_builtin_xid_types(struct xid_class_registry *classes) +{ + PyTypeObject *cls; + crossinterpdatafunc func; + + // builtin memoryview + cls = &PyMemoryView_Type; + func = _memoryview_shared; + if (register_xid_class(cls, func, classes)) { + return -1; + } + + return 0; +} + + +/* module state *************************************************************/ + +typedef struct { + struct xid_class_registry xid_classes; + + /* Added at runtime by interpreters module. */ + PyTypeObject *send_channel_type; + PyTypeObject *recv_channel_type; + + /* heap types */ + PyTypeObject *ChannelInfoType; + PyTypeObject *QueueIDType; + PyTypeObject *XIBufferViewType; + + /* exceptions */ + PyObject *ChannelError; + PyObject *ChannelNotFoundError; + PyObject *ChannelClosedError; + PyObject *ChannelEmptyError; + PyObject *ChannelNotEmptyError; +} module_state; + +static inline module_state * +get_module_state(PyObject *mod) +{ + assert(mod != NULL); + module_state *state = PyModule_GetState(mod); + assert(state != NULL); + return state; +} + +static module_state * +_get_current_module_state(void) +{ + PyObject *mod = _get_current_module(); + if (mod == NULL) { + // XXX import it? + PyErr_SetString(PyExc_RuntimeError, + MODULE_NAME " module not imported yet"); + return NULL; + } + module_state *state = get_module_state(mod); + Py_DECREF(mod); + return state; +} + +static int +traverse_module_state(module_state *state, visitproc visit, void *arg) +{ + /* external types */ + Py_VISIT(state->send_channel_type); + Py_VISIT(state->recv_channel_type); + + /* heap types */ + Py_VISIT(state->ChannelInfoType); + Py_VISIT(state->QueueIDType); + Py_VISIT(state->XIBufferViewType); + + /* exceptions */ + Py_VISIT(state->ChannelError); + Py_VISIT(state->ChannelNotFoundError); + Py_VISIT(state->ChannelClosedError); + Py_VISIT(state->ChannelEmptyError); + Py_VISIT(state->ChannelNotEmptyError); + + return 0; +} + +static int +clear_module_state(module_state *state) +{ + /* external types */ + Py_CLEAR(state->send_channel_type); + Py_CLEAR(state->recv_channel_type); + + /* heap types */ + Py_CLEAR(state->ChannelInfoType); + if (state->QueueIDType != NULL) { + (void)_PyCrossInterpreterData_UnregisterClass(state->QueueIDType); + } + Py_CLEAR(state->QueueIDType); + Py_CLEAR(state->XIBufferViewType); + + /* exceptions */ + Py_CLEAR(state->ChannelError); + Py_CLEAR(state->ChannelNotFoundError); + Py_CLEAR(state->ChannelClosedError); + Py_CLEAR(state->ChannelEmptyError); + Py_CLEAR(state->ChannelNotEmptyError); + + return 0; +} + + +static PyTypeObject * +_get_current_xibufferview_type(void) +{ + module_state *state = _get_current_module_state(); + if (state == NULL) { + return NULL; + } + return state->XIBufferViewType; +} + + +/* channel-specific code ****************************************************/ + +#define CHANNEL_SEND 1 +#define CHANNEL_BOTH 0 +#define CHANNEL_RECV -1 + + +/* channel errors */ + +#define ERR_CHANNEL_NOT_FOUND -2 +#define ERR_CHANNEL_CLOSED -3 +#define ERR_CHANNEL_INTERP_CLOSED -4 +#define ERR_CHANNEL_EMPTY -5 +#define ERR_CHANNEL_NOT_EMPTY -6 +#define ERR_CHANNEL_MUTEX_INIT -7 +#define ERR_CHANNELS_MUTEX_INIT -8 +#define ERR_NO_NEXT_CHANNEL_ID -9 +#define ERR_CHANNEL_CLOSED_WAITING -10 + +static int +exceptions_init(PyObject *mod) +{ + module_state *state = get_module_state(mod); + if (state == NULL) { + return -1; + } + +#define ADD(NAME, BASE) \ + do { \ + assert(state->NAME == NULL); \ + state->NAME = ADD_NEW_EXCEPTION(mod, NAME, BASE); \ + if (state->NAME == NULL) { \ + return -1; \ + } \ + } while (0) + + // A channel-related operation failed. + ADD(ChannelError, PyExc_RuntimeError); + // An operation tried to use a channel that doesn't exist. + ADD(ChannelNotFoundError, state->ChannelError); + // An operation tried to use a closed channel. + ADD(ChannelClosedError, state->ChannelError); + // An operation tried to pop from an empty channel. + ADD(ChannelEmptyError, state->ChannelError); + // An operation tried to close a non-empty channel. + ADD(ChannelNotEmptyError, state->ChannelError); +#undef ADD + + return 0; +} + +static int +handle_channel_error(int err, PyObject *mod, int64_t cid) +{ + if (err == 0) { + assert(!PyErr_Occurred()); + return 0; + } + assert(err < 0); + module_state *state = get_module_state(mod); + assert(state != NULL); + if (err == ERR_CHANNEL_NOT_FOUND) { + PyErr_Format(state->ChannelNotFoundError, + "channel %" PRId64 " not found", cid); + } + else if (err == ERR_CHANNEL_CLOSED) { + PyErr_Format(state->ChannelClosedError, + "channel %" PRId64 " is closed", cid); + } + else if (err == ERR_CHANNEL_CLOSED_WAITING) { + PyErr_Format(state->ChannelClosedError, + "channel %" PRId64 " has closed", cid); + } + else if (err == ERR_CHANNEL_INTERP_CLOSED) { + PyErr_Format(state->ChannelClosedError, + "channel %" PRId64 " is already closed", cid); + } + else if (err == ERR_CHANNEL_EMPTY) { + PyErr_Format(state->ChannelEmptyError, + "channel %" PRId64 " is empty", cid); + } + else if (err == ERR_CHANNEL_NOT_EMPTY) { + PyErr_Format(state->ChannelNotEmptyError, + "channel %" PRId64 " may not be closed " + "if not empty (try force=True)", + cid); + } + else if (err == ERR_CHANNEL_MUTEX_INIT) { + PyErr_SetString(state->ChannelError, + "can't initialize mutex for new channel"); + } + else if (err == ERR_CHANNELS_MUTEX_INIT) { + PyErr_SetString(state->ChannelError, + "can't initialize mutex for channel management"); + } + else if (err == ERR_NO_NEXT_CHANNEL_ID) { + PyErr_SetString(state->ChannelError, + "failed to get a channel ID"); + } + else { + assert(PyErr_Occurred()); + } + return 1; +} + + +/* the channel queue */ + +typedef uintptr_t _channelitem_id_t; + +typedef struct wait_info { + PyThread_type_lock mutex; + enum { + WAITING_NO_STATUS = 0, + WAITING_ACQUIRED = 1, + WAITING_RELEASING = 2, + WAITING_RELEASED = 3, + } status; + int received; + _channelitem_id_t itemid; +} _waiting_t; + +static int +_waiting_init(_waiting_t *waiting) +{ + PyThread_type_lock mutex = PyThread_allocate_lock(); + if (mutex == NULL) { + PyErr_NoMemory(); + return -1; + } + + *waiting = (_waiting_t){ + .mutex = mutex, + .status = WAITING_NO_STATUS, + }; + return 0; +} + +static void +_waiting_clear(_waiting_t *waiting) +{ + assert(waiting->status != WAITING_ACQUIRED + && waiting->status != WAITING_RELEASING); + if (waiting->mutex != NULL) { + PyThread_free_lock(waiting->mutex); + waiting->mutex = NULL; + } +} + +static _channelitem_id_t +_waiting_get_itemid(_waiting_t *waiting) +{ + return waiting->itemid; +} + +static void +_waiting_acquire(_waiting_t *waiting) +{ + assert(waiting->status == WAITING_NO_STATUS); + PyThread_acquire_lock(waiting->mutex, NOWAIT_LOCK); + waiting->status = WAITING_ACQUIRED; +} + +static void +_waiting_release(_waiting_t *waiting, int received) +{ + assert(waiting->mutex != NULL); + assert(waiting->status == WAITING_ACQUIRED); + assert(!waiting->received); + + waiting->status = WAITING_RELEASING; + PyThread_release_lock(waiting->mutex); + if (waiting->received != received) { + assert(received == 1); + waiting->received = received; + } + waiting->status = WAITING_RELEASED; +} + +static void +_waiting_finish_releasing(_waiting_t *waiting) +{ + while (waiting->status == WAITING_RELEASING) { +#ifdef MS_WINDOWS + SwitchToThread(); +#elif defined(HAVE_SCHED_H) + sched_yield(); +#endif + } +} + +struct _channelitem; + +typedef struct _channelitem { + _PyCrossInterpreterData *data; + _waiting_t *waiting; + struct _channelitem *next; +} _channelitem; + +static inline _channelitem_id_t +_channelitem_ID(_channelitem *item) +{ + return (_channelitem_id_t)item; +} + +static void +_channelitem_init(_channelitem *item, + _PyCrossInterpreterData *data, _waiting_t *waiting) +{ + *item = (_channelitem){ + .data = data, + .waiting = waiting, + }; + if (waiting != NULL) { + waiting->itemid = _channelitem_ID(item); + } +} + +static void +_channelitem_clear(_channelitem *item) +{ + item->next = NULL; + + if (item->data != NULL) { + // It was allocated in channel_send(). + (void)_release_xid_data(item->data, XID_IGNORE_EXC & XID_FREE); + item->data = NULL; + } + + if (item->waiting != NULL) { + if (item->waiting->status == WAITING_ACQUIRED) { + _waiting_release(item->waiting, 0); + } + item->waiting = NULL; + } +} + +static _channelitem * +_channelitem_new(_PyCrossInterpreterData *data, _waiting_t *waiting) +{ + _channelitem *item = GLOBAL_MALLOC(_channelitem); + if (item == NULL) { + PyErr_NoMemory(); + return NULL; + } + _channelitem_init(item, data, waiting); + return item; +} + +static void +_channelitem_free(_channelitem *item) +{ + _channelitem_clear(item); + GLOBAL_FREE(item); +} + +static void +_channelitem_free_all(_channelitem *item) +{ + while (item != NULL) { + _channelitem *last = item; + item = item->next; + _channelitem_free(last); + } +} + +static void +_channelitem_popped(_channelitem *item, + _PyCrossInterpreterData **p_data, _waiting_t **p_waiting) +{ + assert(item->waiting == NULL || item->waiting->status == WAITING_ACQUIRED); + *p_data = item->data; + *p_waiting = item->waiting; + // We clear them here, so they won't be released in _channelitem_clear(). + item->data = NULL; + item->waiting = NULL; + _channelitem_free(item); +} + +typedef struct _channelqueue { + int64_t count; + _channelitem *first; + _channelitem *last; +} _channelqueue; + +static _channelqueue * +_channelqueue_new(void) +{ + _channelqueue *queue = GLOBAL_MALLOC(_channelqueue); + if (queue == NULL) { + PyErr_NoMemory(); + return NULL; + } + queue->count = 0; + queue->first = NULL; + queue->last = NULL; + return queue; +} + +static void +_channelqueue_clear(_channelqueue *queue) +{ + _channelitem_free_all(queue->first); + queue->count = 0; + queue->first = NULL; + queue->last = NULL; +} + +static void +_channelqueue_free(_channelqueue *queue) +{ + _channelqueue_clear(queue); + GLOBAL_FREE(queue); +} + +static int +_channelqueue_put(_channelqueue *queue, + _PyCrossInterpreterData *data, _waiting_t *waiting) +{ + _channelitem *item = _channelitem_new(data, waiting); + if (item == NULL) { + return -1; + } + + queue->count += 1; + if (queue->first == NULL) { + queue->first = item; + } + else { + queue->last->next = item; + } + queue->last = item; + + if (waiting != NULL) { + _waiting_acquire(waiting); + } + + return 0; +} + +static int +_channelqueue_get(_channelqueue *queue, + _PyCrossInterpreterData **p_data, _waiting_t **p_waiting) +{ + _channelitem *item = queue->first; + if (item == NULL) { + return ERR_CHANNEL_EMPTY; + } + queue->first = item->next; + if (queue->last == item) { + queue->last = NULL; + } + queue->count -= 1; + + _channelitem_popped(item, p_data, p_waiting); + return 0; +} + +static int +_channelqueue_find(_channelqueue *queue, _channelitem_id_t itemid, + _channelitem **p_item, _channelitem **p_prev) +{ + _channelitem *prev = NULL; + _channelitem *item = NULL; + if (queue->first != NULL) { + if (_channelitem_ID(queue->first) == itemid) { + item = queue->first; + } + else { + prev = queue->first; + while (prev->next != NULL) { + if (_channelitem_ID(prev->next) == itemid) { + item = prev->next; + break; + } + prev = prev->next; + } + if (item == NULL) { + prev = NULL; + } + } + } + if (p_item != NULL) { + *p_item = item; + } + if (p_prev != NULL) { + *p_prev = prev; + } + return (item != NULL); +} + +static void +_channelqueue_remove(_channelqueue *queue, _channelitem_id_t itemid, + _PyCrossInterpreterData **p_data, _waiting_t **p_waiting) +{ + _channelitem *prev = NULL; + _channelitem *item = NULL; + int found = _channelqueue_find(queue, itemid, &item, &prev); + if (!found) { + return; + } + + assert(item->waiting != NULL); + assert(!item->waiting->received); + if (prev == NULL) { + assert(queue->first == item); + queue->first = item->next; + } + else { + assert(queue->first != item); + assert(prev->next == item); + prev->next = item->next; + } + item->next = NULL; + + if (queue->last == item) { + queue->last = prev; + } + queue->count -= 1; + + _channelitem_popped(item, p_data, p_waiting); +} + +static void +_channelqueue_clear_interpreter(_channelqueue *queue, int64_t interpid) +{ + _channelitem *prev = NULL; + _channelitem *next = queue->first; + while (next != NULL) { + _channelitem *item = next; + next = item->next; + if (item->data->interpid == interpid) { + if (prev == NULL) { + queue->first = item->next; + } + else { + prev->next = item->next; + } + _channelitem_free(item); + queue->count -= 1; + } + else { + prev = item; + } + } +} + + +/* channel-interpreter associations */ + +struct _channelend; + +typedef struct _channelend { + struct _channelend *next; + int64_t interpid; + int open; +} _channelend; + +static _channelend * +_channelend_new(int64_t interpid) +{ + _channelend *end = GLOBAL_MALLOC(_channelend); + if (end == NULL) { + PyErr_NoMemory(); + return NULL; + } + end->next = NULL; + end->interpid = interpid; + end->open = 1; + return end; +} + +static void +_channelend_free(_channelend *end) +{ + GLOBAL_FREE(end); +} + +static void +_channelend_free_all(_channelend *end) +{ + while (end != NULL) { + _channelend *last = end; + end = end->next; + _channelend_free(last); + } +} + +static _channelend * +_channelend_find(_channelend *first, int64_t interpid, _channelend **pprev) +{ + _channelend *prev = NULL; + _channelend *end = first; + while (end != NULL) { + if (end->interpid == interpid) { + break; + } + prev = end; + end = end->next; + } + if (pprev != NULL) { + *pprev = prev; + } + return end; +} + +typedef struct _channelassociations { + // Note that the list entries are never removed for interpreter + // for which the channel is closed. This should not be a problem in + // practice. Also, a channel isn't automatically closed when an + // interpreter is destroyed. + int64_t numsendopen; + int64_t numrecvopen; + _channelend *send; + _channelend *recv; +} _channelends; + +static _channelends * +_channelends_new(void) +{ + _channelends *ends = GLOBAL_MALLOC(_channelends); + if (ends== NULL) { + return NULL; + } + ends->numsendopen = 0; + ends->numrecvopen = 0; + ends->send = NULL; + ends->recv = NULL; + return ends; +} + +static void +_channelends_clear(_channelends *ends) +{ + _channelend_free_all(ends->send); + ends->send = NULL; + ends->numsendopen = 0; + + _channelend_free_all(ends->recv); + ends->recv = NULL; + ends->numrecvopen = 0; +} + +static void +_channelends_free(_channelends *ends) +{ + _channelends_clear(ends); + GLOBAL_FREE(ends); +} + +static _channelend * +_channelends_add(_channelends *ends, _channelend *prev, int64_t interpid, + int send) +{ + _channelend *end = _channelend_new(interpid); + if (end == NULL) { + return NULL; + } + + if (prev == NULL) { + if (send) { + ends->send = end; + } + else { + ends->recv = end; + } + } + else { + prev->next = end; + } + if (send) { + ends->numsendopen += 1; + } + else { + ends->numrecvopen += 1; + } + return end; +} + +static int +_channelends_associate(_channelends *ends, int64_t interpid, int send) +{ + _channelend *prev; + _channelend *end = _channelend_find(send ? ends->send : ends->recv, + interpid, &prev); + if (end != NULL) { + if (!end->open) { + return ERR_CHANNEL_CLOSED; + } + // already associated + return 0; + } + if (_channelends_add(ends, prev, interpid, send) == NULL) { + return -1; + } + return 0; +} + +static int +_channelends_is_open(_channelends *ends) +{ + if (ends->numsendopen != 0 || ends->numrecvopen != 0) { + // At least one interpreter is still associated with the channel + // (and hasn't been released). + return 1; + } + // XXX This is wrong if an end can ever be removed. + if (ends->send == NULL && ends->recv == NULL) { + // The channel has never had any interpreters associated with it. + return 1; + } + return 0; +} + +static void +_channelends_release_end(_channelends *ends, _channelend *end, int send) +{ + end->open = 0; + if (send) { + ends->numsendopen -= 1; + } + else { + ends->numrecvopen -= 1; + } +} + +static int +_channelends_release_interpreter(_channelends *ends, int64_t interpid, int which) +{ + _channelend *prev; + _channelend *end; + if (which >= 0) { // send/both + end = _channelend_find(ends->send, interpid, &prev); + if (end == NULL) { + // never associated so add it + end = _channelends_add(ends, prev, interpid, 1); + if (end == NULL) { + return -1; + } + } + _channelends_release_end(ends, end, 1); + } + if (which <= 0) { // recv/both + end = _channelend_find(ends->recv, interpid, &prev); + if (end == NULL) { + // never associated so add it + end = _channelends_add(ends, prev, interpid, 0); + if (end == NULL) { + return -1; + } + } + _channelends_release_end(ends, end, 0); + } + return 0; +} + +static void +_channelends_release_all(_channelends *ends, int which, int force) +{ + // XXX Handle the ends. + // XXX Handle force is True. + + // Ensure all the "send"-associated interpreters are closed. + _channelend *end; + for (end = ends->send; end != NULL; end = end->next) { + _channelends_release_end(ends, end, 1); + } + + // Ensure all the "recv"-associated interpreters are closed. + for (end = ends->recv; end != NULL; end = end->next) { + _channelends_release_end(ends, end, 0); + } +} + +static void +_channelends_clear_interpreter(_channelends *ends, int64_t interpid) +{ + // XXX Actually remove the entries? + _channelend *end; + end = _channelend_find(ends->send, interpid, NULL); + if (end != NULL) { + _channelends_release_end(ends, end, 1); + } + end = _channelend_find(ends->recv, interpid, NULL); + if (end != NULL) { + _channelends_release_end(ends, end, 0); + } +} + + +/* each channel's state */ + +struct _channel; +struct _channel_closing; +static void _channel_clear_closing(struct _channel *); +static void _channel_finish_closing(struct _channel *); + +typedef struct _channel { + PyThread_type_lock mutex; + _channelqueue *queue; + _channelends *ends; + int open; + struct _channel_closing *closing; +} _channel_state; + +static _channel_state * +_channel_new(PyThread_type_lock mutex) +{ + _channel_state *chan = GLOBAL_MALLOC(_channel_state); + if (chan == NULL) { + return NULL; + } + chan->mutex = mutex; + chan->queue = _channelqueue_new(); + if (chan->queue == NULL) { + GLOBAL_FREE(chan); + return NULL; + } + chan->ends = _channelends_new(); + if (chan->ends == NULL) { + _channelqueue_free(chan->queue); + GLOBAL_FREE(chan); + return NULL; + } + chan->open = 1; + chan->closing = NULL; + return chan; +} + +static void +_channel_free(_channel_state *chan) +{ + _channel_clear_closing(chan); + PyThread_acquire_lock(chan->mutex, WAIT_LOCK); + _channelqueue_free(chan->queue); + _channelends_free(chan->ends); + PyThread_release_lock(chan->mutex); + + PyThread_free_lock(chan->mutex); + GLOBAL_FREE(chan); +} + +static int +_channel_add(_channel_state *chan, int64_t interpid, + _PyCrossInterpreterData *data, _waiting_t *waiting) +{ + int res = -1; + PyThread_acquire_lock(chan->mutex, WAIT_LOCK); + + if (!chan->open) { + res = ERR_CHANNEL_CLOSED; + goto done; + } + if (_channelends_associate(chan->ends, interpid, 1) != 0) { + res = ERR_CHANNEL_INTERP_CLOSED; + goto done; + } + + if (_channelqueue_put(chan->queue, data, waiting) != 0) { + goto done; + } + // Any errors past this point must cause a _waiting_release() call. + + res = 0; +done: + PyThread_release_lock(chan->mutex); + return res; +} + +static int +_channel_next(_channel_state *chan, int64_t interpid, + _PyCrossInterpreterData **p_data, _waiting_t **p_waiting) +{ + int err = 0; + PyThread_acquire_lock(chan->mutex, WAIT_LOCK); + + if (!chan->open) { + err = ERR_CHANNEL_CLOSED; + goto done; + } + if (_channelends_associate(chan->ends, interpid, 0) != 0) { + err = ERR_CHANNEL_INTERP_CLOSED; + goto done; + } + + int empty = _channelqueue_get(chan->queue, p_data, p_waiting); + assert(empty == 0 || empty == ERR_CHANNEL_EMPTY); + assert(!PyErr_Occurred()); + if (empty && chan->closing != NULL) { + chan->open = 0; + } + +done: + PyThread_release_lock(chan->mutex); + if (chan->queue->count == 0) { + _channel_finish_closing(chan); + } + return err; +} + +static void +_channel_remove(_channel_state *chan, _channelitem_id_t itemid) +{ + _PyCrossInterpreterData *data = NULL; + _waiting_t *waiting = NULL; + + PyThread_acquire_lock(chan->mutex, WAIT_LOCK); + _channelqueue_remove(chan->queue, itemid, &data, &waiting); + PyThread_release_lock(chan->mutex); + + (void)_release_xid_data(data, XID_IGNORE_EXC | XID_FREE); + if (waiting != NULL) { + _waiting_release(waiting, 0); + } + + if (chan->queue->count == 0) { + _channel_finish_closing(chan); + } +} + +static int +_channel_release_interpreter(_channel_state *chan, int64_t interpid, int end) +{ + PyThread_acquire_lock(chan->mutex, WAIT_LOCK); + + int res = -1; + if (!chan->open) { + res = ERR_CHANNEL_CLOSED; + goto done; + } + + if (_channelends_release_interpreter(chan->ends, interpid, end) != 0) { + goto done; + } + chan->open = _channelends_is_open(chan->ends); + // XXX Clear the queue if not empty? + // XXX Activate the "closing" mechanism? + + res = 0; +done: + PyThread_release_lock(chan->mutex); + return res; +} + +static int +_channel_release_all(_channel_state *chan, int end, int force) +{ + int res = -1; + PyThread_acquire_lock(chan->mutex, WAIT_LOCK); + + if (!chan->open) { + res = ERR_CHANNEL_CLOSED; + goto done; + } + + if (!force && chan->queue->count > 0) { + res = ERR_CHANNEL_NOT_EMPTY; + goto done; + } + // XXX Clear the queue? + + chan->open = 0; + + // We *could* also just leave these in place, since we've marked + // the channel as closed already. + _channelends_release_all(chan->ends, end, force); + + res = 0; +done: + PyThread_release_lock(chan->mutex); + return res; +} + +static void +_channel_clear_interpreter(_channel_state *chan, int64_t interpid) +{ + PyThread_acquire_lock(chan->mutex, WAIT_LOCK); + + _channelqueue_clear_interpreter(chan->queue, interpid); + _channelends_clear_interpreter(chan->ends, interpid); + chan->open = _channelends_is_open(chan->ends); + + PyThread_release_lock(chan->mutex); +} + + +/* the set of channels */ + +struct _channelref; + +typedef struct _channelref { + int64_t cid; + _channel_state *chan; + struct _channelref *next; + // The number of ChannelID objects referring to this channel. + Py_ssize_t objcount; +} _channelref; + +static _channelref * +_channelref_new(int64_t cid, _channel_state *chan) +{ + _channelref *ref = GLOBAL_MALLOC(_channelref); + if (ref == NULL) { + return NULL; + } + ref->cid = cid; + ref->chan = chan; + ref->next = NULL; + ref->objcount = 0; + return ref; +} + +//static void +//_channelref_clear(_channelref *ref) +//{ +// ref->cid = -1; +// ref->chan = NULL; +// ref->next = NULL; +// ref->objcount = 0; +//} + +static void +_channelref_free(_channelref *ref) +{ + if (ref->chan != NULL) { + _channel_clear_closing(ref->chan); + } + //_channelref_clear(ref); + GLOBAL_FREE(ref); +} + +static _channelref * +_channelref_find(_channelref *first, int64_t cid, _channelref **pprev) +{ + _channelref *prev = NULL; + _channelref *ref = first; + while (ref != NULL) { + if (ref->cid == cid) { + break; + } + prev = ref; + ref = ref->next; + } + if (pprev != NULL) { + *pprev = prev; + } + return ref; +} + + +typedef struct _channels { + PyThread_type_lock mutex; + _channelref *head; + int64_t numopen; + int64_t next_id; +} _channels; + +static void +_channels_init(_channels *channels, PyThread_type_lock mutex) +{ + channels->mutex = mutex; + channels->head = NULL; + channels->numopen = 0; + channels->next_id = 0; +} + +static void +_channels_fini(_channels *channels) +{ + assert(channels->numopen == 0); + assert(channels->head == NULL); + if (channels->mutex != NULL) { + PyThread_free_lock(channels->mutex); + channels->mutex = NULL; + } +} + +static int64_t +_channels_next_id(_channels *channels) // needs lock +{ + int64_t cid = channels->next_id; + if (cid < 0) { + /* overflow */ + return -1; + } + channels->next_id += 1; + return cid; +} + +static int +_channels_lookup(_channels *channels, int64_t cid, PyThread_type_lock *pmutex, + _channel_state **res) +{ + int err = -1; + _channel_state *chan = NULL; + PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + if (pmutex != NULL) { + *pmutex = NULL; + } + + _channelref *ref = _channelref_find(channels->head, cid, NULL); + if (ref == NULL) { + err = ERR_CHANNEL_NOT_FOUND; + goto done; + } + if (ref->chan == NULL || !ref->chan->open) { + err = ERR_CHANNEL_CLOSED; + goto done; + } + + if (pmutex != NULL) { + // The mutex will be closed by the caller. + *pmutex = channels->mutex; + } + + chan = ref->chan; + err = 0; + +done: + if (pmutex == NULL || *pmutex == NULL) { + PyThread_release_lock(channels->mutex); + } + *res = chan; + return err; +} + +static int64_t +_channels_add(_channels *channels, _channel_state *chan) +{ + int64_t cid = -1; + PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + + // Create a new ref. + int64_t _cid = _channels_next_id(channels); + if (_cid < 0) { + cid = ERR_NO_NEXT_CHANNEL_ID; + goto done; + } + _channelref *ref = _channelref_new(_cid, chan); + if (ref == NULL) { + goto done; + } + + // Add it to the list. + // We assume that the channel is a new one (not already in the list). + ref->next = channels->head; + channels->head = ref; + channels->numopen += 1; + + cid = _cid; +done: + PyThread_release_lock(channels->mutex); + return cid; +} + +/* forward */ +static int _channel_set_closing(_channelref *, PyThread_type_lock); + +static int +_channels_close(_channels *channels, int64_t cid, _channel_state **pchan, + int end, int force) +{ + int res = -1; + PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + if (pchan != NULL) { + *pchan = NULL; + } + + _channelref *ref = _channelref_find(channels->head, cid, NULL); + if (ref == NULL) { + res = ERR_CHANNEL_NOT_FOUND; + goto done; + } + + if (ref->chan == NULL) { + res = ERR_CHANNEL_CLOSED; + goto done; + } + else if (!force && end == CHANNEL_SEND && ref->chan->closing != NULL) { + res = ERR_CHANNEL_CLOSED; + goto done; + } + else { + int err = _channel_release_all(ref->chan, end, force); + if (err != 0) { + if (end == CHANNEL_SEND && err == ERR_CHANNEL_NOT_EMPTY) { + if (ref->chan->closing != NULL) { + res = ERR_CHANNEL_CLOSED; + goto done; + } + // Mark the channel as closing and return. The channel + // will be cleaned up in _channel_next(). + PyErr_Clear(); + int err = _channel_set_closing(ref, channels->mutex); + if (err != 0) { + res = err; + goto done; + } + if (pchan != NULL) { + *pchan = ref->chan; + } + res = 0; + } + else { + res = err; + } + goto done; + } + if (pchan != NULL) { + *pchan = ref->chan; + } + else { + _channel_free(ref->chan); + } + ref->chan = NULL; + } + + res = 0; +done: + PyThread_release_lock(channels->mutex); + return res; +} + +static void +_channels_remove_ref(_channels *channels, _channelref *ref, _channelref *prev, + _channel_state **pchan) +{ + if (ref == channels->head) { + channels->head = ref->next; + } + else { + prev->next = ref->next; + } + channels->numopen -= 1; + + if (pchan != NULL) { + *pchan = ref->chan; + } + _channelref_free(ref); +} + +static int +_channels_remove(_channels *channels, int64_t cid, _channel_state **pchan) +{ + int res = -1; + PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + + if (pchan != NULL) { + *pchan = NULL; + } + + _channelref *prev = NULL; + _channelref *ref = _channelref_find(channels->head, cid, &prev); + if (ref == NULL) { + res = ERR_CHANNEL_NOT_FOUND; + goto done; + } + + _channels_remove_ref(channels, ref, prev, pchan); + + res = 0; +done: + PyThread_release_lock(channels->mutex); + return res; +} + +static int +_channels_add_id_object(_channels *channels, int64_t cid) +{ + int res = -1; + PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + + _channelref *ref = _channelref_find(channels->head, cid, NULL); + if (ref == NULL) { + res = ERR_CHANNEL_NOT_FOUND; + goto done; + } + ref->objcount += 1; + + res = 0; +done: + PyThread_release_lock(channels->mutex); + return res; +} + +static void +_channels_release_cid_object(_channels *channels, int64_t cid) +{ + PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + + _channelref *prev = NULL; + _channelref *ref = _channelref_find(channels->head, cid, &prev); + if (ref == NULL) { + // Already destroyed. + goto done; + } + ref->objcount -= 1; + + // Destroy if no longer used. + if (ref->objcount == 0) { + _channel_state *chan = NULL; + _channels_remove_ref(channels, ref, prev, &chan); + if (chan != NULL) { + _channel_free(chan); + } + } + +done: + PyThread_release_lock(channels->mutex); +} + +static int64_t * +_channels_list_all(_channels *channels, int64_t *count) +{ + int64_t *cids = NULL; + PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + int64_t *ids = PyMem_NEW(int64_t, (Py_ssize_t)(channels->numopen)); + if (ids == NULL) { + goto done; + } + _channelref *ref = channels->head; + for (int64_t i=0; ref != NULL; ref = ref->next, i++) { + ids[i] = ref->cid; + } + *count = channels->numopen; + + cids = ids; +done: + PyThread_release_lock(channels->mutex); + return cids; +} + +static void +_channels_clear_interpreter(_channels *channels, int64_t interpid) +{ + PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + + _channelref *ref = channels->head; + for (; ref != NULL; ref = ref->next) { + if (ref->chan != NULL) { + _channel_clear_interpreter(ref->chan, interpid); + } + } + + PyThread_release_lock(channels->mutex); +} + + +/* support for closing non-empty channels */ + +struct _channel_closing { + _channelref *ref; +}; + +static int +_channel_set_closing(_channelref *ref, PyThread_type_lock mutex) { + _channel_state *chan = ref->chan; + if (chan == NULL) { + // already closed + return 0; + } + int res = -1; + PyThread_acquire_lock(chan->mutex, WAIT_LOCK); + if (chan->closing != NULL) { + res = ERR_CHANNEL_CLOSED; + goto done; + } + chan->closing = GLOBAL_MALLOC(struct _channel_closing); + if (chan->closing == NULL) { + goto done; + } + chan->closing->ref = ref; + + res = 0; +done: + PyThread_release_lock(chan->mutex); + return res; +} + +static void +_channel_clear_closing(_channel_state *chan) { + PyThread_acquire_lock(chan->mutex, WAIT_LOCK); + if (chan->closing != NULL) { + GLOBAL_FREE(chan->closing); + chan->closing = NULL; + } + PyThread_release_lock(chan->mutex); +} + +static void +_channel_finish_closing(_channel_state *chan) { + struct _channel_closing *closing = chan->closing; + if (closing == NULL) { + return; + } + _channelref *ref = closing->ref; + _channel_clear_closing(chan); + // Do the things that would have been done in _channels_close(). + ref->chan = NULL; + _channel_free(chan); +} + + +/* "high"-level channel-related functions */ + +// Create a new channel. +static int64_t +channel_create(_channels *channels) +{ + PyThread_type_lock mutex = PyThread_allocate_lock(); + if (mutex == NULL) { + return ERR_CHANNEL_MUTEX_INIT; + } + _channel_state *chan = _channel_new(mutex); + if (chan == NULL) { + PyThread_free_lock(mutex); + return -1; + } + int64_t cid = _channels_add(channels, chan); + if (cid < 0) { + _channel_free(chan); + } + return cid; +} + +// Completely destroy the channel. +static int +channel_destroy(_channels *channels, int64_t cid) +{ + _channel_state *chan = NULL; + int err = _channels_remove(channels, cid, &chan); + if (err != 0) { + return err; + } + if (chan != NULL) { + _channel_free(chan); + } + return 0; +} + +// Push an object onto the channel. +// The current interpreter gets associated with the send end of the channel. +// Optionally request to be notified when it is received. +static int +channel_send(_channels *channels, int64_t cid, PyObject *obj, + _waiting_t *waiting) +{ + PyInterpreterState *interp = _get_current_interp(); + if (interp == NULL) { + return -1; + } + int64_t interpid = PyInterpreterState_GetID(interp); + + // Look up the channel. + PyThread_type_lock mutex = NULL; + _channel_state *chan = NULL; + int err = _channels_lookup(channels, cid, &mutex, &chan); + if (err != 0) { + return err; + } + assert(chan != NULL); + // Past this point we are responsible for releasing the mutex. + + if (chan->closing != NULL) { + PyThread_release_lock(mutex); + return ERR_CHANNEL_CLOSED; + } + + // Convert the object to cross-interpreter data. + _PyCrossInterpreterData *data = GLOBAL_MALLOC(_PyCrossInterpreterData); + if (data == NULL) { + PyThread_release_lock(mutex); + return -1; + } + if (_PyObject_GetCrossInterpreterData(obj, data) != 0) { + PyThread_release_lock(mutex); + GLOBAL_FREE(data); + return -1; + } + + // Add the data to the channel. + int res = _channel_add(chan, interpid, data, waiting); + PyThread_release_lock(mutex); + if (res != 0) { + // We may chain an exception here: + (void)_release_xid_data(data, 0); + GLOBAL_FREE(data); + return res; + } + + return 0; +} + +// Basically, un-send an object. +static void +channel_clear_sent(_channels *channels, int64_t cid, _waiting_t *waiting) +{ + // Look up the channel. + PyThread_type_lock mutex = NULL; + _channel_state *chan = NULL; + int err = _channels_lookup(channels, cid, &mutex, &chan); + if (err != 0) { + // The channel was already closed, etc. + assert(waiting->status == WAITING_RELEASED); + return; // Ignore the error. + } + assert(chan != NULL); + // Past this point we are responsible for releasing the mutex. + + _channelitem_id_t itemid = _waiting_get_itemid(waiting); + _channel_remove(chan, itemid); + + PyThread_release_lock(mutex); +} + +// Like channel_send(), but strictly wait for the object to be received. +static int +channel_send_wait(_channels *channels, int64_t cid, PyObject *obj, + PY_TIMEOUT_T timeout) +{ + // We use a stack variable here, so we must ensure that &waiting + // is not held by any channel item at the point this function exits. + _waiting_t waiting; + if (_waiting_init(&waiting) < 0) { + assert(PyErr_Occurred()); + return -1; + } + + /* Queue up the object. */ + int res = channel_send(channels, cid, obj, &waiting); + if (res < 0) { + assert(waiting.status == WAITING_NO_STATUS); + goto finally; + } + + /* Wait until the object is received. */ + if (wait_for_lock(waiting.mutex, timeout) < 0) { + assert(PyErr_Occurred()); + _waiting_finish_releasing(&waiting); + /* The send() call is failing now, so make sure the item + won't be received. */ + channel_clear_sent(channels, cid, &waiting); + assert(waiting.status == WAITING_RELEASED); + if (!waiting.received) { + res = -1; + goto finally; + } + // XXX Emit a warning if not a TimeoutError? + PyErr_Clear(); + } + else { + _waiting_finish_releasing(&waiting); + assert(waiting.status == WAITING_RELEASED); + if (!waiting.received) { + res = ERR_CHANNEL_CLOSED_WAITING; + goto finally; + } + } + + /* success! */ + res = 0; + +finally: + _waiting_clear(&waiting); + return res; +} + +// Pop the next object off the channel. Fail if empty. +// The current interpreter gets associated with the recv end of the channel. +// XXX Support a "wait" mutex? +static int +channel_recv(_channels *channels, int64_t cid, PyObject **res) +{ + int err; + *res = NULL; + + PyInterpreterState *interp = _get_current_interp(); + if (interp == NULL) { + // XXX Is this always an error? + if (PyErr_Occurred()) { + return -1; + } + return 0; + } + int64_t interpid = PyInterpreterState_GetID(interp); + + // Look up the channel. + PyThread_type_lock mutex = NULL; + _channel_state *chan = NULL; + err = _channels_lookup(channels, cid, &mutex, &chan); + if (err != 0) { + return err; + } + assert(chan != NULL); + // Past this point we are responsible for releasing the mutex. + + // Pop off the next item from the channel. + _PyCrossInterpreterData *data = NULL; + _waiting_t *waiting = NULL; + err = _channel_next(chan, interpid, &data, &waiting); + PyThread_release_lock(mutex); + if (err != 0) { + return err; + } + else if (data == NULL) { + assert(!PyErr_Occurred()); + return 0; + } + + // Convert the data back to an object. + PyObject *obj = _PyCrossInterpreterData_NewObject(data); + if (obj == NULL) { + assert(PyErr_Occurred()); + // It was allocated in channel_send(), so we free it. + (void)_release_xid_data(data, XID_IGNORE_EXC | XID_FREE); + if (waiting != NULL) { + _waiting_release(waiting, 0); + } + return -1; + } + // It was allocated in channel_send(), so we free it. + int release_res = _release_xid_data(data, XID_FREE); + if (release_res < 0) { + // The source interpreter has been destroyed already. + assert(PyErr_Occurred()); + Py_DECREF(obj); + if (waiting != NULL) { + _waiting_release(waiting, 0); + } + return -1; + } + + // Notify the sender. + if (waiting != NULL) { + _waiting_release(waiting, 1); + } + + *res = obj; + return 0; +} + +// Disallow send/recv for the current interpreter. +// The channel is marked as closed if no other interpreters +// are currently associated. +static int +channel_release(_channels *channels, int64_t cid, int send, int recv) +{ + PyInterpreterState *interp = _get_current_interp(); + if (interp == NULL) { + return -1; + } + int64_t interpid = PyInterpreterState_GetID(interp); + + // Look up the channel. + PyThread_type_lock mutex = NULL; + _channel_state *chan = NULL; + int err = _channels_lookup(channels, cid, &mutex, &chan); + if (err != 0) { + return err; + } + // Past this point we are responsible for releasing the mutex. + + // Close one or both of the two ends. + int res = _channel_release_interpreter(chan, interpid, send-recv); + PyThread_release_lock(mutex); + return res; +} + +// Close the channel (for all interpreters). Fail if it's already closed. +// Close immediately if it's empty. Otherwise, disallow sending and +// finally close once empty. Optionally, immediately clear and close it. +static int +channel_close(_channels *channels, int64_t cid, int end, int force) +{ + return _channels_close(channels, cid, NULL, end, force); +} + +// Return true if the identified interpreter is associated +// with the given end of the channel. +static int +channel_is_associated(_channels *channels, int64_t cid, int64_t interpid, + int send) +{ + _channel_state *chan = NULL; + int err = _channels_lookup(channels, cid, NULL, &chan); + if (err != 0) { + return err; + } + else if (send && chan->closing != NULL) { + return ERR_CHANNEL_CLOSED; + } + + _channelend *end = _channelend_find(send ? chan->ends->send : chan->ends->recv, + interpid, NULL); + + return (end != NULL && end->open); +} + + +/* channel info */ + +struct channel_info { + struct { + // 1: closed; -1: closing + int closed; + struct { + Py_ssize_t nsend_only; // not released + Py_ssize_t nsend_only_released; + Py_ssize_t nrecv_only; // not released + Py_ssize_t nrecv_only_released; + Py_ssize_t nboth; // not released + Py_ssize_t nboth_released; + Py_ssize_t nboth_send_released; + Py_ssize_t nboth_recv_released; + } all; + struct { + // 1: associated; -1: released + int send; + int recv; + } cur; + } status; + Py_ssize_t count; +}; + +static int +_channel_get_info(_channels *channels, int64_t cid, struct channel_info *info) +{ + int err = 0; + *info = (struct channel_info){0}; + + // Get the current interpreter. + PyInterpreterState *interp = _get_current_interp(); + if (interp == NULL) { + return -1; + } + Py_ssize_t interpid = PyInterpreterState_GetID(interp); + + // Hold the global lock until we're done. + PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + + // Find the channel. + _channelref *ref = _channelref_find(channels->head, cid, NULL); + if (ref == NULL) { + err = ERR_CHANNEL_NOT_FOUND; + goto finally; + } + _channel_state *chan = ref->chan; + + // Check if open. + if (chan == NULL) { + info->status.closed = 1; + goto finally; + } + if (!chan->open) { + assert(chan->queue->count == 0); + info->status.closed = 1; + goto finally; + } + if (chan->closing != NULL) { + assert(chan->queue->count > 0); + info->status.closed = -1; + } + else { + info->status.closed = 0; + } + + // Get the number of queued objects. + info->count = chan->queue->count; + + // Get the ends statuses. + assert(info->status.cur.send == 0); + assert(info->status.cur.recv == 0); + _channelend *send = chan->ends->send; + while (send != NULL) { + if (send->interpid == interpid) { + info->status.cur.send = send->open ? 1 : -1; + } + + if (send->open) { + info->status.all.nsend_only += 1; + } + else { + info->status.all.nsend_only_released += 1; + } + send = send->next; + } + _channelend *recv = chan->ends->recv; + while (recv != NULL) { + if (recv->interpid == interpid) { + info->status.cur.recv = recv->open ? 1 : -1; + } + + // XXX This is O(n*n). Why do we have 2 linked lists? + _channelend *send = chan->ends->send; + while (send != NULL) { + if (send->interpid == recv->interpid) { + break; + } + send = send->next; + } + if (send == NULL) { + if (recv->open) { + info->status.all.nrecv_only += 1; + } + else { + info->status.all.nrecv_only_released += 1; + } + } + else { + if (recv->open) { + if (send->open) { + info->status.all.nboth += 1; + info->status.all.nsend_only -= 1; + } + else { + info->status.all.nboth_recv_released += 1; + info->status.all.nsend_only_released -= 1; + } + } + else { + if (send->open) { + info->status.all.nboth_send_released += 1; + info->status.all.nsend_only -= 1; + } + else { + info->status.all.nboth_released += 1; + info->status.all.nsend_only_released -= 1; + } + } + } + recv = recv->next; + } + +finally: + PyThread_release_lock(channels->mutex); + return err; +} + +PyDoc_STRVAR(channel_info_doc, +"ChannelInfo\n\ +\n\ +A named tuple of a channel's state."); + +static PyStructSequence_Field channel_info_fields[] = { + {"open", "both ends are open"}, + {"closing", "send is closed, recv is non-empty"}, + {"closed", "both ends are closed"}, + {"count", "queued objects"}, + + {"num_interp_send", "interpreters bound to the send end"}, + {"num_interp_send_released", + "interpreters bound to the send end and released"}, + + {"num_interp_recv", "interpreters bound to the send end"}, + {"num_interp_recv_released", + "interpreters bound to the send end and released"}, + + {"num_interp_both", "interpreters bound to both ends"}, + {"num_interp_both_released", + "interpreters bound to both ends and released_from_both"}, + {"num_interp_both_send_released", + "interpreters bound to both ends and released_from_the send end"}, + {"num_interp_both_recv_released", + "interpreters bound to both ends and released_from_the recv end"}, + + {"send_associated", "current interpreter is bound to the send end"}, + {"send_released", "current interpreter *was* bound to the send end"}, + {"recv_associated", "current interpreter is bound to the recv end"}, + {"recv_released", "current interpreter *was* bound to the recv end"}, + {0} +}; + +static PyStructSequence_Desc channel_info_desc = { + .name = MODULE_NAME ".ChannelInfo", + .doc = channel_info_doc, + .fields = channel_info_fields, + .n_in_sequence = 8, +}; + +static PyObject * +new_channel_info(PyObject *mod, struct channel_info *info) +{ + module_state *state = get_module_state(mod); + if (state == NULL) { + return NULL; + } + + assert(state->ChannelInfoType != NULL); + PyObject *self = PyStructSequence_New(state->ChannelInfoType); + if (self == NULL) { + return NULL; + } + + int pos = 0; +#define SET_BOOL(val) \ + PyStructSequence_SET_ITEM(self, pos++, \ + Py_NewRef(val ? Py_True : Py_False)) +#define SET_COUNT(val) \ + do { \ + PyObject *obj = PyLong_FromLongLong(val); \ + if (obj == NULL) { \ + Py_CLEAR(info); \ + return NULL; \ + } \ + PyStructSequence_SET_ITEM(self, pos++, obj); \ + } while(0) + SET_BOOL(info->status.closed == 0); + SET_BOOL(info->status.closed == -1); + SET_BOOL(info->status.closed == 1); + SET_COUNT(info->count); + SET_COUNT(info->status.all.nsend_only); + SET_COUNT(info->status.all.nsend_only_released); + SET_COUNT(info->status.all.nrecv_only); + SET_COUNT(info->status.all.nrecv_only_released); + SET_COUNT(info->status.all.nboth); + SET_COUNT(info->status.all.nboth_released); + SET_COUNT(info->status.all.nboth_send_released); + SET_COUNT(info->status.all.nboth_recv_released); + SET_BOOL(info->status.cur.send == 1); + SET_BOOL(info->status.cur.send == -1); + SET_BOOL(info->status.cur.recv == 1); + SET_BOOL(info->status.cur.recv == -1); +#undef SET_COUNT +#undef SET_BOOL + assert(!PyErr_Occurred()); + return self; +} + + +/* ChannelID class */ + +typedef struct queueid { + PyObject_HEAD + int64_t cid; + int end; + int resolve; + _channels *channels; +} queueid; + +struct channel_id_converter_data { + PyObject *module; + int64_t cid; + int end; +}; + +static int +channel_id_converter(PyObject *arg, void *ptr) +{ + int64_t cid; + int end = 0; + struct channel_id_converter_data *data = ptr; + module_state *state = get_module_state(data->module); + assert(state != NULL); + if (PyObject_TypeCheck(arg, state->QueueIDType)) { + cid = ((queueid *)arg)->cid; + end = ((queueid *)arg)->end; + } + else if (PyIndex_Check(arg)) { + cid = PyLong_AsLongLong(arg); + if (cid == -1 && PyErr_Occurred()) { + return 0; + } + if (cid < 0) { + PyErr_Format(PyExc_ValueError, + "channel ID must be a non-negative int, got %R", arg); + return 0; + } + } + else { + PyErr_Format(PyExc_TypeError, + "channel ID must be an int, got %.100s", + Py_TYPE(arg)->tp_name); + return 0; + } + data->cid = cid; + data->end = end; + return 1; +} + +static int +newqueueid(PyTypeObject *cls, int64_t cid, int end, _channels *channels, + int force, int resolve, queueid **res) +{ + *res = NULL; + + queueid *self = PyObject_New(queueid, cls); + if (self == NULL) { + return -1; + } + self->cid = cid; + self->end = end; + self->resolve = resolve; + self->channels = channels; + + int err = _channels_add_id_object(channels, cid); + if (err != 0) { + if (force && err == ERR_CHANNEL_NOT_FOUND) { + assert(!PyErr_Occurred()); + } + else { + Py_DECREF((PyObject *)self); + return err; + } + } + + *res = self; + return 0; +} + +static _channels * _global_channels(void); + +static PyObject * +_queueid_new(PyObject *mod, PyTypeObject *cls, + PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"id", "send", "recv", "force", "_resolve", NULL}; + int64_t cid; + int end; + struct channel_id_converter_data cid_data = { + .module = mod, + }; + int send = -1; + int recv = -1; + int force = 0; + int resolve = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O&|$pppp:ChannelID.__new__", kwlist, + channel_id_converter, &cid_data, + &send, &recv, &force, &resolve)) { + return NULL; + } + cid = cid_data.cid; + end = cid_data.end; + + // Handle "send" and "recv". + if (send == 0 && recv == 0) { + PyErr_SetString(PyExc_ValueError, + "'send' and 'recv' cannot both be False"); + return NULL; + } + else if (send == 1) { + if (recv == 0 || recv == -1) { + end = CHANNEL_SEND; + } + else { + assert(recv == 1); + end = 0; + } + } + else if (recv == 1) { + assert(send == 0 || send == -1); + end = CHANNEL_RECV; + } + + PyObject *cidobj = NULL; + int err = newqueueid(cls, cid, end, _global_channels(), + force, resolve, + (queueid **)&cidobj); + if (handle_channel_error(err, mod, cid)) { + assert(cidobj == NULL); + return NULL; + } + assert(cidobj != NULL); + return cidobj; +} + +static void +queueid_dealloc(PyObject *self) +{ + int64_t cid = ((queueid *)self)->cid; + _channels *channels = ((queueid *)self)->channels; + + PyTypeObject *tp = Py_TYPE(self); + tp->tp_free(self); + /* "Instances of heap-allocated types hold a reference to their type." + * See: https://docs.python.org/3.11/howto/isolating-extensions.html#garbage-collection-protocol + * See: https://docs.python.org/3.11/c-api/typeobj.html#c.PyTypeObject.tp_traverse + */ + // XXX Why don't we implement Py_TPFLAGS_HAVE_GC, e.g. Py_tp_traverse, + // like we do for _abc._abc_data? + Py_DECREF(tp); + + _channels_release_cid_object(channels, cid); +} + +static PyObject * +queueid_repr(PyObject *self) +{ + PyTypeObject *type = Py_TYPE(self); + const char *name = _PyType_Name(type); + + queueid *cidobj = (queueid *)self; + const char *fmt; + if (cidobj->end == CHANNEL_SEND) { + fmt = "%s(%" PRId64 ", send=True)"; + } + else if (cidobj->end == CHANNEL_RECV) { + fmt = "%s(%" PRId64 ", recv=True)"; + } + else { + fmt = "%s(%" PRId64 ")"; + } + return PyUnicode_FromFormat(fmt, name, cidobj->cid); +} + +static PyObject * +queueid_str(PyObject *self) +{ + queueid *cidobj = (queueid *)self; + return PyUnicode_FromFormat("%" PRId64 "", cidobj->cid); +} + +static PyObject * +queueid_int(PyObject *self) +{ + queueid *cidobj = (queueid *)self; + return PyLong_FromLongLong(cidobj->cid); +} + +static Py_hash_t +queueid_hash(PyObject *self) +{ + queueid *cidobj = (queueid *)self; + PyObject *pyid = PyLong_FromLongLong(cidobj->cid); + if (pyid == NULL) { + return -1; + } + Py_hash_t hash = PyObject_Hash(pyid); + Py_DECREF(pyid); + return hash; +} + +static PyObject * +queueid_richcompare(PyObject *self, PyObject *other, int op) +{ + PyObject *res = NULL; + if (op != Py_EQ && op != Py_NE) { + Py_RETURN_NOTIMPLEMENTED; + } + + PyObject *mod = get_module_from_type(Py_TYPE(self)); + if (mod == NULL) { + return NULL; + } + module_state *state = get_module_state(mod); + if (state == NULL) { + goto done; + } + + if (!PyObject_TypeCheck(self, state->QueueIDType)) { + res = Py_NewRef(Py_NotImplemented); + goto done; + } + + queueid *cidobj = (queueid *)self; + int equal; + if (PyObject_TypeCheck(other, state->QueueIDType)) { + queueid *othercidobj = (queueid *)other; + equal = (cidobj->end == othercidobj->end) && (cidobj->cid == othercidobj->cid); + } + else if (PyLong_Check(other)) { + /* Fast path */ + int overflow; + long long othercid = PyLong_AsLongLongAndOverflow(other, &overflow); + if (othercid == -1 && PyErr_Occurred()) { + goto done; + } + equal = !overflow && (othercid >= 0) && (cidobj->cid == othercid); + } + else if (PyNumber_Check(other)) { + PyObject *pyid = PyLong_FromLongLong(cidobj->cid); + if (pyid == NULL) { + goto done; + } + res = PyObject_RichCompare(pyid, other, op); + Py_DECREF(pyid); + goto done; + } + else { + res = Py_NewRef(Py_NotImplemented); + goto done; + } + + if ((op == Py_EQ && equal) || (op == Py_NE && !equal)) { + res = Py_NewRef(Py_True); + } + else { + res = Py_NewRef(Py_False); + } + +done: + Py_DECREF(mod); + return res; +} + +static PyTypeObject * _get_current_channelend_type(int end); + +static PyObject * +_channelobj_from_cidobj(PyObject *cidobj, int end) +{ + PyObject *cls = (PyObject *)_get_current_channelend_type(end); + if (cls == NULL) { + return NULL; + } + PyObject *chan = PyObject_CallFunctionObjArgs(cls, cidobj, NULL); + Py_DECREF(cls); + if (chan == NULL) { + return NULL; + } + return chan; +} + +struct _queueid_xid { + int64_t cid; + int end; + int resolve; +}; + +static PyObject * +_queueid_from_xid(_PyCrossInterpreterData *data) +{ + struct _queueid_xid *xid = (struct _queueid_xid *)data->data; + + // It might not be imported yet, so we can't use _get_current_module(). + PyObject *mod = PyImport_ImportModule(MODULE_NAME); + if (mod == NULL) { + return NULL; + } + assert(mod != Py_None); + module_state *state = get_module_state(mod); + if (state == NULL) { + return NULL; + } + + // Note that we do not preserve the "resolve" flag. + PyObject *cidobj = NULL; + int err = newqueueid(state->QueueIDType, xid->cid, xid->end, + _global_channels(), 0, 0, + (queueid **)&cidobj); + if (err != 0) { + assert(cidobj == NULL); + (void)handle_channel_error(err, mod, xid->cid); + goto done; + } + assert(cidobj != NULL); + if (xid->end == 0) { + goto done; + } + if (!xid->resolve) { + goto done; + } + + /* Try returning a high-level channel end but fall back to the ID. */ + PyObject *chan = _channelobj_from_cidobj(cidobj, xid->end); + if (chan == NULL) { + PyErr_Clear(); + goto done; + } + Py_DECREF(cidobj); + cidobj = chan; + +done: + Py_DECREF(mod); + return cidobj; +} + +static int +_queueid_shared(PyThreadState *tstate, PyObject *obj, + _PyCrossInterpreterData *data) +{ + if (_PyCrossInterpreterData_InitWithSize( + data, tstate->interp, sizeof(struct _queueid_xid), obj, + _queueid_from_xid + ) < 0) + { + return -1; + } + struct _queueid_xid *xid = (struct _queueid_xid *)data->data; + xid->cid = ((queueid *)obj)->cid; + xid->end = ((queueid *)obj)->end; + xid->resolve = ((queueid *)obj)->resolve; + return 0; +} + +static PyObject * +queueid_end(PyObject *self, void *end) +{ + int force = 1; + queueid *cidobj = (queueid *)self; + if (end != NULL) { + PyObject *obj = NULL; + int err = newqueueid(Py_TYPE(self), cidobj->cid, *(int *)end, + cidobj->channels, force, cidobj->resolve, + (queueid **)&obj); + if (err != 0) { + assert(obj == NULL); + PyObject *mod = get_module_from_type(Py_TYPE(self)); + if (mod == NULL) { + return NULL; + } + (void)handle_channel_error(err, mod, cidobj->cid); + Py_DECREF(mod); + return NULL; + } + assert(obj != NULL); + return obj; + } + + if (cidobj->end == CHANNEL_SEND) { + return PyUnicode_InternFromString("send"); + } + if (cidobj->end == CHANNEL_RECV) { + return PyUnicode_InternFromString("recv"); + } + return PyUnicode_InternFromString("both"); +} + +static int _queueid_end_send = CHANNEL_SEND; +static int _queueid_end_recv = CHANNEL_RECV; + +static PyGetSetDef queueid_getsets[] = { + {"end", (getter)queueid_end, NULL, + PyDoc_STR("'send', 'recv', or 'both'")}, + {"send", (getter)queueid_end, NULL, + PyDoc_STR("the 'send' end of the channel"), &_queueid_end_send}, + {"recv", (getter)queueid_end, NULL, + PyDoc_STR("the 'recv' end of the channel"), &_queueid_end_recv}, + {NULL} +}; + +PyDoc_STRVAR(queueid_doc, +"A channel ID identifies a channel and may be used as an int."); + +static PyType_Slot queueid_typeslots[] = { + {Py_tp_dealloc, (destructor)queueid_dealloc}, + {Py_tp_doc, (void *)queueid_doc}, + {Py_tp_repr, (reprfunc)queueid_repr}, + {Py_tp_str, (reprfunc)queueid_str}, + {Py_tp_hash, queueid_hash}, + {Py_tp_richcompare, queueid_richcompare}, + {Py_tp_getset, queueid_getsets}, + // number slots + {Py_nb_int, (unaryfunc)queueid_int}, + {Py_nb_index, (unaryfunc)queueid_int}, + {0, NULL}, +}; + +static PyType_Spec queueid_typespec = { + .name = MODULE_NAME ".ChannelID", + .basicsize = sizeof(queueid), + .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | + Py_TPFLAGS_DISALLOW_INSTANTIATION | Py_TPFLAGS_IMMUTABLETYPE), + .slots = queueid_typeslots, +}; + + +/* SendChannel and RecvChannel classes */ + +// XXX Use a new __xid__ protocol instead? + +static PyTypeObject * +_get_current_channelend_type(int end) +{ + module_state *state = _get_current_module_state(); + if (state == NULL) { + return NULL; + } + PyTypeObject *cls; + if (end == CHANNEL_SEND) { + cls = state->send_channel_type; + } + else { + assert(end == CHANNEL_RECV); + cls = state->recv_channel_type; + } + if (cls == NULL) { + PyObject *highlevel = PyImport_ImportModule("interpreters"); + if (highlevel == NULL) { + PyErr_Clear(); + highlevel = PyImport_ImportModule("test.support.interpreters"); + if (highlevel == NULL) { + return NULL; + } + } + Py_DECREF(highlevel); + if (end == CHANNEL_SEND) { + cls = state->send_channel_type; + } + else { + cls = state->recv_channel_type; + } + assert(cls != NULL); + } + return cls; +} + +static PyObject * +_channelend_from_xid(_PyCrossInterpreterData *data) +{ + queueid *cidobj = (queueid *)_queueid_from_xid(data); + if (cidobj == NULL) { + return NULL; + } + PyTypeObject *cls = _get_current_channelend_type(cidobj->end); + if (cls == NULL) { + Py_DECREF(cidobj); + return NULL; + } + PyObject *obj = PyObject_CallOneArg((PyObject *)cls, (PyObject *)cidobj); + Py_DECREF(cidobj); + return obj; +} + +static int +_channelend_shared(PyThreadState *tstate, PyObject *obj, + _PyCrossInterpreterData *data) +{ + PyObject *cidobj = PyObject_GetAttrString(obj, "_id"); + if (cidobj == NULL) { + return -1; + } + int res = _queueid_shared(tstate, cidobj, data); + Py_DECREF(cidobj); + if (res < 0) { + return -1; + } + data->new_object = _channelend_from_xid; + return 0; +} + +static int +set_channelend_types(PyObject *mod, PyTypeObject *send, PyTypeObject *recv) +{ + module_state *state = get_module_state(mod); + if (state == NULL) { + return -1; + } + struct xid_class_registry *xid_classes = &state->xid_classes; + + if (state->send_channel_type != NULL + || state->recv_channel_type != NULL) + { + PyErr_SetString(PyExc_TypeError, "already registered"); + return -1; + } + state->send_channel_type = (PyTypeObject *)Py_NewRef(send); + state->recv_channel_type = (PyTypeObject *)Py_NewRef(recv); + + if (register_xid_class(send, _channelend_shared, xid_classes)) { + return -1; + } + if (register_xid_class(recv, _channelend_shared, xid_classes)) { + return -1; + } + + return 0; +} + + +/* module level code ********************************************************/ + +/* globals is the process-global state for the module. It holds all + the data that we need to share between interpreters, so it cannot + hold PyObject values. */ +static struct globals { + int module_count; + _channels channels; +} _globals = {0}; + +static int +_globals_init(void) +{ + // XXX This isn't thread-safe. + _globals.module_count++; + if (_globals.module_count > 1) { + // Already initialized. + return 0; + } + + assert(_globals.channels.mutex == NULL); + PyThread_type_lock mutex = PyThread_allocate_lock(); + if (mutex == NULL) { + return ERR_CHANNELS_MUTEX_INIT; + } + _channels_init(&_globals.channels, mutex); + return 0; +} + +static void +_globals_fini(void) +{ + // XXX This isn't thread-safe. + _globals.module_count--; + if (_globals.module_count > 0) { + return; + } + + _channels_fini(&_globals.channels); +} + +static _channels * +_global_channels(void) { + return &_globals.channels; +} + + +static void +clear_interpreter(void *data) +{ + if (_globals.module_count == 0) { + return; + } + PyInterpreterState *interp = (PyInterpreterState *)data; + assert(interp == _get_current_interp()); + int64_t interpid = PyInterpreterState_GetID(interp); + _channels_clear_interpreter(&_globals.channels, interpid); +} + + +static PyObject * +queuesmod_create(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + int64_t cid = channel_create(&_globals.channels); + if (cid < 0) { + (void)handle_channel_error(-1, self, cid); + return NULL; + } + module_state *state = get_module_state(self); + if (state == NULL) { + return NULL; + } + PyObject *cidobj = NULL; + int err = newqueueid(state->QueueIDType, cid, 0, + &_globals.channels, 0, 0, + (queueid **)&cidobj); + if (handle_channel_error(err, self, cid)) { + assert(cidobj == NULL); + err = channel_destroy(&_globals.channels, cid); + if (handle_channel_error(err, self, cid)) { + // XXX issue a warning? + } + return NULL; + } + assert(cidobj != NULL); + assert(((queueid *)cidobj)->channels != NULL); + return cidobj; +} + +PyDoc_STRVAR(queuesmod_create_doc, +"channel_create() -> cid\n\ +\n\ +Create a new cross-interpreter channel and return a unique generated ID."); + +static PyObject * +queuesmod_destroy(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"cid", NULL}; + int64_t cid; + struct channel_id_converter_data cid_data = { + .module = self, + }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&:channel_destroy", kwlist, + channel_id_converter, &cid_data)) { + return NULL; + } + cid = cid_data.cid; + + int err = channel_destroy(&_globals.channels, cid); + if (handle_channel_error(err, self, cid)) { + return NULL; + } + Py_RETURN_NONE; +} + +PyDoc_STRVAR(queuesmod_destroy_doc, +"channel_destroy(cid)\n\ +\n\ +Close and finalize the channel. Afterward attempts to use the channel\n\ +will behave as though it never existed."); + +static PyObject * +queuesmod_list_all(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + int64_t count = 0; + int64_t *cids = _channels_list_all(&_globals.channels, &count); + if (cids == NULL) { + if (count == 0) { + return PyList_New(0); + } + return NULL; + } + PyObject *ids = PyList_New((Py_ssize_t)count); + if (ids == NULL) { + goto finally; + } + module_state *state = get_module_state(self); + if (state == NULL) { + Py_DECREF(ids); + ids = NULL; + goto finally; + } + int64_t *cur = cids; + for (int64_t i=0; i < count; cur++, i++) { + PyObject *cidobj = NULL; + int err = newqueueid(state->QueueIDType, *cur, 0, + &_globals.channels, 0, 0, + (queueid **)&cidobj); + if (handle_channel_error(err, self, *cur)) { + assert(cidobj == NULL); + Py_SETREF(ids, NULL); + break; + } + assert(cidobj != NULL); + PyList_SET_ITEM(ids, (Py_ssize_t)i, cidobj); + } + +finally: + PyMem_Free(cids); + return ids; +} + +PyDoc_STRVAR(queuesmod_list_all_doc, +"channel_list_all() -> [cid]\n\ +\n\ +Return the list of all IDs for active channels."); + +static PyObject * +queuesmod_list_interpreters(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"cid", "send", NULL}; + int64_t cid; /* Channel ID */ + struct channel_id_converter_data cid_data = { + .module = self, + }; + int send = 0; /* Send or receive end? */ + int64_t interpid; + PyObject *ids, *interpid_obj; + PyInterpreterState *interp; + + if (!PyArg_ParseTupleAndKeywords( + args, kwds, "O&$p:channel_list_interpreters", + kwlist, channel_id_converter, &cid_data, &send)) { + return NULL; + } + cid = cid_data.cid; + + ids = PyList_New(0); + if (ids == NULL) { + goto except; + } + + interp = PyInterpreterState_Head(); + while (interp != NULL) { + interpid = PyInterpreterState_GetID(interp); + assert(interpid >= 0); + int res = channel_is_associated(&_globals.channels, cid, interpid, send); + if (res < 0) { + (void)handle_channel_error(res, self, cid); + goto except; + } + if (res) { + interpid_obj = PyInterpreterState_GetIDObject(interp); + if (interpid_obj == NULL) { + goto except; + } + res = PyList_Insert(ids, 0, interpid_obj); + Py_DECREF(interpid_obj); + if (res < 0) { + goto except; + } + } + interp = PyInterpreterState_Next(interp); + } + + goto finally; + +except: + Py_CLEAR(ids); + +finally: + return ids; +} + +PyDoc_STRVAR(queuesmod_list_interpreters_doc, +"channel_list_interpreters(cid, *, send) -> [id]\n\ +\n\ +Return the list of all interpreter IDs associated with an end of the channel.\n\ +\n\ +The 'send' argument should be a boolean indicating whether to use the send or\n\ +receive end."); + + +static PyObject * +queuesmod_put(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"cid", "obj", "blocking", "timeout", NULL}; + struct channel_id_converter_data cid_data = { + .module = self, + }; + PyObject *obj; + int blocking = 1; + PyObject *timeout_obj = NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O|$pO:channel_send", kwlist, + channel_id_converter, &cid_data, &obj, + &blocking, &timeout_obj)) { + return NULL; + } + + int64_t cid = cid_data.cid; + PY_TIMEOUT_T timeout; + if (PyThread_ParseTimeoutArg(timeout_obj, blocking, &timeout) < 0) { + return NULL; + } + + /* Queue up the object. */ + int err = 0; + if (blocking) { + err = channel_send_wait(&_globals.channels, cid, obj, timeout); + } + else { + err = channel_send(&_globals.channels, cid, obj, NULL); + } + if (handle_channel_error(err, self, cid)) { + return NULL; + } + + Py_RETURN_NONE; +} + +PyDoc_STRVAR(queuesmod_put_doc, +"channel_send(cid, obj, blocking=True)\n\ +\n\ +Add the object's data to the channel's queue.\n\ +By default this waits for the object to be received."); + +static PyObject * +queuesmod_send_buffer(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"cid", "obj", "blocking", "timeout", NULL}; + struct channel_id_converter_data cid_data = { + .module = self, + }; + PyObject *obj; + int blocking = 1; + PyObject *timeout_obj = NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O&O|$pO:channel_send_buffer", kwlist, + channel_id_converter, &cid_data, &obj, + &blocking, &timeout_obj)) { + return NULL; + } + + int64_t cid = cid_data.cid; + PY_TIMEOUT_T timeout; + if (PyThread_ParseTimeoutArg(timeout_obj, blocking, &timeout) < 0) { + return NULL; + } + + PyObject *tempobj = PyMemoryView_FromObject(obj); + if (tempobj == NULL) { + return NULL; + } + + /* Queue up the object. */ + int err = 0; + if (blocking) { + err = channel_send_wait(&_globals.channels, cid, tempobj, timeout); + } + else { + err = channel_send(&_globals.channels, cid, tempobj, NULL); + } + Py_DECREF(tempobj); + if (handle_channel_error(err, self, cid)) { + return NULL; + } + + Py_RETURN_NONE; +} + +PyDoc_STRVAR(queuesmod_send_buffer_doc, +"channel_send_buffer(cid, obj, blocking=True)\n\ +\n\ +Add the object's buffer to the channel's queue.\n\ +By default this waits for the object to be received."); + +static PyObject * +queuesmod_get(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"cid", "default", NULL}; + int64_t cid; + struct channel_id_converter_data cid_data = { + .module = self, + }; + PyObject *dflt = NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&|O:channel_recv", kwlist, + channel_id_converter, &cid_data, &dflt)) { + return NULL; + } + cid = cid_data.cid; + + PyObject *obj = NULL; + int err = channel_recv(&_globals.channels, cid, &obj); + if (handle_channel_error(err, self, cid)) { + return NULL; + } + Py_XINCREF(dflt); + if (obj == NULL) { + // Use the default. + if (dflt == NULL) { + (void)handle_channel_error(ERR_CHANNEL_EMPTY, self, cid); + return NULL; + } + obj = Py_NewRef(dflt); + } + Py_XDECREF(dflt); + return obj; +} + +PyDoc_STRVAR(queuesmod_get_doc, +"channel_recv(cid, [default]) -> obj\n\ +\n\ +Return a new object from the data at the front of the channel's queue.\n\ +\n\ +If there is nothing to receive then raise ChannelEmptyError, unless\n\ +a default value is provided. In that case return it."); + +static PyObject * +queuesmod_close(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"cid", "send", "recv", "force", NULL}; + int64_t cid; + struct channel_id_converter_data cid_data = { + .module = self, + }; + int send = 0; + int recv = 0; + int force = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O&|$ppp:channel_close", kwlist, + channel_id_converter, &cid_data, + &send, &recv, &force)) { + return NULL; + } + cid = cid_data.cid; + + int err = channel_close(&_globals.channels, cid, send-recv, force); + if (handle_channel_error(err, self, cid)) { + return NULL; + } + Py_RETURN_NONE; +} + +PyDoc_STRVAR(queuesmod_close_doc, +"channel_close(cid, *, send=None, recv=None, force=False)\n\ +\n\ +Close the channel for all interpreters.\n\ +\n\ +If the channel is empty then the keyword args are ignored and both\n\ +ends are immediately closed. Otherwise, if 'force' is True then\n\ +all queued items are released and both ends are immediately\n\ +closed.\n\ +\n\ +If the channel is not empty *and* 'force' is False then following\n\ +happens:\n\ +\n\ + * recv is True (regardless of send):\n\ + - raise ChannelNotEmptyError\n\ + * recv is None and send is None:\n\ + - raise ChannelNotEmptyError\n\ + * send is True and recv is not True:\n\ + - fully close the 'send' end\n\ + - close the 'recv' end to interpreters not already receiving\n\ + - fully close it once empty\n\ +\n\ +Closing an already closed channel results in a ChannelClosedError.\n\ +\n\ +Once the channel's ID has no more ref counts in any interpreter\n\ +the channel will be destroyed."); + +static PyObject * +queuesmod_release(PyObject *self, PyObject *args, PyObject *kwds) +{ + // Note that only the current interpreter is affected. + static char *kwlist[] = {"cid", "send", "recv", "force", NULL}; + int64_t cid; + struct channel_id_converter_data cid_data = { + .module = self, + }; + int send = 0; + int recv = 0; + int force = 0; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O&|$ppp:channel_release", kwlist, + channel_id_converter, &cid_data, + &send, &recv, &force)) { + return NULL; + } + cid = cid_data.cid; + if (send == 0 && recv == 0) { + send = 1; + recv = 1; + } + + // XXX Handle force is True. + // XXX Fix implicit release. + + int err = channel_release(&_globals.channels, cid, send, recv); + if (handle_channel_error(err, self, cid)) { + return NULL; + } + Py_RETURN_NONE; +} + +PyDoc_STRVAR(queuesmod_release_doc, +"channel_release(cid, *, send=None, recv=None, force=True)\n\ +\n\ +Close the channel for the current interpreter. 'send' and 'recv'\n\ +(bool) may be used to indicate the ends to close. By default both\n\ +ends are closed. Closing an already closed end is a noop."); + +static PyObject * +queuesmod_get_info(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"cid", NULL}; + struct channel_id_converter_data cid_data = { + .module = self, + }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O&:_get_info", kwlist, + channel_id_converter, &cid_data)) { + return NULL; + } + int64_t cid = cid_data.cid; + + struct channel_info info; + int err = _channel_get_info(&_globals.channels, cid, &info); + if (handle_channel_error(err, self, cid)) { + return NULL; + } + return new_channel_info(self, &info); +} + +PyDoc_STRVAR(queuesmod_get_info_doc, +"get_info(cid)\n\ +\n\ +Return details about the channel."); + +static PyObject * +queuesmod__queue_id(PyObject *self, PyObject *args, PyObject *kwds) +{ + module_state *state = get_module_state(self); + if (state == NULL) { + return NULL; + } + PyTypeObject *cls = state->QueueIDType; + + PyObject *mod = get_module_from_owned_type(cls); + assert(mod == self); + Py_DECREF(mod); + + return _queueid_new(self, cls, args, kwds); +} + +static PyObject * +queuesmod__register_queue_type(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"send", "recv", NULL}; + PyObject *send; + PyObject *recv; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "OO:_register_queue_type", kwlist, + &send, &recv)) { + return NULL; + } + if (!PyType_Check(send)) { + PyErr_SetString(PyExc_TypeError, "expected a type for 'send'"); + return NULL; + } + if (!PyType_Check(recv)) { + PyErr_SetString(PyExc_TypeError, "expected a type for 'recv'"); + return NULL; + } + PyTypeObject *cls_send = (PyTypeObject *)send; + PyTypeObject *cls_recv = (PyTypeObject *)recv; + + if (set_channelend_types(self, cls_send, cls_recv) < 0) { + return NULL; + } + + Py_RETURN_NONE; +} + +static PyMethodDef module_functions[] = { + {"create", queuesmod_create, + METH_NOARGS, queuesmod_create_doc}, + {"destroy", _PyCFunction_CAST(queuesmod_destroy), + METH_VARARGS | METH_KEYWORDS, queuesmod_destroy_doc}, + {"list_all", queuesmod_list_all, + METH_NOARGS, queuesmod_list_all_doc}, + {"put", _PyCFunction_CAST(queuesmod_put), + METH_VARARGS | METH_KEYWORDS, queuesmod_put_doc}, + {"get", _PyCFunction_CAST(queuesmod_get), + METH_VARARGS | METH_KEYWORDS, queuesmod_get_doc}, + {"close", _PyCFunction_CAST(queuesmod_close), + METH_VARARGS | METH_KEYWORDS, queuesmod_close_doc}, + {"release", _PyCFunction_CAST(queuesmod_release), + METH_VARARGS | METH_KEYWORDS, queuesmod_release_doc}, + {"get_info", _PyCFunction_CAST(queuesmod_get_info), + METH_VARARGS | METH_KEYWORDS, queuesmod_get_info_doc}, + {"_queue_id", _PyCFunction_CAST(queuesmod__queue_id), + METH_VARARGS | METH_KEYWORDS, NULL}, +// {"_register_queue_type", _PyCFunction_CAST(queuesmod__register_queue_type), +// METH_VARARGS | METH_KEYWORDS, NULL}, + + {NULL, NULL} /* sentinel */ +}; + + +/* initialization function */ + +PyDoc_STRVAR(module_doc, +"This module provides primitive operations to manage Python interpreters.\n\ +The 'interpreters' module provides a more convenient interface."); + +static int +module_exec(PyObject *mod) +{ + if (_globals_init() != 0) { + return -1; + } + struct xid_class_registry *xid_classes = NULL; + + module_state *state = get_module_state(mod); + if (state == NULL) { + goto error; + } + xid_classes = &state->xid_classes; + + /* Add exception types */ + if (exceptions_init(mod) != 0) { + goto error; + } + + /* Add other types */ + + // ChannelInfo + state->ChannelInfoType = PyStructSequence_NewType(&channel_info_desc); + if (state->ChannelInfoType == NULL) { + goto error; + } + if (PyModule_AddType(mod, state->ChannelInfoType) < 0) { + goto error; + } + + // ChannelID + state->QueueIDType = add_new_type( + mod, &queueid_typespec, _queueid_shared, xid_classes); + if (state->QueueIDType == NULL) { + goto error; + } + + // XIBufferView + state->XIBufferViewType = add_new_type(mod, &XIBufferViewType_spec, NULL, + xid_classes); + if (state->XIBufferViewType == NULL) { + goto error; + } + + // Register external types. + if (register_builtin_xid_types(xid_classes) < 0) { + goto error; + } + + /* Make sure chnnels drop objects owned by this interpreter. */ + PyInterpreterState *interp = _get_current_interp(); + PyUnstable_AtExit(interp, clear_interpreter, (void *)interp); + + return 0; + +error: + if (xid_classes != NULL) { + clear_xid_class_registry(xid_classes); + } + _globals_fini(); + return -1; +} + +static struct PyModuleDef_Slot module_slots[] = { + {Py_mod_exec, module_exec}, + {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, + {0, NULL}, +}; + +static int +module_traverse(PyObject *mod, visitproc visit, void *arg) +{ + module_state *state = get_module_state(mod); + assert(state != NULL); + traverse_module_state(state, visit, arg); + return 0; +} + +static int +module_clear(PyObject *mod) +{ + module_state *state = get_module_state(mod); + assert(state != NULL); + + // Before clearing anything, we unregister the various XID types. */ + clear_xid_class_registry(&state->xid_classes); + + // Now we clear the module state. + clear_module_state(state); + return 0; +} + +static void +module_free(void *mod) +{ + module_state *state = get_module_state(mod); + assert(state != NULL); + + // Before clearing anything, we unregister the various XID types. */ + clear_xid_class_registry(&state->xid_classes); + + // Now we clear the module state. + clear_module_state(state); + + _globals_fini(); +} + +static struct PyModuleDef moduledef = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = MODULE_NAME, + .m_doc = module_doc, + .m_size = sizeof(module_state), + .m_methods = module_functions, + .m_slots = module_slots, + .m_traverse = module_traverse, + .m_clear = module_clear, + .m_free = (freefunc)module_free, +}; + +PyMODINIT_FUNC +PyInit__xxinterpqueues(void) +{ + return PyModuleDef_Init(&moduledef); +} diff --git a/PC/config.c b/PC/config.c index da2bde640961e0..f754ce6d3b057b 100644 --- a/PC/config.c +++ b/PC/config.c @@ -37,6 +37,7 @@ extern PyObject* PyInit__weakref(void); extern PyObject* PyInit_xxsubtype(void); extern PyObject* PyInit__xxsubinterpreters(void); extern PyObject* PyInit__xxinterpchannels(void); +extern PyObject* PyInit__xxinterpqueues(void); extern PyObject* PyInit__random(void); extern PyObject* PyInit_itertools(void); extern PyObject* PyInit__collections(void); @@ -142,6 +143,7 @@ struct _inittab _PyImport_Inittab[] = { {"xxsubtype", PyInit_xxsubtype}, {"_xxsubinterpreters", PyInit__xxsubinterpreters}, {"_xxinterpchannels", PyInit__xxinterpchannels}, + {"_xxinterpqueues", PyInit__xxinterpqueues}, #ifdef _Py_HAVE_ZLIB {"zlib", PyInit_zlib}, #endif diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index 278f1f5622543c..778fc834c0db9c 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -458,6 +458,7 @@ + diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index c9b34c64fbf75f..a96ca24cf08b66 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -1505,6 +1505,9 @@ Modules + + Modules + Parser diff --git a/Tools/build/generate_stdlib_module_names.py b/Tools/build/generate_stdlib_module_names.py index 766a85d3d6f39e..5dce4e042d1eb4 100644 --- a/Tools/build/generate_stdlib_module_names.py +++ b/Tools/build/generate_stdlib_module_names.py @@ -36,6 +36,7 @@ '_testsinglephase', '_xxsubinterpreters', '_xxinterpchannels', + '_xxinterpqueues', '_xxtestfuzz', 'idlelib.idle_test', 'test', diff --git a/configure b/configure index cad3bce0c7de87..b5dcad96f35414 100755 --- a/configure +++ b/configure @@ -771,6 +771,8 @@ MODULE__ZONEINFO_FALSE MODULE__ZONEINFO_TRUE MODULE__XXINTERPCHANNELS_FALSE MODULE__XXINTERPCHANNELS_TRUE +MODULE__XXINTERPQUEUES_FALSE +MODULE__XXINTERPQUEUES_TRUE MODULE__XXSUBINTERPRETERS_FALSE MODULE__XXSUBINTERPRETERS_TRUE MODULE__TYPING_FALSE @@ -28025,6 +28027,7 @@ case $ac_sys_system in #( py_cv_module__tkinter=n/a py_cv_module__xxsubinterpreters=n/a py_cv_module__xxinterpchannels=n/a + py_cv_module__xxinterpqueues=n/a py_cv_module_grp=n/a py_cv_module_pwd=n/a py_cv_module_resource=n/a @@ -28524,6 +28527,28 @@ then : +fi + + + if test "$py_cv_module__xxinterpqueues" != "n/a" +then : + py_cv_module__xxinterpqueues=yes +fi + if test "$py_cv_module__xxinterpqueues" = yes; then + MODULE__XXINTERPQUEUES_TRUE= + MODULE__XXINTERPQUEUES_FALSE='#' +else + MODULE__XXINTERPQUEUES_TRUE='#' + MODULE__XXINTERPQUEUES_FALSE= +fi + + as_fn_append MODULE_BLOCK "MODULE__XXINTERPQUEUES_STATE=$py_cv_module__xxinterpqueues$as_nl" + if test "x$py_cv_module__xxinterpqueues" = xyes +then : + + + + fi @@ -30760,6 +30785,10 @@ if test -z "${MODULE__XXINTERPCHANNELS_TRUE}" && test -z "${MODULE__XXINTERPCHAN as_fn_error $? "conditional \"MODULE__XXINTERPCHANNELS\" was never defined. Usually this means the macro was only invoked conditionally." "$LINENO" 5 fi +if test -z "${MODULE__XXINTERPQUEUES_TRUE}" && test -z "${MODULE__XXINTERPQUEUES_FALSE}"; then + as_fn_error $? "conditional \"MODULE__XXINTERPQUEUESS\" was never defined. +Usually this means the macro was only invoked conditionally." "$LINENO" 5 +fi if test -z "${MODULE__ZONEINFO_TRUE}" && test -z "${MODULE__ZONEINFO_FALSE}"; then as_fn_error $? "conditional \"MODULE__ZONEINFO\" was never defined. Usually this means the macro was only invoked conditionally." "$LINENO" 5 diff --git a/configure.ac b/configure.ac index 7dda0b3fff95be..020553abd71b4f 100644 --- a/configure.ac +++ b/configure.ac @@ -7120,6 +7120,7 @@ AS_CASE([$ac_sys_system], [_tkinter], [_xxsubinterpreters], [_xxinterpchannels], + [_xxinterpqueues], [grp], [pwd], [resource], @@ -7236,6 +7237,7 @@ PY_STDLIB_MOD_SIMPLE([_struct]) PY_STDLIB_MOD_SIMPLE([_typing]) PY_STDLIB_MOD_SIMPLE([_xxsubinterpreters]) PY_STDLIB_MOD_SIMPLE([_xxinterpchannels]) +PY_STDLIB_MOD_SIMPLE([_xxinterpqueues]) PY_STDLIB_MOD_SIMPLE([_zoneinfo]) dnl multiprocessing modules From 5c604e0fec946c2fbdb32804c2a9d7c9f2dca508 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Fri, 1 Dec 2023 10:17:05 -0700 Subject: [PATCH 02/14] Adjust the _xxinterpqueues implementation. --- Lib/test/support/interpreters/queues.py | 71 +- Lib/test/test_interpreters/test_queues.py | 53 +- Modules/_xxinterpqueuesmodule.c | 3283 ++++----------------- 3 files changed, 631 insertions(+), 2776 deletions(-) diff --git a/Lib/test/support/interpreters/queues.py b/Lib/test/support/interpreters/queues.py index ed6b0d551dd890..cd18c14a477b60 100644 --- a/Lib/test/support/interpreters/queues.py +++ b/Lib/test/support/interpreters/queues.py @@ -3,13 +3,11 @@ import queue import time import weakref -import _xxinterpchannels as _channels -import _xxinterpchannels as _queues +import _xxinterpqueues as _queues # aliases: -from _xxinterpchannels import ( - ChannelError as QueueError, - ChannelNotFoundError as QueueNotFoundError, +from _xxinterpqueues import ( + QueueError, QueueNotFoundError, ) __all__ = [ @@ -26,7 +24,7 @@ def create(maxsize=0): """ # XXX honor maxsize qid = _queues.create() - return Queue._with_maxsize(qid, maxsize) + return Queue(qid, _maxsize=maxsize) def list_all(): @@ -35,14 +33,14 @@ def list_all(): for qid in _queues.list_all()] -class QueueEmpty(queue.Empty): +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(queue.Full): +class QueueFull(QueueError, queue.Full): """Raised from put_nowait() when the queue is full. It is also raised from put() if it times out. @@ -55,33 +53,44 @@ class Queue: """A cross-interpreter queue.""" @classmethod - def _with_maxsize(cls, id, maxsize): - if not isinstance(maxsize, int): + def _resolve_maxsize(cls, maxsize): + if maxsize is None: + maxsize = 0 + elif not isinstance(maxsize, int): raise TypeError(f'maxsize must be an int, got {maxsize!r}') elif maxsize < 0: maxsize = 0 else: maxsize = int(maxsize) - self = cls(id) - self._maxsize = maxsize - return self + return maxsize - def __new__(cls, id, /): + def __new__(cls, id, /, *, _maxsize=None): # There is only one instance for any given ID. if isinstance(id, int): - id = _channels._channel_id(id, force=False) - elif not isinstance(id, _channels.ChannelID): + id = int(id) + else: raise TypeError(f'id must be an int, got {id!r}') - key = int(id) try: - self = _known_queues[key] + self = _known_queues[id] except KeyError: + maxsize = cls._resolve_maxsize(_maxsize) self = super().__new__(cls) self._id = id - self._maxsize = 0 - _known_queues[key] = self + self._maxsize = maxsize + _known_queues[id] = self + _queues.bind(id) + else: + if _maxsize is not None: + raise Exception('maxsize may not be changed') return self + def __del__(self): + _queues.release(self._id) + try: + del _known_queues[self._id] + except KeyError: + pass + def __repr__(self): return f'{type(self).__name__}({self.id})' @@ -90,34 +99,30 @@ def __hash__(self): @property def id(self): - return int(self._id) + return self._id @property def maxsize(self): return self._maxsize - @property - def _info(self): - return _channels.get_info(self._id) - def empty(self): - return self._info.count == 0 + return self.qsize() == 0 def full(self): if self._maxsize <= 0: return False - return self._info.count >= self._maxsize + return self.qsize() >= self._maxsize def qsize(self): - return self._info.count + return _queues.get_count(self._id) def put(self, obj, timeout=None): # XXX block if full - _channels.send(self._id, obj, blocking=False) + _queues.put(self._id, obj) def put_nowait(self, obj): # XXX raise QueueFull if full - return _channels.send(self._id, obj, blocking=False) + return _queues.put(self._id, obj) def get(self, timeout=None, *, _sentinel=object(), @@ -132,12 +137,12 @@ def get(self, timeout=None, *, if timeout < 0: raise ValueError(f'timeout value must be non-negative') end = time.time() + timeout - obj = _channels.recv(self._id, _sentinel) + obj = _queues.get(self._id, _sentinel) while obj is _sentinel: time.sleep(_delay) if timeout is not None and time.time() >= end: raise QueueEmpty - obj = _channels.recv(self._id, _sentinel) + obj = _queues.get(self._id, _sentinel) return obj def get_nowait(self, *, _sentinel=object()): @@ -146,7 +151,7 @@ def get_nowait(self, *, _sentinel=object()): If the queue is empty then raise QueueEmpty. Otherwise this is the same as get(). """ - obj = _channels.recv(self._id, _sentinel) + obj = _queues.get(self._id, _sentinel) if obj is _sentinel: raise QueueEmpty return obj diff --git a/Lib/test/test_interpreters/test_queues.py b/Lib/test/test_interpreters/test_queues.py index 2af90b14d3e3c4..2a7f3246e2c461 100644 --- a/Lib/test/test_interpreters/test_queues.py +++ b/Lib/test/test_interpreters/test_queues.py @@ -5,13 +5,21 @@ from test.support import import_helper # Raise SkipTest if subinterpreters not supported. -import_helper.import_module('_xxinterpchannels') -#import_helper.import_module('_xxinterpqueues') +_queues = import_helper.import_module('_xxinterpqueues') from test.support import interpreters from test.support.interpreters import queues from .utils import _run_output, TestBase +class TestBase(TestBase): + def tearDown(self): + for qid in _queues.list_all(): + try: + _queues.destroy(qid) + except Exception: + pass + + class QueueTests(TestBase): def test_create(self): @@ -179,31 +187,64 @@ def test_put_get_same_interpreter(self): assert obj is not orig, 'expected: obj is not orig' """)) - @unittest.expectedFailure def test_put_get_different_interpreters(self): + interp = interpreters.create() queue1 = queues.create() queue2 = queues.create() + self.assertEqual(len(queues.list_all()), 2) + obj1 = b'spam' queue1.put(obj1) + out = _run_output( - interpreters.create(), + interp, dedent(f""" - import test.support.interpreters.queue as queues + from test.support.interpreters import queues queue1 = queues.Queue({queue1.id}) queue2 = queues.Queue({queue2.id}) + assert queue1.qsize() == 1, 'expected: queue1.qsize() == 1' obj = queue1.get() + assert queue1.qsize() == 0, 'expected: queue1.qsize() == 0' assert obj == b'spam', 'expected: obj == obj1' # When going to another interpreter we get a copy. assert id(obj) != {id(obj1)}, 'expected: obj is not obj1' obj2 = b'eggs' print(id(obj2)) + assert queue2.qsize() == 0, 'expected: queue2.qsize() == 0' queue2.put(obj2) + assert queue2.qsize() == 1, 'expected: queue2.qsize() == 1' """)) - obj2 = queue2.get() + self.assertEqual(len(queues.list_all()), 2) + self.assertEqual(queue1.qsize(), 0) + self.assertEqual(queue2.qsize(), 1) + obj2 = queue2.get() self.assertEqual(obj2, b'eggs') self.assertNotEqual(id(obj2), int(out)) + def test_put_cleared_with_subinterpreter(self): + interp = interpreters.create() + queue = queues.create() + + out = _run_output( + interp, + dedent(f""" + from test.support.interpreters import queues + queue = queues.Queue({queue.id}) + obj1 = b'spam' + obj2 = b'eggs' + queue.put(obj1) + queue.put(obj2) + """)) + self.assertEqual(queue.qsize(), 2) + + obj1 = queue.get() + self.assertEqual(obj1, b'spam') + self.assertEqual(queue.qsize(), 1) + + del interp + self.assertEqual(queue.qsize(), 0) + def test_put_get_different_threads(self): queue1 = queues.create() queue2 = queues.create() diff --git a/Modules/_xxinterpqueuesmodule.c b/Modules/_xxinterpqueuesmodule.c index b95de5dd05a72a..c5fe52929d98e1 100644 --- a/Modules/_xxinterpqueuesmodule.c +++ b/Modules/_xxinterpqueuesmodule.c @@ -6,80 +6,8 @@ #endif #include "Python.h" -#include "interpreteridobject.h" #include "pycore_crossinterp.h" // struct _xid -#include "pycore_pybuffer.h" // _PyBuffer_ReleaseInInterpreterAndRawFree() -#include "pycore_interp.h" // _PyInterpreterState_LookUpID() - -#ifdef MS_WINDOWS -#define WIN32_LEAN_AND_MEAN -#include // SwitchToThread() -#elif defined(HAVE_SCHED_H) -#include // sched_yield() -#endif - -/* -This module has the following process-global state: - -_globals (static struct globals): - module_count (int) - channels (struct _channels): - numopen (int64_t) - next_id; (int64_t) - mutex (PyThread_type_lock) - head (linked list of struct _channelref *): - cid (int64_t) - objcount (Py_ssize_t) - next (struct _channelref *): - ... - chan (struct _channel *): - open (int) - mutex (PyThread_type_lock) - closing (struct _channel_closing *): - ref (struct _channelref *): - ... - ends (struct _channelends *): - numsendopen (int64_t) - numrecvopen (int64_t) - send (struct _channelend *): - interpid (int64_t) - open (int) - next (struct _channelend *) - recv (struct _channelend *): - ... - queue (struct _channelqueue *): - count (int64_t) - first (struct _channelitem *): - next (struct _channelitem *): - ... - data (_PyCrossInterpreterData *): - data (void *) - obj (PyObject *) - interpid (int64_t) - new_object (xid_newobjectfunc) - free (xid_freefunc) - last (struct _channelitem *): - ... - -The above state includes the following allocations by the module: - -* 1 top-level mutex (to protect the rest of the state) -* for each channel: - * 1 struct _channelref - * 1 struct _channel - * 0-1 struct _channel_closing - * 1 struct _channelends - * 2 struct _channelend - * 1 struct _channelqueue -* for each item in each channel: - * 1 struct _channelitem - * 1 _PyCrossInterpreterData - -The only objects in that global state are the references held by each -channel's queue, which are safely managed via the _PyCrossInterpreterData_*() -API.. The module does not create any objects that are shared globally. -*/ #define MODULE_NAME "_xxinterpqueues" @@ -90,39 +18,6 @@ API.. The module does not create any objects that are shared globally. PyMem_RawFree(VAR) -struct xid_class_registry { - size_t count; -#define MAX_XID_CLASSES 5 - struct { - PyTypeObject *cls; - } added[MAX_XID_CLASSES]; -}; - -static int -register_xid_class(PyTypeObject *cls, crossinterpdatafunc shared, - struct xid_class_registry *classes) -{ - return 0; - int res = _PyCrossInterpreterData_RegisterClass(cls, shared); - if (res == 0) { - assert(classes->count < MAX_XID_CLASSES); - // The class has refs elsewhere, so we need to incref here. - classes->added[classes->count].cls = cls; - classes->count += 1; - } - return res; -} - -static void -clear_xid_class_registry(struct xid_class_registry *classes) -{ - while (classes->count > 0) { - classes->count -= 1; - PyTypeObject *cls = classes->added[classes->count].cls; - _PyCrossInterpreterData_UnregisterClass(cls); - } -} - #define XID_IGNORE_EXC 1 #define XID_FREE 2 @@ -166,42 +61,6 @@ _get_current_interp(void) return PyInterpreterState_Get(); } -static PyObject * -_get_current_module(void) -{ - PyObject *name = PyUnicode_FromString(MODULE_NAME); - if (name == NULL) { - return NULL; - } - PyObject *mod = PyImport_GetModule(name); - Py_DECREF(name); - if (mod == NULL) { - return NULL; - } - assert(mod != Py_None); - return mod; -} - -static PyObject * -get_module_from_owned_type(PyTypeObject *cls) -{ - assert(cls != NULL); - return _get_current_module(); - // XXX Use the more efficient API now that we use heap types: - //return PyType_GetModule(cls); -} - -static struct PyModuleDef moduledef; - -static PyObject * -get_module_from_type(PyTypeObject *cls) -{ - assert(cls != NULL); - return _get_current_module(); - // XXX Use the more efficient API now that we use heap types: - //return PyType_GetModuleByDef(cls, &moduledef); -} - static PyObject * add_new_exception(PyObject *mod, const char *name, PyObject *base) { @@ -221,199 +80,59 @@ add_new_exception(PyObject *mod, const char *name, PyObject *base) #define ADD_NEW_EXCEPTION(MOD, NAME, BASE) \ add_new_exception(MOD, MODULE_NAME "." Py_STRINGIFY(NAME), BASE) -static PyTypeObject * -add_new_type(PyObject *mod, PyType_Spec *spec, crossinterpdatafunc shared, - struct xid_class_registry *classes) -{ - PyTypeObject *cls = (PyTypeObject *)PyType_FromModuleAndSpec( - mod, spec, NULL); - if (cls == NULL) { - return NULL; - } - if (PyModule_AddType(mod, cls) < 0) { - Py_DECREF(cls); - return NULL; - } - if (shared != NULL) { - if (register_xid_class(cls, shared, classes)) { - Py_DECREF(cls); - return NULL; - } - } - return cls; -} - -static int -wait_for_lock(PyThread_type_lock mutex, PY_TIMEOUT_T timeout) -{ - PyLockStatus res = PyThread_acquire_lock_timed_with_retries(mutex, timeout); - if (res == PY_LOCK_INTR) { - /* KeyboardInterrupt, etc. */ - assert(PyErr_Occurred()); - return -1; - } - else if (res == PY_LOCK_FAILURE) { - assert(!PyErr_Occurred()); - assert(timeout > 0); - PyErr_SetString(PyExc_TimeoutError, "timed out"); - return -1; - } - assert(res == PY_LOCK_ACQUIRED); - PyThread_release_lock(mutex); - return 0; -} - - -/* Cross-interpreter Buffer Views *******************************************/ - -// XXX Release when the original interpreter is destroyed. - -typedef struct { - PyObject_HEAD - Py_buffer *view; - int64_t interpid; -} XIBufferViewObject; - -static PyObject * -xibufferview_from_xid(PyTypeObject *cls, _PyCrossInterpreterData *data) -{ - assert(data->data != NULL); - assert(data->obj == NULL); - assert(data->interpid >= 0); - XIBufferViewObject *self = PyObject_Malloc(sizeof(XIBufferViewObject)); - if (self == NULL) { - return NULL; - } - PyObject_Init((PyObject *)self, cls); - self->view = (Py_buffer *)data->data; - self->interpid = data->interpid; - return (PyObject *)self; -} -static void -xibufferview_dealloc(XIBufferViewObject *self) -{ - PyInterpreterState *interp = _PyInterpreterState_LookUpID(self->interpid); - /* If the interpreter is no longer alive then we have problems, - since other objects may be using the buffer still. */ - assert(interp != NULL); - - if (_PyBuffer_ReleaseInInterpreterAndRawFree(interp, self->view) < 0) { - // XXX Emit a warning? - PyErr_Clear(); - } - - PyTypeObject *tp = Py_TYPE(self); - tp->tp_free(self); - /* "Instances of heap-allocated types hold a reference to their type." - * See: https://docs.python.org/3.11/howto/isolating-extensions.html#garbage-collection-protocol - * See: https://docs.python.org/3.11/c-api/typeobj.html#c.PyTypeObject.tp_traverse - */ - // XXX Why don't we implement Py_TPFLAGS_HAVE_GC, e.g. Py_tp_traverse, - // like we do for _abc._abc_data? - Py_DECREF(tp); -} +struct idarg_int64_converter_data { + // input: + const char *label; + // output: + int64_t id; +}; static int -xibufferview_getbuf(XIBufferViewObject *self, Py_buffer *view, int flags) +idarg_int64_converter(PyObject *arg, void *ptr) { - /* Only PyMemoryView_FromObject() should ever call this, - via _memoryview_from_xid() below. */ - *view = *self->view; - view->obj = (PyObject *)self; - // XXX Should we leave it alone? - view->internal = NULL; - return 0; -} - -static PyType_Slot XIBufferViewType_slots[] = { - {Py_tp_dealloc, (destructor)xibufferview_dealloc}, - {Py_bf_getbuffer, (getbufferproc)xibufferview_getbuf}, - // We don't bother with Py_bf_releasebuffer since we don't need it. - {0, NULL}, -}; - -static PyType_Spec XIBufferViewType_spec = { - .name = MODULE_NAME ".CrossInterpreterBufferView", - .basicsize = sizeof(XIBufferViewObject), - .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | - Py_TPFLAGS_DISALLOW_INSTANTIATION | Py_TPFLAGS_IMMUTABLETYPE), - .slots = XIBufferViewType_slots, -}; + int64_t id; + struct idarg_int64_converter_data *data = ptr; - -/* extra XID types **********************************************************/ - -static PyTypeObject * _get_current_xibufferview_type(void); - -static PyObject * -_memoryview_from_xid(_PyCrossInterpreterData *data) -{ - PyTypeObject *cls = _get_current_xibufferview_type(); - if (cls == NULL) { - return NULL; - } - PyObject *obj = xibufferview_from_xid(cls, data); - if (obj == NULL) { - return NULL; + const char *label = data->label; + if (label == NULL) { + label = "ID"; } - return PyMemoryView_FromObject(obj); -} -static int -_memoryview_shared(PyThreadState *tstate, PyObject *obj, - _PyCrossInterpreterData *data) -{ - Py_buffer *view = PyMem_RawMalloc(sizeof(Py_buffer)); - if (view == NULL) { - return -1; - } - if (PyObject_GetBuffer(obj, view, PyBUF_FULL_RO) < 0) { - PyMem_RawFree(view); - return -1; + if (PyIndex_Check(arg)) { + int overflow = 0; + id = PyLong_AsLongLongAndOverflow(arg, &overflow); + if (id == -1 && PyErr_Occurred()) { + return 0; + } + else if (id == -1 && overflow == 1) { + PyErr_Format(PyExc_OverflowError, + "max %s is %lld, got %R", label, INT64_MAX, arg); + return 0; + } + else if (id < 0) { + PyErr_Format(PyExc_ValueError, + "%s must be a non-negative int, got %R", label, arg); + return 0; + } } - _PyCrossInterpreterData_Init(data, tstate->interp, view, NULL, - _memoryview_from_xid); - return 0; -} - -static int -register_builtin_xid_types(struct xid_class_registry *classes) -{ - PyTypeObject *cls; - crossinterpdatafunc func; - - // builtin memoryview - cls = &PyMemoryView_Type; - func = _memoryview_shared; - if (register_xid_class(cls, func, classes)) { - return -1; + else { + PyErr_Format(PyExc_TypeError, + "%s must be an int, got %.100s", + label, Py_TYPE(arg)->tp_name); + return 0; } - - return 0; + data->id = id; + return 1; } /* module state *************************************************************/ typedef struct { - struct xid_class_registry xid_classes; - - /* Added at runtime by interpreters module. */ - PyTypeObject *send_channel_type; - PyTypeObject *recv_channel_type; - - /* heap types */ - PyTypeObject *ChannelInfoType; - PyTypeObject *QueueIDType; - PyTypeObject *XIBufferViewType; - /* exceptions */ - PyObject *ChannelError; - PyObject *ChannelNotFoundError; - PyObject *ChannelClosedError; - PyObject *ChannelEmptyError; - PyObject *ChannelNotEmptyError; + PyObject *QueueError; + PyObject *QueueNotFoundError; } module_state; static inline module_state * @@ -425,39 +144,12 @@ get_module_state(PyObject *mod) return state; } -static module_state * -_get_current_module_state(void) -{ - PyObject *mod = _get_current_module(); - if (mod == NULL) { - // XXX import it? - PyErr_SetString(PyExc_RuntimeError, - MODULE_NAME " module not imported yet"); - return NULL; - } - module_state *state = get_module_state(mod); - Py_DECREF(mod); - return state; -} - static int traverse_module_state(module_state *state, visitproc visit, void *arg) { - /* external types */ - Py_VISIT(state->send_channel_type); - Py_VISIT(state->recv_channel_type); - - /* heap types */ - Py_VISIT(state->ChannelInfoType); - Py_VISIT(state->QueueIDType); - Py_VISIT(state->XIBufferViewType); - /* exceptions */ - Py_VISIT(state->ChannelError); - Py_VISIT(state->ChannelNotFoundError); - Py_VISIT(state->ChannelClosedError); - Py_VISIT(state->ChannelEmptyError); - Py_VISIT(state->ChannelNotEmptyError); + Py_VISIT(state->QueueError); + Py_VISIT(state->QueueNotFoundError); return 0; } @@ -465,58 +157,23 @@ traverse_module_state(module_state *state, visitproc visit, void *arg) static int clear_module_state(module_state *state) { - /* external types */ - Py_CLEAR(state->send_channel_type); - Py_CLEAR(state->recv_channel_type); - - /* heap types */ - Py_CLEAR(state->ChannelInfoType); - if (state->QueueIDType != NULL) { - (void)_PyCrossInterpreterData_UnregisterClass(state->QueueIDType); - } - Py_CLEAR(state->QueueIDType); - Py_CLEAR(state->XIBufferViewType); - /* exceptions */ - Py_CLEAR(state->ChannelError); - Py_CLEAR(state->ChannelNotFoundError); - Py_CLEAR(state->ChannelClosedError); - Py_CLEAR(state->ChannelEmptyError); - Py_CLEAR(state->ChannelNotEmptyError); + Py_CLEAR(state->QueueError); + Py_CLEAR(state->QueueNotFoundError); return 0; } -static PyTypeObject * -_get_current_xibufferview_type(void) -{ - module_state *state = _get_current_module_state(); - if (state == NULL) { - return NULL; - } - return state->XIBufferViewType; -} - - -/* channel-specific code ****************************************************/ - -#define CHANNEL_SEND 1 -#define CHANNEL_BOTH 0 -#define CHANNEL_RECV -1 +/* queue-specific code ******************************************************/ +/* queue errors */ -/* channel errors */ - -#define ERR_CHANNEL_NOT_FOUND -2 -#define ERR_CHANNEL_CLOSED -3 -#define ERR_CHANNEL_INTERP_CLOSED -4 -#define ERR_CHANNEL_EMPTY -5 -#define ERR_CHANNEL_NOT_EMPTY -6 -#define ERR_CHANNEL_MUTEX_INIT -7 -#define ERR_CHANNELS_MUTEX_INIT -8 -#define ERR_NO_NEXT_CHANNEL_ID -9 -#define ERR_CHANNEL_CLOSED_WAITING -10 +#define ERR_QUEUE_NOT_FOUND -2 +#define ERR_QUEUE_EMPTY -5 +#define ERR_QUEUE_MUTEX_INIT -7 +#define ERR_QUEUES_MUTEX_INIT -8 +#define ERR_NO_NEXT_QUEUE_ID -9 static int exceptions_init(PyObject *mod) @@ -535,23 +192,17 @@ exceptions_init(PyObject *mod) } \ } while (0) - // A channel-related operation failed. - ADD(ChannelError, PyExc_RuntimeError); - // An operation tried to use a channel that doesn't exist. - ADD(ChannelNotFoundError, state->ChannelError); - // An operation tried to use a closed channel. - ADD(ChannelClosedError, state->ChannelError); - // An operation tried to pop from an empty channel. - ADD(ChannelEmptyError, state->ChannelError); - // An operation tried to close a non-empty channel. - ADD(ChannelNotEmptyError, state->ChannelError); + // A queue-related operation failed. + ADD(QueueError, PyExc_RuntimeError); + // An operation tried to use a queue that doesn't exist. + ADD(QueueNotFoundError, state->QueueError); #undef ADD return 0; } static int -handle_channel_error(int err, PyObject *mod, int64_t cid) +handle_queue_error(int err, PyObject *mod, int64_t qid) { if (err == 0) { assert(!PyErr_Occurred()); @@ -560,43 +211,27 @@ handle_channel_error(int err, PyObject *mod, int64_t cid) assert(err < 0); module_state *state = get_module_state(mod); assert(state != NULL); - if (err == ERR_CHANNEL_NOT_FOUND) { - PyErr_Format(state->ChannelNotFoundError, - "channel %" PRId64 " not found", cid); - } - else if (err == ERR_CHANNEL_CLOSED) { - PyErr_Format(state->ChannelClosedError, - "channel %" PRId64 " is closed", cid); + if (err == ERR_QUEUE_NOT_FOUND) { + PyErr_Format(state->QueueNotFoundError, + "queue %" PRId64 " not found", qid); } - else if (err == ERR_CHANNEL_CLOSED_WAITING) { - PyErr_Format(state->ChannelClosedError, - "channel %" PRId64 " has closed", cid); + else if (err == ERR_QUEUE_EMPTY) { + // XXX + PyErr_Format(state->QueueError, + //PyErr_Format(state->QueueEmpty, + "queue %" PRId64 " is empty", qid); } - else if (err == ERR_CHANNEL_INTERP_CLOSED) { - PyErr_Format(state->ChannelClosedError, - "channel %" PRId64 " is already closed", cid); + else if (err == ERR_QUEUE_MUTEX_INIT) { + PyErr_SetString(state->QueueError, + "can't initialize mutex for new queue"); } - else if (err == ERR_CHANNEL_EMPTY) { - PyErr_Format(state->ChannelEmptyError, - "channel %" PRId64 " is empty", cid); + else if (err == ERR_QUEUES_MUTEX_INIT) { + PyErr_SetString(state->QueueError, + "can't initialize mutex for queue management"); } - else if (err == ERR_CHANNEL_NOT_EMPTY) { - PyErr_Format(state->ChannelNotEmptyError, - "channel %" PRId64 " may not be closed " - "if not empty (try force=True)", - cid); - } - else if (err == ERR_CHANNEL_MUTEX_INIT) { - PyErr_SetString(state->ChannelError, - "can't initialize mutex for new channel"); - } - else if (err == ERR_CHANNELS_MUTEX_INIT) { - PyErr_SetString(state->ChannelError, - "can't initialize mutex for channel management"); - } - else if (err == ERR_NO_NEXT_CHANNEL_ID) { - PyErr_SetString(state->ChannelError, - "failed to get a channel ID"); + else if (err == ERR_NO_NEXT_QUEUE_ID) { + PyErr_SetString(state->QueueError, + "ran out of queue IDs"); } else { assert(PyErr_Occurred()); @@ -607,118 +242,31 @@ handle_channel_error(int err, PyObject *mod, int64_t cid) /* the channel queue */ -typedef uintptr_t _channelitem_id_t; - -typedef struct wait_info { - PyThread_type_lock mutex; - enum { - WAITING_NO_STATUS = 0, - WAITING_ACQUIRED = 1, - WAITING_RELEASING = 2, - WAITING_RELEASED = 3, - } status; - int received; - _channelitem_id_t itemid; -} _waiting_t; - -static int -_waiting_init(_waiting_t *waiting) -{ - PyThread_type_lock mutex = PyThread_allocate_lock(); - if (mutex == NULL) { - PyErr_NoMemory(); - return -1; - } - - *waiting = (_waiting_t){ - .mutex = mutex, - .status = WAITING_NO_STATUS, - }; - return 0; -} - -static void -_waiting_clear(_waiting_t *waiting) -{ - assert(waiting->status != WAITING_ACQUIRED - && waiting->status != WAITING_RELEASING); - if (waiting->mutex != NULL) { - PyThread_free_lock(waiting->mutex); - waiting->mutex = NULL; - } -} - -static _channelitem_id_t -_waiting_get_itemid(_waiting_t *waiting) -{ - return waiting->itemid; -} - -static void -_waiting_acquire(_waiting_t *waiting) -{ - assert(waiting->status == WAITING_NO_STATUS); - PyThread_acquire_lock(waiting->mutex, NOWAIT_LOCK); - waiting->status = WAITING_ACQUIRED; -} - -static void -_waiting_release(_waiting_t *waiting, int received) -{ - assert(waiting->mutex != NULL); - assert(waiting->status == WAITING_ACQUIRED); - assert(!waiting->received); - - waiting->status = WAITING_RELEASING; - PyThread_release_lock(waiting->mutex); - if (waiting->received != received) { - assert(received == 1); - waiting->received = received; - } - waiting->status = WAITING_RELEASED; -} - -static void -_waiting_finish_releasing(_waiting_t *waiting) -{ - while (waiting->status == WAITING_RELEASING) { -#ifdef MS_WINDOWS - SwitchToThread(); -#elif defined(HAVE_SCHED_H) - sched_yield(); -#endif - } -} +typedef uintptr_t _queueitem_id_t; -struct _channelitem; +struct _queueitem; -typedef struct _channelitem { +typedef struct _queueitem { _PyCrossInterpreterData *data; - _waiting_t *waiting; - struct _channelitem *next; -} _channelitem; + struct _queueitem *next; +} _queueitem; -static inline _channelitem_id_t -_channelitem_ID(_channelitem *item) +static inline _queueitem_id_t +_queueitem_ID(_queueitem *item) { - return (_channelitem_id_t)item; + return (_queueitem_id_t)item; } static void -_channelitem_init(_channelitem *item, - _PyCrossInterpreterData *data, _waiting_t *waiting) +_queueitem_init(_queueitem *item, _PyCrossInterpreterData *data) { - *item = (_channelitem){ + *item = (_queueitem){ .data = data, - .waiting = waiting, }; - if (waiting != NULL) { - waiting->itemid = _channelitem_ID(item); - } } static void -_channelitem_clear(_channelitem *item) +_queueitem_clear(_queueitem *item) { item->next = NULL; @@ -727,67 +275,56 @@ _channelitem_clear(_channelitem *item) (void)_release_xid_data(item->data, XID_IGNORE_EXC & XID_FREE); item->data = NULL; } - - if (item->waiting != NULL) { - if (item->waiting->status == WAITING_ACQUIRED) { - _waiting_release(item->waiting, 0); - } - item->waiting = NULL; - } } -static _channelitem * -_channelitem_new(_PyCrossInterpreterData *data, _waiting_t *waiting) +static _queueitem * +_queueitem_new(_PyCrossInterpreterData *data) { - _channelitem *item = GLOBAL_MALLOC(_channelitem); + _queueitem *item = GLOBAL_MALLOC(_queueitem); if (item == NULL) { PyErr_NoMemory(); return NULL; } - _channelitem_init(item, data, waiting); + _queueitem_init(item, data); return item; } static void -_channelitem_free(_channelitem *item) +_queueitem_free(_queueitem *item) { - _channelitem_clear(item); + _queueitem_clear(item); GLOBAL_FREE(item); } static void -_channelitem_free_all(_channelitem *item) +_queueitem_free_all(_queueitem *item) { while (item != NULL) { - _channelitem *last = item; + _queueitem *last = item; item = item->next; - _channelitem_free(last); + _queueitem_free(last); } } static void -_channelitem_popped(_channelitem *item, - _PyCrossInterpreterData **p_data, _waiting_t **p_waiting) +_queueitem_popped(_queueitem *item, _PyCrossInterpreterData **p_data) { - assert(item->waiting == NULL || item->waiting->status == WAITING_ACQUIRED); *p_data = item->data; - *p_waiting = item->waiting; - // We clear them here, so they won't be released in _channelitem_clear(). + // We clear them here, so they won't be released in _queueitem_clear(). item->data = NULL; - item->waiting = NULL; - _channelitem_free(item); + _queueitem_free(item); } -typedef struct _channelqueue { +typedef struct _queueitems { int64_t count; - _channelitem *first; - _channelitem *last; -} _channelqueue; + _queueitem *first; + _queueitem *last; +} _queueitems; -static _channelqueue * -_channelqueue_new(void) +static _queueitems * +_queueitems_new(void) { - _channelqueue *queue = GLOBAL_MALLOC(_channelqueue); + _queueitems *queue = GLOBAL_MALLOC(_queueitems); if (queue == NULL) { PyErr_NoMemory(); return NULL; @@ -799,26 +336,26 @@ _channelqueue_new(void) } static void -_channelqueue_clear(_channelqueue *queue) +_queueitems_clear(_queueitems *queue) { - _channelitem_free_all(queue->first); + _queueitem_free_all(queue->first); queue->count = 0; queue->first = NULL; queue->last = NULL; } static void -_channelqueue_free(_channelqueue *queue) +_queueitems_free(_queueitems *queue) { - _channelqueue_clear(queue); + _queueitems_clear(queue); GLOBAL_FREE(queue); } static int -_channelqueue_put(_channelqueue *queue, - _PyCrossInterpreterData *data, _waiting_t *waiting) +_queueitems_put(_queueitems *queue, + _PyCrossInterpreterData *data) { - _channelitem *item = _channelitem_new(data, waiting); + _queueitem *item = _queueitem_new(data); if (item == NULL) { return -1; } @@ -832,20 +369,15 @@ _channelqueue_put(_channelqueue *queue, } queue->last = item; - if (waiting != NULL) { - _waiting_acquire(waiting); - } - return 0; } static int -_channelqueue_get(_channelqueue *queue, - _PyCrossInterpreterData **p_data, _waiting_t **p_waiting) +_queueitems_get(_queueitems *queue, _PyCrossInterpreterData **p_data) { - _channelitem *item = queue->first; + _queueitem *item = queue->first; if (item == NULL) { - return ERR_CHANNEL_EMPTY; + return ERR_QUEUE_EMPTY; } queue->first = item->next; if (queue->last == item) { @@ -853,82 +385,17 @@ _channelqueue_get(_channelqueue *queue, } queue->count -= 1; - _channelitem_popped(item, p_data, p_waiting); + _queueitem_popped(item, p_data); return 0; } -static int -_channelqueue_find(_channelqueue *queue, _channelitem_id_t itemid, - _channelitem **p_item, _channelitem **p_prev) -{ - _channelitem *prev = NULL; - _channelitem *item = NULL; - if (queue->first != NULL) { - if (_channelitem_ID(queue->first) == itemid) { - item = queue->first; - } - else { - prev = queue->first; - while (prev->next != NULL) { - if (_channelitem_ID(prev->next) == itemid) { - item = prev->next; - break; - } - prev = prev->next; - } - if (item == NULL) { - prev = NULL; - } - } - } - if (p_item != NULL) { - *p_item = item; - } - if (p_prev != NULL) { - *p_prev = prev; - } - return (item != NULL); -} - -static void -_channelqueue_remove(_channelqueue *queue, _channelitem_id_t itemid, - _PyCrossInterpreterData **p_data, _waiting_t **p_waiting) -{ - _channelitem *prev = NULL; - _channelitem *item = NULL; - int found = _channelqueue_find(queue, itemid, &item, &prev); - if (!found) { - return; - } - - assert(item->waiting != NULL); - assert(!item->waiting->received); - if (prev == NULL) { - assert(queue->first == item); - queue->first = item->next; - } - else { - assert(queue->first != item); - assert(prev->next == item); - prev->next = item->next; - } - item->next = NULL; - - if (queue->last == item) { - queue->last = prev; - } - queue->count -= 1; - - _channelitem_popped(item, p_data, p_waiting); -} - static void -_channelqueue_clear_interpreter(_channelqueue *queue, int64_t interpid) +_queueitems_clear_interpreter(_queueitems *queue, int64_t interpid) { - _channelitem *prev = NULL; - _channelitem *next = queue->first; + _queueitem *prev = NULL; + _queueitem *next = queue->first; while (next != NULL) { - _channelitem *item = next; + _queueitem *item = next; next = item->next; if (item->data->interpid == interpid) { if (prev == NULL) { @@ -937,7 +404,7 @@ _channelqueue_clear_interpreter(_channelqueue *queue, int64_t interpid) else { prev->next = item->next; } - _channelitem_free(item); + _queueitem_free(item); queue->count -= 1; } else { @@ -947,496 +414,127 @@ _channelqueue_clear_interpreter(_channelqueue *queue, int64_t interpid) } -/* channel-interpreter associations */ +/* each channel's state */ -struct _channelend; +struct _queue; -typedef struct _channelend { - struct _channelend *next; - int64_t interpid; - int open; -} _channelend; +typedef struct _queue { + PyThread_type_lock mutex; + _queueitems *items; +} _queue_state; -static _channelend * -_channelend_new(int64_t interpid) +static _queue_state * +_queue_new(PyThread_type_lock mutex) { - _channelend *end = GLOBAL_MALLOC(_channelend); - if (end == NULL) { - PyErr_NoMemory(); + _queue_state *queue = GLOBAL_MALLOC(_queue_state); + if (queue == NULL) { return NULL; } - end->next = NULL; - end->interpid = interpid; - end->open = 1; - return end; + queue->mutex = mutex; + queue->items = _queueitems_new(); + if (queue->items == NULL) { + GLOBAL_FREE(queue); + return NULL; + } + return queue; } static void -_channelend_free(_channelend *end) +_queue_free(_queue_state *queue) { - GLOBAL_FREE(end); -} + PyThread_acquire_lock(queue->mutex, WAIT_LOCK); + _queueitems_free(queue->items); + PyThread_release_lock(queue->mutex); -static void -_channelend_free_all(_channelend *end) -{ - while (end != NULL) { - _channelend *last = end; - end = end->next; - _channelend_free(last); - } + PyThread_free_lock(queue->mutex); + GLOBAL_FREE(queue); } -static _channelend * -_channelend_find(_channelend *first, int64_t interpid, _channelend **pprev) +static int +_queue_add(_queue_state *queue, _PyCrossInterpreterData *data) { - _channelend *prev = NULL; - _channelend *end = first; - while (end != NULL) { - if (end->interpid == interpid) { - break; - } - prev = end; - end = end->next; - } - if (pprev != NULL) { - *pprev = prev; - } - return end; -} + int res = -1; + PyThread_acquire_lock(queue->mutex, WAIT_LOCK); -typedef struct _channelassociations { - // Note that the list entries are never removed for interpreter - // for which the channel is closed. This should not be a problem in - // practice. Also, a channel isn't automatically closed when an - // interpreter is destroyed. - int64_t numsendopen; - int64_t numrecvopen; - _channelend *send; - _channelend *recv; -} _channelends; - -static _channelends * -_channelends_new(void) -{ - _channelends *ends = GLOBAL_MALLOC(_channelends); - if (ends== NULL) { - return NULL; + if (_queueitems_put(queue->items, data) != 0) { + goto done; } - ends->numsendopen = 0; - ends->numrecvopen = 0; - ends->send = NULL; - ends->recv = NULL; - return ends; + + res = 0; +done: + PyThread_release_lock(queue->mutex); + return res; } -static void -_channelends_clear(_channelends *ends) +static int +_queue_next(_queue_state *queue, _PyCrossInterpreterData **p_data) { - _channelend_free_all(ends->send); - ends->send = NULL; - ends->numsendopen = 0; + int err = 0; + PyThread_acquire_lock(queue->mutex, WAIT_LOCK); + +#ifdef NDEBUG + (void)_queueitems_get(queue->items, p_data); +#else + int empty = _queueitems_get(queue->items, p_data); + assert(empty == 0 || empty == ERR_QUEUE_EMPTY); +#endif + assert(!PyErr_Occurred()); - _channelend_free_all(ends->recv); - ends->recv = NULL; - ends->numrecvopen = 0; + PyThread_release_lock(queue->mutex); + return err; } static void -_channelends_free(_channelends *ends) +_queue_clear_interpreter(_queue_state *queue, int64_t interpid) { - _channelends_clear(ends); - GLOBAL_FREE(ends); + PyThread_acquire_lock(queue->mutex, WAIT_LOCK); + + _queueitems_clear_interpreter(queue->items, interpid); + + PyThread_release_lock(queue->mutex); } -static _channelend * -_channelends_add(_channelends *ends, _channelend *prev, int64_t interpid, - int send) + +/* the set of channels */ + +struct _queueref; + +typedef struct _queueref { + struct _queueref *next; + int64_t qid; + Py_ssize_t refcount; + _queue_state *queue; +} _queueref; + +static _queueref * +_queueref_new(int64_t qid, _queue_state *queue) { - _channelend *end = _channelend_new(interpid); - if (end == NULL) { + _queueref *ref = GLOBAL_MALLOC(_queueref); + if (ref == NULL) { return NULL; } - - if (prev == NULL) { - if (send) { - ends->send = end; - } - else { - ends->recv = end; - } - } - else { - prev->next = end; - } - if (send) { - ends->numsendopen += 1; - } - else { - ends->numrecvopen += 1; - } - return end; -} - -static int -_channelends_associate(_channelends *ends, int64_t interpid, int send) -{ - _channelend *prev; - _channelend *end = _channelend_find(send ? ends->send : ends->recv, - interpid, &prev); - if (end != NULL) { - if (!end->open) { - return ERR_CHANNEL_CLOSED; - } - // already associated - return 0; - } - if (_channelends_add(ends, prev, interpid, send) == NULL) { - return -1; - } - return 0; -} - -static int -_channelends_is_open(_channelends *ends) -{ - if (ends->numsendopen != 0 || ends->numrecvopen != 0) { - // At least one interpreter is still associated with the channel - // (and hasn't been released). - return 1; - } - // XXX This is wrong if an end can ever be removed. - if (ends->send == NULL && ends->recv == NULL) { - // The channel has never had any interpreters associated with it. - return 1; - } - return 0; -} - -static void -_channelends_release_end(_channelends *ends, _channelend *end, int send) -{ - end->open = 0; - if (send) { - ends->numsendopen -= 1; - } - else { - ends->numrecvopen -= 1; - } -} - -static int -_channelends_release_interpreter(_channelends *ends, int64_t interpid, int which) -{ - _channelend *prev; - _channelend *end; - if (which >= 0) { // send/both - end = _channelend_find(ends->send, interpid, &prev); - if (end == NULL) { - // never associated so add it - end = _channelends_add(ends, prev, interpid, 1); - if (end == NULL) { - return -1; - } - } - _channelends_release_end(ends, end, 1); - } - if (which <= 0) { // recv/both - end = _channelend_find(ends->recv, interpid, &prev); - if (end == NULL) { - // never associated so add it - end = _channelends_add(ends, prev, interpid, 0); - if (end == NULL) { - return -1; - } - } - _channelends_release_end(ends, end, 0); - } - return 0; -} - -static void -_channelends_release_all(_channelends *ends, int which, int force) -{ - // XXX Handle the ends. - // XXX Handle force is True. - - // Ensure all the "send"-associated interpreters are closed. - _channelend *end; - for (end = ends->send; end != NULL; end = end->next) { - _channelends_release_end(ends, end, 1); - } - - // Ensure all the "recv"-associated interpreters are closed. - for (end = ends->recv; end != NULL; end = end->next) { - _channelends_release_end(ends, end, 0); - } -} - -static void -_channelends_clear_interpreter(_channelends *ends, int64_t interpid) -{ - // XXX Actually remove the entries? - _channelend *end; - end = _channelend_find(ends->send, interpid, NULL); - if (end != NULL) { - _channelends_release_end(ends, end, 1); - } - end = _channelend_find(ends->recv, interpid, NULL); - if (end != NULL) { - _channelends_release_end(ends, end, 0); - } -} - - -/* each channel's state */ - -struct _channel; -struct _channel_closing; -static void _channel_clear_closing(struct _channel *); -static void _channel_finish_closing(struct _channel *); - -typedef struct _channel { - PyThread_type_lock mutex; - _channelqueue *queue; - _channelends *ends; - int open; - struct _channel_closing *closing; -} _channel_state; - -static _channel_state * -_channel_new(PyThread_type_lock mutex) -{ - _channel_state *chan = GLOBAL_MALLOC(_channel_state); - if (chan == NULL) { - return NULL; - } - chan->mutex = mutex; - chan->queue = _channelqueue_new(); - if (chan->queue == NULL) { - GLOBAL_FREE(chan); - return NULL; - } - chan->ends = _channelends_new(); - if (chan->ends == NULL) { - _channelqueue_free(chan->queue); - GLOBAL_FREE(chan); - return NULL; - } - chan->open = 1; - chan->closing = NULL; - return chan; -} - -static void -_channel_free(_channel_state *chan) -{ - _channel_clear_closing(chan); - PyThread_acquire_lock(chan->mutex, WAIT_LOCK); - _channelqueue_free(chan->queue); - _channelends_free(chan->ends); - PyThread_release_lock(chan->mutex); - - PyThread_free_lock(chan->mutex); - GLOBAL_FREE(chan); -} - -static int -_channel_add(_channel_state *chan, int64_t interpid, - _PyCrossInterpreterData *data, _waiting_t *waiting) -{ - int res = -1; - PyThread_acquire_lock(chan->mutex, WAIT_LOCK); - - if (!chan->open) { - res = ERR_CHANNEL_CLOSED; - goto done; - } - if (_channelends_associate(chan->ends, interpid, 1) != 0) { - res = ERR_CHANNEL_INTERP_CLOSED; - goto done; - } - - if (_channelqueue_put(chan->queue, data, waiting) != 0) { - goto done; - } - // Any errors past this point must cause a _waiting_release() call. - - res = 0; -done: - PyThread_release_lock(chan->mutex); - return res; -} - -static int -_channel_next(_channel_state *chan, int64_t interpid, - _PyCrossInterpreterData **p_data, _waiting_t **p_waiting) -{ - int err = 0; - PyThread_acquire_lock(chan->mutex, WAIT_LOCK); - - if (!chan->open) { - err = ERR_CHANNEL_CLOSED; - goto done; - } - if (_channelends_associate(chan->ends, interpid, 0) != 0) { - err = ERR_CHANNEL_INTERP_CLOSED; - goto done; - } - - int empty = _channelqueue_get(chan->queue, p_data, p_waiting); - assert(empty == 0 || empty == ERR_CHANNEL_EMPTY); - assert(!PyErr_Occurred()); - if (empty && chan->closing != NULL) { - chan->open = 0; - } - -done: - PyThread_release_lock(chan->mutex); - if (chan->queue->count == 0) { - _channel_finish_closing(chan); - } - return err; -} - -static void -_channel_remove(_channel_state *chan, _channelitem_id_t itemid) -{ - _PyCrossInterpreterData *data = NULL; - _waiting_t *waiting = NULL; - - PyThread_acquire_lock(chan->mutex, WAIT_LOCK); - _channelqueue_remove(chan->queue, itemid, &data, &waiting); - PyThread_release_lock(chan->mutex); - - (void)_release_xid_data(data, XID_IGNORE_EXC | XID_FREE); - if (waiting != NULL) { - _waiting_release(waiting, 0); - } - - if (chan->queue->count == 0) { - _channel_finish_closing(chan); - } -} - -static int -_channel_release_interpreter(_channel_state *chan, int64_t interpid, int end) -{ - PyThread_acquire_lock(chan->mutex, WAIT_LOCK); - - int res = -1; - if (!chan->open) { - res = ERR_CHANNEL_CLOSED; - goto done; - } - - if (_channelends_release_interpreter(chan->ends, interpid, end) != 0) { - goto done; - } - chan->open = _channelends_is_open(chan->ends); - // XXX Clear the queue if not empty? - // XXX Activate the "closing" mechanism? - - res = 0; -done: - PyThread_release_lock(chan->mutex); - return res; -} - -static int -_channel_release_all(_channel_state *chan, int end, int force) -{ - int res = -1; - PyThread_acquire_lock(chan->mutex, WAIT_LOCK); - - if (!chan->open) { - res = ERR_CHANNEL_CLOSED; - goto done; - } - - if (!force && chan->queue->count > 0) { - res = ERR_CHANNEL_NOT_EMPTY; - goto done; - } - // XXX Clear the queue? - - chan->open = 0; - - // We *could* also just leave these in place, since we've marked - // the channel as closed already. - _channelends_release_all(chan->ends, end, force); - - res = 0; -done: - PyThread_release_lock(chan->mutex); - return res; -} - -static void -_channel_clear_interpreter(_channel_state *chan, int64_t interpid) -{ - PyThread_acquire_lock(chan->mutex, WAIT_LOCK); - - _channelqueue_clear_interpreter(chan->queue, interpid); - _channelends_clear_interpreter(chan->ends, interpid); - chan->open = _channelends_is_open(chan->ends); - - PyThread_release_lock(chan->mutex); -} - - -/* the set of channels */ - -struct _channelref; - -typedef struct _channelref { - int64_t cid; - _channel_state *chan; - struct _channelref *next; - // The number of ChannelID objects referring to this channel. - Py_ssize_t objcount; -} _channelref; - -static _channelref * -_channelref_new(int64_t cid, _channel_state *chan) -{ - _channelref *ref = GLOBAL_MALLOC(_channelref); - if (ref == NULL) { - return NULL; - } - ref->cid = cid; - ref->chan = chan; ref->next = NULL; - ref->objcount = 0; + ref->qid = qid; + ref->refcount = 0; + ref->queue = queue; return ref; } -//static void -//_channelref_clear(_channelref *ref) -//{ -// ref->cid = -1; -// ref->chan = NULL; -// ref->next = NULL; -// ref->objcount = 0; -//} - static void -_channelref_free(_channelref *ref) +_queueref_free(_queueref *ref) { - if (ref->chan != NULL) { - _channel_clear_closing(ref->chan); - } - //_channelref_clear(ref); + assert(ref->next == NULL); + // ref->queue is freed by the caller. GLOBAL_FREE(ref); } -static _channelref * -_channelref_find(_channelref *first, int64_t cid, _channelref **pprev) +static _queueref * +_queuerefs_find(_queueref *first, int64_t qid, _queueref **pprev) { - _channelref *prev = NULL; - _channelref *ref = first; + _queueref *prev = NULL; + _queueref *ref = first; while (ref != NULL) { - if (ref->cid == cid) { + if (ref->qid == qid) { break; } prev = ref; @@ -1449,1415 +547,396 @@ _channelref_find(_channelref *first, int64_t cid, _channelref **pprev) } -typedef struct _channels { +typedef struct _queues { PyThread_type_lock mutex; - _channelref *head; - int64_t numopen; + _queueref *head; + int64_t count; int64_t next_id; -} _channels; +} _queues; static void -_channels_init(_channels *channels, PyThread_type_lock mutex) +_queues_init(_queues *queues, PyThread_type_lock mutex) { - channels->mutex = mutex; - channels->head = NULL; - channels->numopen = 0; - channels->next_id = 0; + queues->mutex = mutex; + queues->head = NULL; + queues->count = 0; + queues->next_id = 1; } static void -_channels_fini(_channels *channels) +_queues_fini(_queues *queues) { - assert(channels->numopen == 0); - assert(channels->head == NULL); - if (channels->mutex != NULL) { - PyThread_free_lock(channels->mutex); - channels->mutex = NULL; + assert(queues->count == 0); + assert(queues->head == NULL); + if (queues->mutex != NULL) { + PyThread_free_lock(queues->mutex); + queues->mutex = NULL; } } static int64_t -_channels_next_id(_channels *channels) // needs lock +_queues_next_id(_queues *queues) // needs lock { - int64_t cid = channels->next_id; - if (cid < 0) { + int64_t qid = queues->next_id; + if (qid < 0) { /* overflow */ return -1; } - channels->next_id += 1; - return cid; + queues->next_id += 1; + return qid; } static int -_channels_lookup(_channels *channels, int64_t cid, PyThread_type_lock *pmutex, - _channel_state **res) +_queues_lookup(_queues *queues, int64_t qid, PyThread_type_lock *pmutex, + _queue_state **res) { int err = -1; - _channel_state *chan = NULL; - PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + _queue_state *queue = NULL; + PyThread_acquire_lock(queues->mutex, WAIT_LOCK); if (pmutex != NULL) { *pmutex = NULL; } - _channelref *ref = _channelref_find(channels->head, cid, NULL); + _queueref *ref = _queuerefs_find(queues->head, qid, NULL); if (ref == NULL) { - err = ERR_CHANNEL_NOT_FOUND; - goto done; - } - if (ref->chan == NULL || !ref->chan->open) { - err = ERR_CHANNEL_CLOSED; + err = ERR_QUEUE_NOT_FOUND; goto done; } + assert(ref->queue != NULL); if (pmutex != NULL) { // The mutex will be closed by the caller. - *pmutex = channels->mutex; + *pmutex = queues->mutex; } - chan = ref->chan; + queue = ref->queue; err = 0; done: if (pmutex == NULL || *pmutex == NULL) { - PyThread_release_lock(channels->mutex); + PyThread_release_lock(queues->mutex); } - *res = chan; + *res = queue; return err; } static int64_t -_channels_add(_channels *channels, _channel_state *chan) +_queues_add(_queues *queues, _queue_state *queue) { - int64_t cid = -1; - PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + int64_t qid = -1; + PyThread_acquire_lock(queues->mutex, WAIT_LOCK); // Create a new ref. - int64_t _cid = _channels_next_id(channels); - if (_cid < 0) { - cid = ERR_NO_NEXT_CHANNEL_ID; + int64_t _qid = _queues_next_id(queues); + if (_qid < 0) { + qid = ERR_NO_NEXT_QUEUE_ID; goto done; } - _channelref *ref = _channelref_new(_cid, chan); + _queueref *ref = _queueref_new(_qid, queue); if (ref == NULL) { goto done; } // Add it to the list. // We assume that the channel is a new one (not already in the list). - ref->next = channels->head; - channels->head = ref; - channels->numopen += 1; - - cid = _cid; -done: - PyThread_release_lock(channels->mutex); - return cid; -} - -/* forward */ -static int _channel_set_closing(_channelref *, PyThread_type_lock); - -static int -_channels_close(_channels *channels, int64_t cid, _channel_state **pchan, - int end, int force) -{ - int res = -1; - PyThread_acquire_lock(channels->mutex, WAIT_LOCK); - if (pchan != NULL) { - *pchan = NULL; - } - - _channelref *ref = _channelref_find(channels->head, cid, NULL); - if (ref == NULL) { - res = ERR_CHANNEL_NOT_FOUND; - goto done; - } - - if (ref->chan == NULL) { - res = ERR_CHANNEL_CLOSED; - goto done; - } - else if (!force && end == CHANNEL_SEND && ref->chan->closing != NULL) { - res = ERR_CHANNEL_CLOSED; - goto done; - } - else { - int err = _channel_release_all(ref->chan, end, force); - if (err != 0) { - if (end == CHANNEL_SEND && err == ERR_CHANNEL_NOT_EMPTY) { - if (ref->chan->closing != NULL) { - res = ERR_CHANNEL_CLOSED; - goto done; - } - // Mark the channel as closing and return. The channel - // will be cleaned up in _channel_next(). - PyErr_Clear(); - int err = _channel_set_closing(ref, channels->mutex); - if (err != 0) { - res = err; - goto done; - } - if (pchan != NULL) { - *pchan = ref->chan; - } - res = 0; - } - else { - res = err; - } - goto done; - } - if (pchan != NULL) { - *pchan = ref->chan; - } - else { - _channel_free(ref->chan); - } - ref->chan = NULL; - } + ref->next = queues->head; + queues->head = ref; + queues->count += 1; - res = 0; + qid = _qid; done: - PyThread_release_lock(channels->mutex); - return res; + PyThread_release_lock(queues->mutex); + return qid; } static void -_channels_remove_ref(_channels *channels, _channelref *ref, _channelref *prev, - _channel_state **pchan) +_queues_remove_ref(_queues *queues, _queueref *ref, _queueref *prev, + _queue_state **p_queue) { - if (ref == channels->head) { - channels->head = ref->next; + if (ref == queues->head) { + queues->head = ref->next; } else { prev->next = ref->next; } - channels->numopen -= 1; + ref->next = NULL; + queues->count -= 1; - if (pchan != NULL) { - *pchan = ref->chan; + if (p_queue != NULL) { + *p_queue = ref->queue; } - _channelref_free(ref); + _queueref_free(ref); } static int -_channels_remove(_channels *channels, int64_t cid, _channel_state **pchan) +_queues_remove(_queues *queues, int64_t qid, _queue_state **p_queue) { int res = -1; - PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + PyThread_acquire_lock(queues->mutex, WAIT_LOCK); - if (pchan != NULL) { - *pchan = NULL; + if (p_queue != NULL) { + *p_queue = NULL; } - _channelref *prev = NULL; - _channelref *ref = _channelref_find(channels->head, cid, &prev); + _queueref *prev = NULL; + _queueref *ref = _queuerefs_find(queues->head, qid, &prev); if (ref == NULL) { - res = ERR_CHANNEL_NOT_FOUND; + res = ERR_QUEUE_NOT_FOUND; goto done; } - _channels_remove_ref(channels, ref, prev, pchan); + _queues_remove_ref(queues, ref, prev, p_queue); res = 0; done: - PyThread_release_lock(channels->mutex); + PyThread_release_lock(queues->mutex); return res; } static int -_channels_add_id_object(_channels *channels, int64_t cid) +_queues_incref(_queues *queues, int64_t qid) { + // XXX Track interpreter IDs? int res = -1; - PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + PyThread_acquire_lock(queues->mutex, WAIT_LOCK); - _channelref *ref = _channelref_find(channels->head, cid, NULL); + _queueref *ref = _queuerefs_find(queues->head, qid, NULL); if (ref == NULL) { - res = ERR_CHANNEL_NOT_FOUND; + assert(!PyErr_Occurred()); + res = ERR_QUEUE_NOT_FOUND; goto done; } - ref->objcount += 1; + ref->refcount += 1; res = 0; done: - PyThread_release_lock(channels->mutex); + PyThread_release_lock(queues->mutex); return res; } static void -_channels_release_cid_object(_channels *channels, int64_t cid) +_queues_decref(_queues *queues, int64_t qid) { - PyThread_acquire_lock(channels->mutex, WAIT_LOCK); + PyThread_acquire_lock(queues->mutex, WAIT_LOCK); - _channelref *prev = NULL; - _channelref *ref = _channelref_find(channels->head, cid, &prev); + _queueref *prev = NULL; + _queueref *ref = _queuerefs_find(queues->head, qid, &prev); if (ref == NULL) { + assert(!PyErr_Occurred()); // Already destroyed. + // XXX Warn? goto done; } - ref->objcount -= 1; + assert(ref->refcount > 0); + ref->refcount -= 1; // Destroy if no longer used. - if (ref->objcount == 0) { - _channel_state *chan = NULL; - _channels_remove_ref(channels, ref, prev, &chan); - if (chan != NULL) { - _channel_free(chan); + if (ref->refcount == 0) { + _queue_state *queue = NULL; + _queues_remove_ref(queues, ref, prev, &queue); + if (queue != NULL) { + _queue_free(queue); } } done: - PyThread_release_lock(channels->mutex); + PyThread_release_lock(queues->mutex); } static int64_t * -_channels_list_all(_channels *channels, int64_t *count) -{ - int64_t *cids = NULL; - PyThread_acquire_lock(channels->mutex, WAIT_LOCK); - int64_t *ids = PyMem_NEW(int64_t, (Py_ssize_t)(channels->numopen)); - if (ids == NULL) { - goto done; - } - _channelref *ref = channels->head; - for (int64_t i=0; ref != NULL; ref = ref->next, i++) { - ids[i] = ref->cid; - } - *count = channels->numopen; - - cids = ids; -done: - PyThread_release_lock(channels->mutex); - return cids; -} - -static void -_channels_clear_interpreter(_channels *channels, int64_t interpid) -{ - PyThread_acquire_lock(channels->mutex, WAIT_LOCK); - - _channelref *ref = channels->head; - for (; ref != NULL; ref = ref->next) { - if (ref->chan != NULL) { - _channel_clear_interpreter(ref->chan, interpid); - } - } - - PyThread_release_lock(channels->mutex); -} - - -/* support for closing non-empty channels */ - -struct _channel_closing { - _channelref *ref; -}; - -static int -_channel_set_closing(_channelref *ref, PyThread_type_lock mutex) { - _channel_state *chan = ref->chan; - if (chan == NULL) { - // already closed - return 0; - } - int res = -1; - PyThread_acquire_lock(chan->mutex, WAIT_LOCK); - if (chan->closing != NULL) { - res = ERR_CHANNEL_CLOSED; - goto done; - } - chan->closing = GLOBAL_MALLOC(struct _channel_closing); - if (chan->closing == NULL) { - goto done; - } - chan->closing->ref = ref; - - res = 0; -done: - PyThread_release_lock(chan->mutex); - return res; -} - -static void -_channel_clear_closing(_channel_state *chan) { - PyThread_acquire_lock(chan->mutex, WAIT_LOCK); - if (chan->closing != NULL) { - GLOBAL_FREE(chan->closing); - chan->closing = NULL; - } - PyThread_release_lock(chan->mutex); -} - -static void -_channel_finish_closing(_channel_state *chan) { - struct _channel_closing *closing = chan->closing; - if (closing == NULL) { - return; - } - _channelref *ref = closing->ref; - _channel_clear_closing(chan); - // Do the things that would have been done in _channels_close(). - ref->chan = NULL; - _channel_free(chan); -} - - -/* "high"-level channel-related functions */ - -// Create a new channel. -static int64_t -channel_create(_channels *channels) -{ - PyThread_type_lock mutex = PyThread_allocate_lock(); - if (mutex == NULL) { - return ERR_CHANNEL_MUTEX_INIT; - } - _channel_state *chan = _channel_new(mutex); - if (chan == NULL) { - PyThread_free_lock(mutex); - return -1; - } - int64_t cid = _channels_add(channels, chan); - if (cid < 0) { - _channel_free(chan); - } - return cid; -} - -// Completely destroy the channel. -static int -channel_destroy(_channels *channels, int64_t cid) -{ - _channel_state *chan = NULL; - int err = _channels_remove(channels, cid, &chan); - if (err != 0) { - return err; - } - if (chan != NULL) { - _channel_free(chan); - } - return 0; -} - -// Push an object onto the channel. -// The current interpreter gets associated with the send end of the channel. -// Optionally request to be notified when it is received. -static int -channel_send(_channels *channels, int64_t cid, PyObject *obj, - _waiting_t *waiting) -{ - PyInterpreterState *interp = _get_current_interp(); - if (interp == NULL) { - return -1; - } - int64_t interpid = PyInterpreterState_GetID(interp); - - // Look up the channel. - PyThread_type_lock mutex = NULL; - _channel_state *chan = NULL; - int err = _channels_lookup(channels, cid, &mutex, &chan); - if (err != 0) { - return err; - } - assert(chan != NULL); - // Past this point we are responsible for releasing the mutex. - - if (chan->closing != NULL) { - PyThread_release_lock(mutex); - return ERR_CHANNEL_CLOSED; - } - - // Convert the object to cross-interpreter data. - _PyCrossInterpreterData *data = GLOBAL_MALLOC(_PyCrossInterpreterData); - if (data == NULL) { - PyThread_release_lock(mutex); - return -1; - } - if (_PyObject_GetCrossInterpreterData(obj, data) != 0) { - PyThread_release_lock(mutex); - GLOBAL_FREE(data); - return -1; - } - - // Add the data to the channel. - int res = _channel_add(chan, interpid, data, waiting); - PyThread_release_lock(mutex); - if (res != 0) { - // We may chain an exception here: - (void)_release_xid_data(data, 0); - GLOBAL_FREE(data); - return res; - } - - return 0; -} - -// Basically, un-send an object. -static void -channel_clear_sent(_channels *channels, int64_t cid, _waiting_t *waiting) -{ - // Look up the channel. - PyThread_type_lock mutex = NULL; - _channel_state *chan = NULL; - int err = _channels_lookup(channels, cid, &mutex, &chan); - if (err != 0) { - // The channel was already closed, etc. - assert(waiting->status == WAITING_RELEASED); - return; // Ignore the error. - } - assert(chan != NULL); - // Past this point we are responsible for releasing the mutex. - - _channelitem_id_t itemid = _waiting_get_itemid(waiting); - _channel_remove(chan, itemid); - - PyThread_release_lock(mutex); -} - -// Like channel_send(), but strictly wait for the object to be received. -static int -channel_send_wait(_channels *channels, int64_t cid, PyObject *obj, - PY_TIMEOUT_T timeout) -{ - // We use a stack variable here, so we must ensure that &waiting - // is not held by any channel item at the point this function exits. - _waiting_t waiting; - if (_waiting_init(&waiting) < 0) { - assert(PyErr_Occurred()); - return -1; - } - - /* Queue up the object. */ - int res = channel_send(channels, cid, obj, &waiting); - if (res < 0) { - assert(waiting.status == WAITING_NO_STATUS); - goto finally; - } - - /* Wait until the object is received. */ - if (wait_for_lock(waiting.mutex, timeout) < 0) { - assert(PyErr_Occurred()); - _waiting_finish_releasing(&waiting); - /* The send() call is failing now, so make sure the item - won't be received. */ - channel_clear_sent(channels, cid, &waiting); - assert(waiting.status == WAITING_RELEASED); - if (!waiting.received) { - res = -1; - goto finally; - } - // XXX Emit a warning if not a TimeoutError? - PyErr_Clear(); - } - else { - _waiting_finish_releasing(&waiting); - assert(waiting.status == WAITING_RELEASED); - if (!waiting.received) { - res = ERR_CHANNEL_CLOSED_WAITING; - goto finally; - } - } - - /* success! */ - res = 0; - -finally: - _waiting_clear(&waiting); - return res; -} - -// Pop the next object off the channel. Fail if empty. -// The current interpreter gets associated with the recv end of the channel. -// XXX Support a "wait" mutex? -static int -channel_recv(_channels *channels, int64_t cid, PyObject **res) -{ - int err; - *res = NULL; - - PyInterpreterState *interp = _get_current_interp(); - if (interp == NULL) { - // XXX Is this always an error? - if (PyErr_Occurred()) { - return -1; - } - return 0; - } - int64_t interpid = PyInterpreterState_GetID(interp); - - // Look up the channel. - PyThread_type_lock mutex = NULL; - _channel_state *chan = NULL; - err = _channels_lookup(channels, cid, &mutex, &chan); - if (err != 0) { - return err; - } - assert(chan != NULL); - // Past this point we are responsible for releasing the mutex. - - // Pop off the next item from the channel. - _PyCrossInterpreterData *data = NULL; - _waiting_t *waiting = NULL; - err = _channel_next(chan, interpid, &data, &waiting); - PyThread_release_lock(mutex); - if (err != 0) { - return err; - } - else if (data == NULL) { - assert(!PyErr_Occurred()); - return 0; - } - - // Convert the data back to an object. - PyObject *obj = _PyCrossInterpreterData_NewObject(data); - if (obj == NULL) { - assert(PyErr_Occurred()); - // It was allocated in channel_send(), so we free it. - (void)_release_xid_data(data, XID_IGNORE_EXC | XID_FREE); - if (waiting != NULL) { - _waiting_release(waiting, 0); - } - return -1; - } - // It was allocated in channel_send(), so we free it. - int release_res = _release_xid_data(data, XID_FREE); - if (release_res < 0) { - // The source interpreter has been destroyed already. - assert(PyErr_Occurred()); - Py_DECREF(obj); - if (waiting != NULL) { - _waiting_release(waiting, 0); - } - return -1; - } - - // Notify the sender. - if (waiting != NULL) { - _waiting_release(waiting, 1); - } - - *res = obj; - return 0; -} - -// Disallow send/recv for the current interpreter. -// The channel is marked as closed if no other interpreters -// are currently associated. -static int -channel_release(_channels *channels, int64_t cid, int send, int recv) -{ - PyInterpreterState *interp = _get_current_interp(); - if (interp == NULL) { - return -1; - } - int64_t interpid = PyInterpreterState_GetID(interp); - - // Look up the channel. - PyThread_type_lock mutex = NULL; - _channel_state *chan = NULL; - int err = _channels_lookup(channels, cid, &mutex, &chan); - if (err != 0) { - return err; - } - // Past this point we are responsible for releasing the mutex. - - // Close one or both of the two ends. - int res = _channel_release_interpreter(chan, interpid, send-recv); - PyThread_release_lock(mutex); - return res; -} - -// Close the channel (for all interpreters). Fail if it's already closed. -// Close immediately if it's empty. Otherwise, disallow sending and -// finally close once empty. Optionally, immediately clear and close it. -static int -channel_close(_channels *channels, int64_t cid, int end, int force) -{ - return _channels_close(channels, cid, NULL, end, force); -} - -// Return true if the identified interpreter is associated -// with the given end of the channel. -static int -channel_is_associated(_channels *channels, int64_t cid, int64_t interpid, - int send) -{ - _channel_state *chan = NULL; - int err = _channels_lookup(channels, cid, NULL, &chan); - if (err != 0) { - return err; - } - else if (send && chan->closing != NULL) { - return ERR_CHANNEL_CLOSED; - } - - _channelend *end = _channelend_find(send ? chan->ends->send : chan->ends->recv, - interpid, NULL); - - return (end != NULL && end->open); -} - - -/* channel info */ - -struct channel_info { - struct { - // 1: closed; -1: closing - int closed; - struct { - Py_ssize_t nsend_only; // not released - Py_ssize_t nsend_only_released; - Py_ssize_t nrecv_only; // not released - Py_ssize_t nrecv_only_released; - Py_ssize_t nboth; // not released - Py_ssize_t nboth_released; - Py_ssize_t nboth_send_released; - Py_ssize_t nboth_recv_released; - } all; - struct { - // 1: associated; -1: released - int send; - int recv; - } cur; - } status; - Py_ssize_t count; -}; - -static int -_channel_get_info(_channels *channels, int64_t cid, struct channel_info *info) -{ - int err = 0; - *info = (struct channel_info){0}; - - // Get the current interpreter. - PyInterpreterState *interp = _get_current_interp(); - if (interp == NULL) { - return -1; - } - Py_ssize_t interpid = PyInterpreterState_GetID(interp); - - // Hold the global lock until we're done. - PyThread_acquire_lock(channels->mutex, WAIT_LOCK); - - // Find the channel. - _channelref *ref = _channelref_find(channels->head, cid, NULL); - if (ref == NULL) { - err = ERR_CHANNEL_NOT_FOUND; - goto finally; - } - _channel_state *chan = ref->chan; - - // Check if open. - if (chan == NULL) { - info->status.closed = 1; - goto finally; - } - if (!chan->open) { - assert(chan->queue->count == 0); - info->status.closed = 1; - goto finally; - } - if (chan->closing != NULL) { - assert(chan->queue->count > 0); - info->status.closed = -1; - } - else { - info->status.closed = 0; - } - - // Get the number of queued objects. - info->count = chan->queue->count; - - // Get the ends statuses. - assert(info->status.cur.send == 0); - assert(info->status.cur.recv == 0); - _channelend *send = chan->ends->send; - while (send != NULL) { - if (send->interpid == interpid) { - info->status.cur.send = send->open ? 1 : -1; - } - - if (send->open) { - info->status.all.nsend_only += 1; - } - else { - info->status.all.nsend_only_released += 1; - } - send = send->next; - } - _channelend *recv = chan->ends->recv; - while (recv != NULL) { - if (recv->interpid == interpid) { - info->status.cur.recv = recv->open ? 1 : -1; - } - - // XXX This is O(n*n). Why do we have 2 linked lists? - _channelend *send = chan->ends->send; - while (send != NULL) { - if (send->interpid == recv->interpid) { - break; - } - send = send->next; - } - if (send == NULL) { - if (recv->open) { - info->status.all.nrecv_only += 1; - } - else { - info->status.all.nrecv_only_released += 1; - } - } - else { - if (recv->open) { - if (send->open) { - info->status.all.nboth += 1; - info->status.all.nsend_only -= 1; - } - else { - info->status.all.nboth_recv_released += 1; - info->status.all.nsend_only_released -= 1; - } - } - else { - if (send->open) { - info->status.all.nboth_send_released += 1; - info->status.all.nsend_only -= 1; - } - else { - info->status.all.nboth_released += 1; - info->status.all.nsend_only_released -= 1; - } - } - } - recv = recv->next; - } - -finally: - PyThread_release_lock(channels->mutex); - return err; -} - -PyDoc_STRVAR(channel_info_doc, -"ChannelInfo\n\ -\n\ -A named tuple of a channel's state."); - -static PyStructSequence_Field channel_info_fields[] = { - {"open", "both ends are open"}, - {"closing", "send is closed, recv is non-empty"}, - {"closed", "both ends are closed"}, - {"count", "queued objects"}, - - {"num_interp_send", "interpreters bound to the send end"}, - {"num_interp_send_released", - "interpreters bound to the send end and released"}, - - {"num_interp_recv", "interpreters bound to the send end"}, - {"num_interp_recv_released", - "interpreters bound to the send end and released"}, - - {"num_interp_both", "interpreters bound to both ends"}, - {"num_interp_both_released", - "interpreters bound to both ends and released_from_both"}, - {"num_interp_both_send_released", - "interpreters bound to both ends and released_from_the send end"}, - {"num_interp_both_recv_released", - "interpreters bound to both ends and released_from_the recv end"}, - - {"send_associated", "current interpreter is bound to the send end"}, - {"send_released", "current interpreter *was* bound to the send end"}, - {"recv_associated", "current interpreter is bound to the recv end"}, - {"recv_released", "current interpreter *was* bound to the recv end"}, - {0} -}; - -static PyStructSequence_Desc channel_info_desc = { - .name = MODULE_NAME ".ChannelInfo", - .doc = channel_info_doc, - .fields = channel_info_fields, - .n_in_sequence = 8, -}; - -static PyObject * -new_channel_info(PyObject *mod, struct channel_info *info) -{ - module_state *state = get_module_state(mod); - if (state == NULL) { - return NULL; - } - - assert(state->ChannelInfoType != NULL); - PyObject *self = PyStructSequence_New(state->ChannelInfoType); - if (self == NULL) { - return NULL; - } - - int pos = 0; -#define SET_BOOL(val) \ - PyStructSequence_SET_ITEM(self, pos++, \ - Py_NewRef(val ? Py_True : Py_False)) -#define SET_COUNT(val) \ - do { \ - PyObject *obj = PyLong_FromLongLong(val); \ - if (obj == NULL) { \ - Py_CLEAR(info); \ - return NULL; \ - } \ - PyStructSequence_SET_ITEM(self, pos++, obj); \ - } while(0) - SET_BOOL(info->status.closed == 0); - SET_BOOL(info->status.closed == -1); - SET_BOOL(info->status.closed == 1); - SET_COUNT(info->count); - SET_COUNT(info->status.all.nsend_only); - SET_COUNT(info->status.all.nsend_only_released); - SET_COUNT(info->status.all.nrecv_only); - SET_COUNT(info->status.all.nrecv_only_released); - SET_COUNT(info->status.all.nboth); - SET_COUNT(info->status.all.nboth_released); - SET_COUNT(info->status.all.nboth_send_released); - SET_COUNT(info->status.all.nboth_recv_released); - SET_BOOL(info->status.cur.send == 1); - SET_BOOL(info->status.cur.send == -1); - SET_BOOL(info->status.cur.recv == 1); - SET_BOOL(info->status.cur.recv == -1); -#undef SET_COUNT -#undef SET_BOOL - assert(!PyErr_Occurred()); - return self; -} - - -/* ChannelID class */ - -typedef struct queueid { - PyObject_HEAD - int64_t cid; - int end; - int resolve; - _channels *channels; -} queueid; - -struct channel_id_converter_data { - PyObject *module; - int64_t cid; - int end; -}; - -static int -channel_id_converter(PyObject *arg, void *ptr) -{ - int64_t cid; - int end = 0; - struct channel_id_converter_data *data = ptr; - module_state *state = get_module_state(data->module); - assert(state != NULL); - if (PyObject_TypeCheck(arg, state->QueueIDType)) { - cid = ((queueid *)arg)->cid; - end = ((queueid *)arg)->end; - } - else if (PyIndex_Check(arg)) { - cid = PyLong_AsLongLong(arg); - if (cid == -1 && PyErr_Occurred()) { - return 0; - } - if (cid < 0) { - PyErr_Format(PyExc_ValueError, - "channel ID must be a non-negative int, got %R", arg); - return 0; - } - } - else { - PyErr_Format(PyExc_TypeError, - "channel ID must be an int, got %.100s", - Py_TYPE(arg)->tp_name); - return 0; - } - data->cid = cid; - data->end = end; - return 1; -} - -static int -newqueueid(PyTypeObject *cls, int64_t cid, int end, _channels *channels, - int force, int resolve, queueid **res) -{ - *res = NULL; - - queueid *self = PyObject_New(queueid, cls); - if (self == NULL) { - return -1; - } - self->cid = cid; - self->end = end; - self->resolve = resolve; - self->channels = channels; - - int err = _channels_add_id_object(channels, cid); - if (err != 0) { - if (force && err == ERR_CHANNEL_NOT_FOUND) { - assert(!PyErr_Occurred()); - } - else { - Py_DECREF((PyObject *)self); - return err; - } - } - - *res = self; - return 0; -} - -static _channels * _global_channels(void); - -static PyObject * -_queueid_new(PyObject *mod, PyTypeObject *cls, - PyObject *args, PyObject *kwds) -{ - static char *kwlist[] = {"id", "send", "recv", "force", "_resolve", NULL}; - int64_t cid; - int end; - struct channel_id_converter_data cid_data = { - .module = mod, - }; - int send = -1; - int recv = -1; - int force = 0; - int resolve = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, - "O&|$pppp:ChannelID.__new__", kwlist, - channel_id_converter, &cid_data, - &send, &recv, &force, &resolve)) { - return NULL; - } - cid = cid_data.cid; - end = cid_data.end; - - // Handle "send" and "recv". - if (send == 0 && recv == 0) { - PyErr_SetString(PyExc_ValueError, - "'send' and 'recv' cannot both be False"); - return NULL; - } - else if (send == 1) { - if (recv == 0 || recv == -1) { - end = CHANNEL_SEND; - } - else { - assert(recv == 1); - end = 0; - } - } - else if (recv == 1) { - assert(send == 0 || send == -1); - end = CHANNEL_RECV; - } - - PyObject *cidobj = NULL; - int err = newqueueid(cls, cid, end, _global_channels(), - force, resolve, - (queueid **)&cidobj); - if (handle_channel_error(err, mod, cid)) { - assert(cidobj == NULL); - return NULL; - } - assert(cidobj != NULL); - return cidobj; -} - -static void -queueid_dealloc(PyObject *self) -{ - int64_t cid = ((queueid *)self)->cid; - _channels *channels = ((queueid *)self)->channels; - - PyTypeObject *tp = Py_TYPE(self); - tp->tp_free(self); - /* "Instances of heap-allocated types hold a reference to their type." - * See: https://docs.python.org/3.11/howto/isolating-extensions.html#garbage-collection-protocol - * See: https://docs.python.org/3.11/c-api/typeobj.html#c.PyTypeObject.tp_traverse - */ - // XXX Why don't we implement Py_TPFLAGS_HAVE_GC, e.g. Py_tp_traverse, - // like we do for _abc._abc_data? - Py_DECREF(tp); - - _channels_release_cid_object(channels, cid); -} - -static PyObject * -queueid_repr(PyObject *self) -{ - PyTypeObject *type = Py_TYPE(self); - const char *name = _PyType_Name(type); - - queueid *cidobj = (queueid *)self; - const char *fmt; - if (cidobj->end == CHANNEL_SEND) { - fmt = "%s(%" PRId64 ", send=True)"; - } - else if (cidobj->end == CHANNEL_RECV) { - fmt = "%s(%" PRId64 ", recv=True)"; - } - else { - fmt = "%s(%" PRId64 ")"; - } - return PyUnicode_FromFormat(fmt, name, cidobj->cid); -} - -static PyObject * -queueid_str(PyObject *self) -{ - queueid *cidobj = (queueid *)self; - return PyUnicode_FromFormat("%" PRId64 "", cidobj->cid); -} - -static PyObject * -queueid_int(PyObject *self) -{ - queueid *cidobj = (queueid *)self; - return PyLong_FromLongLong(cidobj->cid); -} - -static Py_hash_t -queueid_hash(PyObject *self) -{ - queueid *cidobj = (queueid *)self; - PyObject *pyid = PyLong_FromLongLong(cidobj->cid); - if (pyid == NULL) { - return -1; - } - Py_hash_t hash = PyObject_Hash(pyid); - Py_DECREF(pyid); - return hash; -} - -static PyObject * -queueid_richcompare(PyObject *self, PyObject *other, int op) -{ - PyObject *res = NULL; - if (op != Py_EQ && op != Py_NE) { - Py_RETURN_NOTIMPLEMENTED; - } - - PyObject *mod = get_module_from_type(Py_TYPE(self)); - if (mod == NULL) { - return NULL; - } - module_state *state = get_module_state(mod); - if (state == NULL) { - goto done; - } - - if (!PyObject_TypeCheck(self, state->QueueIDType)) { - res = Py_NewRef(Py_NotImplemented); - goto done; - } - - queueid *cidobj = (queueid *)self; - int equal; - if (PyObject_TypeCheck(other, state->QueueIDType)) { - queueid *othercidobj = (queueid *)other; - equal = (cidobj->end == othercidobj->end) && (cidobj->cid == othercidobj->cid); - } - else if (PyLong_Check(other)) { - /* Fast path */ - int overflow; - long long othercid = PyLong_AsLongLongAndOverflow(other, &overflow); - if (othercid == -1 && PyErr_Occurred()) { - goto done; - } - equal = !overflow && (othercid >= 0) && (cidobj->cid == othercid); - } - else if (PyNumber_Check(other)) { - PyObject *pyid = PyLong_FromLongLong(cidobj->cid); - if (pyid == NULL) { - goto done; - } - res = PyObject_RichCompare(pyid, other, op); - Py_DECREF(pyid); - goto done; - } - else { - res = Py_NewRef(Py_NotImplemented); - goto done; - } - - if ((op == Py_EQ && equal) || (op == Py_NE && !equal)) { - res = Py_NewRef(Py_True); - } - else { - res = Py_NewRef(Py_False); - } - -done: - Py_DECREF(mod); - return res; -} - -static PyTypeObject * _get_current_channelend_type(int end); - -static PyObject * -_channelobj_from_cidobj(PyObject *cidobj, int end) -{ - PyObject *cls = (PyObject *)_get_current_channelend_type(end); - if (cls == NULL) { - return NULL; - } - PyObject *chan = PyObject_CallFunctionObjArgs(cls, cidobj, NULL); - Py_DECREF(cls); - if (chan == NULL) { - return NULL; - } - return chan; -} - -struct _queueid_xid { - int64_t cid; - int end; - int resolve; -}; - -static PyObject * -_queueid_from_xid(_PyCrossInterpreterData *data) -{ - struct _queueid_xid *xid = (struct _queueid_xid *)data->data; - - // It might not be imported yet, so we can't use _get_current_module(). - PyObject *mod = PyImport_ImportModule(MODULE_NAME); - if (mod == NULL) { - return NULL; - } - assert(mod != Py_None); - module_state *state = get_module_state(mod); - if (state == NULL) { - return NULL; - } - - // Note that we do not preserve the "resolve" flag. - PyObject *cidobj = NULL; - int err = newqueueid(state->QueueIDType, xid->cid, xid->end, - _global_channels(), 0, 0, - (queueid **)&cidobj); - if (err != 0) { - assert(cidobj == NULL); - (void)handle_channel_error(err, mod, xid->cid); - goto done; - } - assert(cidobj != NULL); - if (xid->end == 0) { - goto done; - } - if (!xid->resolve) { - goto done; - } - - /* Try returning a high-level channel end but fall back to the ID. */ - PyObject *chan = _channelobj_from_cidobj(cidobj, xid->end); - if (chan == NULL) { - PyErr_Clear(); - goto done; - } - Py_DECREF(cidobj); - cidobj = chan; - -done: - Py_DECREF(mod); - return cidobj; -} - -static int -_queueid_shared(PyThreadState *tstate, PyObject *obj, - _PyCrossInterpreterData *data) -{ - if (_PyCrossInterpreterData_InitWithSize( - data, tstate->interp, sizeof(struct _queueid_xid), obj, - _queueid_from_xid - ) < 0) - { - return -1; - } - struct _queueid_xid *xid = (struct _queueid_xid *)data->data; - xid->cid = ((queueid *)obj)->cid; - xid->end = ((queueid *)obj)->end; - xid->resolve = ((queueid *)obj)->resolve; - return 0; -} - -static PyObject * -queueid_end(PyObject *self, void *end) -{ - int force = 1; - queueid *cidobj = (queueid *)self; - if (end != NULL) { - PyObject *obj = NULL; - int err = newqueueid(Py_TYPE(self), cidobj->cid, *(int *)end, - cidobj->channels, force, cidobj->resolve, - (queueid **)&obj); - if (err != 0) { - assert(obj == NULL); - PyObject *mod = get_module_from_type(Py_TYPE(self)); - if (mod == NULL) { - return NULL; - } - (void)handle_channel_error(err, mod, cidobj->cid); - Py_DECREF(mod); - return NULL; - } - assert(obj != NULL); - return obj; - } - - if (cidobj->end == CHANNEL_SEND) { - return PyUnicode_InternFromString("send"); +_queues_list_all(_queues *queues, int64_t *count) +{ + int64_t *qids = NULL; + PyThread_acquire_lock(queues->mutex, WAIT_LOCK); + int64_t *ids = PyMem_NEW(int64_t, (Py_ssize_t)(queues->count)); + if (ids == NULL) { + goto done; } - if (cidobj->end == CHANNEL_RECV) { - return PyUnicode_InternFromString("recv"); + _queueref *ref = queues->head; + for (int64_t i=0; ref != NULL; ref = ref->next, i++) { + ids[i] = ref->qid; } - return PyUnicode_InternFromString("both"); -} + *count = queues->count; -static int _queueid_end_send = CHANNEL_SEND; -static int _queueid_end_recv = CHANNEL_RECV; - -static PyGetSetDef queueid_getsets[] = { - {"end", (getter)queueid_end, NULL, - PyDoc_STR("'send', 'recv', or 'both'")}, - {"send", (getter)queueid_end, NULL, - PyDoc_STR("the 'send' end of the channel"), &_queueid_end_send}, - {"recv", (getter)queueid_end, NULL, - PyDoc_STR("the 'recv' end of the channel"), &_queueid_end_recv}, - {NULL} -}; + qids = ids; +done: + PyThread_release_lock(queues->mutex); + return qids; +} -PyDoc_STRVAR(queueid_doc, -"A channel ID identifies a channel and may be used as an int."); - -static PyType_Slot queueid_typeslots[] = { - {Py_tp_dealloc, (destructor)queueid_dealloc}, - {Py_tp_doc, (void *)queueid_doc}, - {Py_tp_repr, (reprfunc)queueid_repr}, - {Py_tp_str, (reprfunc)queueid_str}, - {Py_tp_hash, queueid_hash}, - {Py_tp_richcompare, queueid_richcompare}, - {Py_tp_getset, queueid_getsets}, - // number slots - {Py_nb_int, (unaryfunc)queueid_int}, - {Py_nb_index, (unaryfunc)queueid_int}, - {0, NULL}, -}; +static void +_queues_clear_interpreter(_queues *queues, int64_t interpid) +{ + PyThread_acquire_lock(queues->mutex, WAIT_LOCK); -static PyType_Spec queueid_typespec = { - .name = MODULE_NAME ".ChannelID", - .basicsize = sizeof(queueid), - .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | - Py_TPFLAGS_DISALLOW_INSTANTIATION | Py_TPFLAGS_IMMUTABLETYPE), - .slots = queueid_typeslots, -}; + _queueref *ref = queues->head; + for (; ref != NULL; ref = ref->next) { + assert(ref->queue != NULL); + _queue_clear_interpreter(ref->queue, interpid); + } + PyThread_release_lock(queues->mutex); +} -/* SendChannel and RecvChannel classes */ -// XXX Use a new __xid__ protocol instead? +/* "high"-level channel-related functions */ -static PyTypeObject * -_get_current_channelend_type(int end) +// Create a new channel. +static int64_t +queue_create(_queues *queues) { - module_state *state = _get_current_module_state(); - if (state == NULL) { - return NULL; - } - PyTypeObject *cls; - if (end == CHANNEL_SEND) { - cls = state->send_channel_type; + PyThread_type_lock mutex = PyThread_allocate_lock(); + if (mutex == NULL) { + return ERR_QUEUE_MUTEX_INIT; } - else { - assert(end == CHANNEL_RECV); - cls = state->recv_channel_type; + _queue_state *queue = _queue_new(mutex); + if (queue == NULL) { + PyThread_free_lock(mutex); + return -1; } - if (cls == NULL) { - PyObject *highlevel = PyImport_ImportModule("interpreters"); - if (highlevel == NULL) { - PyErr_Clear(); - highlevel = PyImport_ImportModule("test.support.interpreters"); - if (highlevel == NULL) { - return NULL; - } - } - Py_DECREF(highlevel); - if (end == CHANNEL_SEND) { - cls = state->send_channel_type; - } - else { - cls = state->recv_channel_type; - } - assert(cls != NULL); + int64_t qid = _queues_add(queues, queue); + if (qid < 0) { + _queue_free(queue); } - return cls; + return qid; } -static PyObject * -_channelend_from_xid(_PyCrossInterpreterData *data) +// Completely destroy the channel. +static int +queue_destroy(_queues *queues, int64_t qid) { - queueid *cidobj = (queueid *)_queueid_from_xid(data); - if (cidobj == NULL) { - return NULL; + _queue_state *queue = NULL; + int err = _queues_remove(queues, qid, &queue); + if (err != 0) { + return err; } - PyTypeObject *cls = _get_current_channelend_type(cidobj->end); - if (cls == NULL) { - Py_DECREF(cidobj); - return NULL; + if (queue != NULL) { + _queue_free(queue); } - PyObject *obj = PyObject_CallOneArg((PyObject *)cls, (PyObject *)cidobj); - Py_DECREF(cidobj); - return obj; + return 0; } +// Push an object onto the queue. static int -_channelend_shared(PyThreadState *tstate, PyObject *obj, - _PyCrossInterpreterData *data) +queue_put(_queues *queues, int64_t qid, PyObject *obj) { - PyObject *cidobj = PyObject_GetAttrString(obj, "_id"); - if (cidobj == NULL) { + // Look up the channel. + PyThread_type_lock mutex = NULL; + _queue_state *queue = NULL; + int err = _queues_lookup(queues, qid, &mutex, &queue); + if (err != 0) { + return err; + } + assert(queue != NULL); + // Past this point we are responsible for releasing the mutex. + + // Convert the object to cross-interpreter data. + _PyCrossInterpreterData *data = GLOBAL_MALLOC(_PyCrossInterpreterData); + if (data == NULL) { + PyThread_release_lock(mutex); return -1; } - int res = _queueid_shared(tstate, cidobj, data); - Py_DECREF(cidobj); - if (res < 0) { + if (_PyObject_GetCrossInterpreterData(obj, data) != 0) { + PyThread_release_lock(mutex); + GLOBAL_FREE(data); return -1; } - data->new_object = _channelend_from_xid; + + // Add the data to the channel. + int res = _queue_add(queue, data); + PyThread_release_lock(mutex); + if (res != 0) { + // We may chain an exception here: + (void)_release_xid_data(data, 0); + GLOBAL_FREE(data); + return res; + } + return 0; } +// Pop the next object off the channel. Fail if empty. +// The current interpreter gets associated with the recv end of the channel. +// XXX Support a "wait" mutex? static int -set_channelend_types(PyObject *mod, PyTypeObject *send, PyTypeObject *recv) +queue_get(_queues *queues, int64_t qid, PyObject **res) { - module_state *state = get_module_state(mod); - if (state == NULL) { - return -1; + int err; + *res = NULL; + + // Look up the channel. + PyThread_type_lock mutex = NULL; + _queue_state *queue = NULL; + err = _queues_lookup(queues, qid, &mutex, &queue); + if (err != 0) { + return err; } - struct xid_class_registry *xid_classes = &state->xid_classes; + assert(queue != NULL); + // Past this point we are responsible for releasing the mutex. - if (state->send_channel_type != NULL - || state->recv_channel_type != NULL) - { - PyErr_SetString(PyExc_TypeError, "already registered"); - return -1; + // Pop off the next item from the channel. + _PyCrossInterpreterData *data = NULL; + err = _queue_next(queue, &data); + PyThread_release_lock(mutex); + if (err != 0) { + return err; + } + else if (data == NULL) { + assert(!PyErr_Occurred()); + return 0; } - state->send_channel_type = (PyTypeObject *)Py_NewRef(send); - state->recv_channel_type = (PyTypeObject *)Py_NewRef(recv); - if (register_xid_class(send, _channelend_shared, xid_classes)) { + // Convert the data back to an object. + PyObject *obj = _PyCrossInterpreterData_NewObject(data); + if (obj == NULL) { + assert(PyErr_Occurred()); + // It was allocated in channel_send(), so we free it. + (void)_release_xid_data(data, XID_IGNORE_EXC | XID_FREE); return -1; } - if (register_xid_class(recv, _channelend_shared, xid_classes)) { + // It was allocated in channel_send(), so we free it. + int release_res = _release_xid_data(data, XID_FREE); + if (release_res < 0) { + // The source interpreter has been destroyed already. + assert(PyErr_Occurred()); + Py_DECREF(obj); return -1; } + *res = obj; return 0; } +/* channel info */ + +static int +queue_get_count(_queues *queues, int64_t qid, Py_ssize_t *p_count) +{ + int err = 0; + + // Hold the global lock until we're done. + PyThread_acquire_lock(queues->mutex, WAIT_LOCK); + + // Find the channel. + _queueref *ref = _queuerefs_find(queues->head, qid, NULL); + if (ref == NULL) { + err = ERR_QUEUE_NOT_FOUND; + goto finally; + } + _queue_state *queue = ref->queue; + assert(queue != NULL); + + // Get the number of queued objects. + assert(queue->items->count <= PY_SSIZE_T_MAX); + *p_count = (Py_ssize_t)queue->items->count; + +finally: + PyThread_release_lock(queues->mutex); + return err; +} + + /* module level code ********************************************************/ /* globals is the process-global state for the module. It holds all @@ -2865,7 +944,7 @@ set_channelend_types(PyObject *mod, PyTypeObject *send, PyTypeObject *recv) hold PyObject values. */ static struct globals { int module_count; - _channels channels; + _queues queues; } _globals = {0}; static int @@ -2878,12 +957,12 @@ _globals_init(void) return 0; } - assert(_globals.channels.mutex == NULL); + assert(_globals.queues.mutex == NULL); PyThread_type_lock mutex = PyThread_allocate_lock(); if (mutex == NULL) { - return ERR_CHANNELS_MUTEX_INIT; + return ERR_QUEUES_MUTEX_INIT; } - _channels_init(&_globals.channels, mutex); + _queues_init(&_globals.queues, mutex); return 0; } @@ -2896,12 +975,7 @@ _globals_fini(void) return; } - _channels_fini(&_globals.channels); -} - -static _channels * -_global_channels(void) { - return &_globals.channels; + _queues_fini(&_globals.queues); } @@ -2914,77 +988,82 @@ clear_interpreter(void *data) PyInterpreterState *interp = (PyInterpreterState *)data; assert(interp == _get_current_interp()); int64_t interpid = PyInterpreterState_GetID(interp); - _channels_clear_interpreter(&_globals.channels, interpid); + _queues_clear_interpreter(&_globals.queues, interpid); +} + + +typedef struct idarg_int64_converter_data qidarg_converter_data; + +static int +qidarg_converter(PyObject *arg, void *ptr) +{ + qidarg_converter_data *data = ptr; + if (data->label == NULL) { + data->label = "queue ID"; + } + return idarg_int64_converter(arg, ptr); } static PyObject * queuesmod_create(PyObject *self, PyObject *Py_UNUSED(ignored)) { - int64_t cid = channel_create(&_globals.channels); - if (cid < 0) { - (void)handle_channel_error(-1, self, cid); + int64_t qid = queue_create(&_globals.queues); + if (qid < 0) { + (void)handle_queue_error(-1, self, qid); return NULL; } module_state *state = get_module_state(self); if (state == NULL) { return NULL; } - PyObject *cidobj = NULL; - int err = newqueueid(state->QueueIDType, cid, 0, - &_globals.channels, 0, 0, - (queueid **)&cidobj); - if (handle_channel_error(err, self, cid)) { - assert(cidobj == NULL); - err = channel_destroy(&_globals.channels, cid); - if (handle_channel_error(err, self, cid)) { + PyObject *qidobj = PyLong_FromLongLong(qid); + if (qidobj == NULL) { + int err = queue_destroy(&_globals.queues, qid); + if (handle_queue_error(err, self, qid)) { // XXX issue a warning? } return NULL; } - assert(cidobj != NULL); - assert(((queueid *)cidobj)->channels != NULL); - return cidobj; + return qidobj; } PyDoc_STRVAR(queuesmod_create_doc, -"channel_create() -> cid\n\ +"create() -> qid\n\ \n\ -Create a new cross-interpreter channel and return a unique generated ID."); +Create a new cross-interpreter queue and return its unique generated ID.\n\ +It is a new reference as though bind() had been called on the queue."); static PyObject * queuesmod_destroy(PyObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"cid", NULL}; - int64_t cid; - struct channel_id_converter_data cid_data = { - .module = self, - }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&:channel_destroy", kwlist, - channel_id_converter, &cid_data)) { + static char *kwlist[] = {"qid", NULL}; + qidarg_converter_data qidarg; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&:destroy", kwlist, + qidarg_converter, &qidarg)) { return NULL; } - cid = cid_data.cid; + int64_t qid = qidarg.id; - int err = channel_destroy(&_globals.channels, cid); - if (handle_channel_error(err, self, cid)) { + int err = queue_destroy(&_globals.queues, qid); + if (handle_queue_error(err, self, qid)) { return NULL; } Py_RETURN_NONE; } PyDoc_STRVAR(queuesmod_destroy_doc, -"channel_destroy(cid)\n\ +"destroy(qid)\n\ \n\ -Close and finalize the channel. Afterward attempts to use the channel\n\ +Clear and destroy the queue. Afterward attempts to use the queue\n\ will behave as though it never existed."); static PyObject * queuesmod_list_all(PyObject *self, PyObject *Py_UNUSED(ignored)) { int64_t count = 0; - int64_t *cids = _channels_list_all(&_globals.channels, &count); - if (cids == NULL) { + int64_t *qids = _queues_list_all(&_globals.queues, &count); + if (qids == NULL) { if (count == 0) { return PyList_New(0); } @@ -3000,128 +1079,41 @@ queuesmod_list_all(PyObject *self, PyObject *Py_UNUSED(ignored)) ids = NULL; goto finally; } - int64_t *cur = cids; + int64_t *cur = qids; for (int64_t i=0; i < count; cur++, i++) { - PyObject *cidobj = NULL; - int err = newqueueid(state->QueueIDType, *cur, 0, - &_globals.channels, 0, 0, - (queueid **)&cidobj); - if (handle_channel_error(err, self, *cur)) { - assert(cidobj == NULL); + PyObject *qidobj = PyLong_FromLongLong(*cur); + if (qidobj == NULL) { Py_SETREF(ids, NULL); break; } - assert(cidobj != NULL); - PyList_SET_ITEM(ids, (Py_ssize_t)i, cidobj); + PyList_SET_ITEM(ids, (Py_ssize_t)i, qidobj); } finally: - PyMem_Free(cids); + PyMem_Free(qids); return ids; } PyDoc_STRVAR(queuesmod_list_all_doc, -"channel_list_all() -> [cid]\n\ -\n\ -Return the list of all IDs for active channels."); - -static PyObject * -queuesmod_list_interpreters(PyObject *self, PyObject *args, PyObject *kwds) -{ - static char *kwlist[] = {"cid", "send", NULL}; - int64_t cid; /* Channel ID */ - struct channel_id_converter_data cid_data = { - .module = self, - }; - int send = 0; /* Send or receive end? */ - int64_t interpid; - PyObject *ids, *interpid_obj; - PyInterpreterState *interp; - - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "O&$p:channel_list_interpreters", - kwlist, channel_id_converter, &cid_data, &send)) { - return NULL; - } - cid = cid_data.cid; - - ids = PyList_New(0); - if (ids == NULL) { - goto except; - } - - interp = PyInterpreterState_Head(); - while (interp != NULL) { - interpid = PyInterpreterState_GetID(interp); - assert(interpid >= 0); - int res = channel_is_associated(&_globals.channels, cid, interpid, send); - if (res < 0) { - (void)handle_channel_error(res, self, cid); - goto except; - } - if (res) { - interpid_obj = PyInterpreterState_GetIDObject(interp); - if (interpid_obj == NULL) { - goto except; - } - res = PyList_Insert(ids, 0, interpid_obj); - Py_DECREF(interpid_obj); - if (res < 0) { - goto except; - } - } - interp = PyInterpreterState_Next(interp); - } - - goto finally; - -except: - Py_CLEAR(ids); - -finally: - return ids; -} - -PyDoc_STRVAR(queuesmod_list_interpreters_doc, -"channel_list_interpreters(cid, *, send) -> [id]\n\ +"list_all() -> [qid]\n\ \n\ -Return the list of all interpreter IDs associated with an end of the channel.\n\ -\n\ -The 'send' argument should be a boolean indicating whether to use the send or\n\ -receive end."); - +Return the list of IDs for all queues."); static PyObject * queuesmod_put(PyObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"cid", "obj", "blocking", "timeout", NULL}; - struct channel_id_converter_data cid_data = { - .module = self, - }; + static char *kwlist[] = {"qid", "obj", NULL}; + qidarg_converter_data qidarg; PyObject *obj; - int blocking = 1; - PyObject *timeout_obj = NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O|$pO:channel_send", kwlist, - channel_id_converter, &cid_data, &obj, - &blocking, &timeout_obj)) { - return NULL; - } - - int64_t cid = cid_data.cid; - PY_TIMEOUT_T timeout; - if (PyThread_ParseTimeoutArg(timeout_obj, blocking, &timeout) < 0) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&O:put", kwlist, + qidarg_converter, &qidarg, &obj)) { return NULL; } + int64_t qid = qidarg.id; /* Queue up the object. */ - int err = 0; - if (blocking) { - err = channel_send_wait(&_globals.channels, cid, obj, timeout); - } - else { - err = channel_send(&_globals.channels, cid, obj, NULL); - } - if (handle_channel_error(err, self, cid)) { + int err = queue_put(&_globals.queues, qid, obj); + if (handle_queue_error(err, self, qid)) { return NULL; } @@ -3129,86 +1121,32 @@ queuesmod_put(PyObject *self, PyObject *args, PyObject *kwds) } PyDoc_STRVAR(queuesmod_put_doc, -"channel_send(cid, obj, blocking=True)\n\ -\n\ -Add the object's data to the channel's queue.\n\ -By default this waits for the object to be received."); - -static PyObject * -queuesmod_send_buffer(PyObject *self, PyObject *args, PyObject *kwds) -{ - static char *kwlist[] = {"cid", "obj", "blocking", "timeout", NULL}; - struct channel_id_converter_data cid_data = { - .module = self, - }; - PyObject *obj; - int blocking = 1; - PyObject *timeout_obj = NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, - "O&O|$pO:channel_send_buffer", kwlist, - channel_id_converter, &cid_data, &obj, - &blocking, &timeout_obj)) { - return NULL; - } - - int64_t cid = cid_data.cid; - PY_TIMEOUT_T timeout; - if (PyThread_ParseTimeoutArg(timeout_obj, blocking, &timeout) < 0) { - return NULL; - } - - PyObject *tempobj = PyMemoryView_FromObject(obj); - if (tempobj == NULL) { - return NULL; - } - - /* Queue up the object. */ - int err = 0; - if (blocking) { - err = channel_send_wait(&_globals.channels, cid, tempobj, timeout); - } - else { - err = channel_send(&_globals.channels, cid, tempobj, NULL); - } - Py_DECREF(tempobj); - if (handle_channel_error(err, self, cid)) { - return NULL; - } - - Py_RETURN_NONE; -} - -PyDoc_STRVAR(queuesmod_send_buffer_doc, -"channel_send_buffer(cid, obj, blocking=True)\n\ +"put(qid, obj)\n\ \n\ -Add the object's buffer to the channel's queue.\n\ -By default this waits for the object to be received."); +Add the object's data to the queue."); static PyObject * queuesmod_get(PyObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"cid", "default", NULL}; - int64_t cid; - struct channel_id_converter_data cid_data = { - .module = self, - }; + static char *kwlist[] = {"qid", "default", NULL}; + qidarg_converter_data qidarg; PyObject *dflt = NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&|O:channel_recv", kwlist, - channel_id_converter, &cid_data, &dflt)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&|O:get", kwlist, + qidarg_converter, &qidarg, &dflt)) { return NULL; } - cid = cid_data.cid; + int64_t qid = qidarg.id; PyObject *obj = NULL; - int err = channel_recv(&_globals.channels, cid, &obj); - if (handle_channel_error(err, self, cid)) { + int err = queue_get(&_globals.queues, qid, &obj); + if (handle_queue_error(err, self, qid)) { return NULL; } Py_XINCREF(dflt); if (obj == NULL) { // Use the default. if (dflt == NULL) { - (void)handle_channel_error(ERR_CHANNEL_EMPTY, self, cid); + (void)handle_queue_error(ERR_QUEUE_EMPTY, self, qid); return NULL; } obj = Py_NewRef(dflt); @@ -3218,178 +1156,94 @@ queuesmod_get(PyObject *self, PyObject *args, PyObject *kwds) } PyDoc_STRVAR(queuesmod_get_doc, -"channel_recv(cid, [default]) -> obj\n\ +"get(qid, [default]) -> obj\n\ \n\ -Return a new object from the data at the front of the channel's queue.\n\ +Return a new object from the data at the front of the queue.\n\ \n\ -If there is nothing to receive then raise ChannelEmptyError, unless\n\ +If there is nothing to receive then raise QueueEmpty, unless\n\ a default value is provided. In that case return it."); static PyObject * -queuesmod_close(PyObject *self, PyObject *args, PyObject *kwds) +queuesmod_bind(PyObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"cid", "send", "recv", "force", NULL}; - int64_t cid; - struct channel_id_converter_data cid_data = { - .module = self, - }; - int send = 0; - int recv = 0; - int force = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, - "O&|$ppp:channel_close", kwlist, - channel_id_converter, &cid_data, - &send, &recv, &force)) { + static char *kwlist[] = {"qid", NULL}; + qidarg_converter_data qidarg; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&:bind", kwlist, + qidarg_converter, &qidarg)) { return NULL; } - cid = cid_data.cid; + int64_t qid = qidarg.id; - int err = channel_close(&_globals.channels, cid, send-recv, force); - if (handle_channel_error(err, self, cid)) { + // XXX Check module state if bound already. + + int err = _queues_incref(&_globals.queues, qid); + if (handle_queue_error(err, self, qid)) { return NULL; } + + // XXX Update module state. + Py_RETURN_NONE; } -PyDoc_STRVAR(queuesmod_close_doc, -"channel_close(cid, *, send=None, recv=None, force=False)\n\ -\n\ -Close the channel for all interpreters.\n\ -\n\ -If the channel is empty then the keyword args are ignored and both\n\ -ends are immediately closed. Otherwise, if 'force' is True then\n\ -all queued items are released and both ends are immediately\n\ -closed.\n\ -\n\ -If the channel is not empty *and* 'force' is False then following\n\ -happens:\n\ +PyDoc_STRVAR(queuesmod_bind_doc, +"bind(qid)\n\ \n\ - * recv is True (regardless of send):\n\ - - raise ChannelNotEmptyError\n\ - * recv is None and send is None:\n\ - - raise ChannelNotEmptyError\n\ - * send is True and recv is not True:\n\ - - fully close the 'send' end\n\ - - close the 'recv' end to interpreters not already receiving\n\ - - fully close it once empty\n\ -\n\ -Closing an already closed channel results in a ChannelClosedError.\n\ -\n\ -Once the channel's ID has no more ref counts in any interpreter\n\ -the channel will be destroyed."); +Take a reference to the identified queue.\n\ +The queue is not destroyed until there are no references left."); static PyObject * queuesmod_release(PyObject *self, PyObject *args, PyObject *kwds) { // Note that only the current interpreter is affected. - static char *kwlist[] = {"cid", "send", "recv", "force", NULL}; - int64_t cid; - struct channel_id_converter_data cid_data = { - .module = self, - }; - int send = 0; - int recv = 0; - int force = 0; + static char *kwlist[] = {"qid", NULL}; + qidarg_converter_data qidarg; if (!PyArg_ParseTupleAndKeywords(args, kwds, - "O&|$ppp:channel_release", kwlist, - channel_id_converter, &cid_data, - &send, &recv, &force)) { + "O&:release", kwlist, + qidarg_converter, &qidarg)) { return NULL; } - cid = cid_data.cid; - if (send == 0 && recv == 0) { - send = 1; - recv = 1; - } + int64_t qid = qidarg.id; - // XXX Handle force is True. - // XXX Fix implicit release. + // XXX Check module state if bound already. + // XXX Update module state. + + _queues_decref(&_globals.queues, qid); - int err = channel_release(&_globals.channels, cid, send, recv); - if (handle_channel_error(err, self, cid)) { - return NULL; - } Py_RETURN_NONE; } PyDoc_STRVAR(queuesmod_release_doc, -"channel_release(cid, *, send=None, recv=None, force=True)\n\ +"release(qid)\n\ \n\ -Close the channel for the current interpreter. 'send' and 'recv'\n\ -(bool) may be used to indicate the ends to close. By default both\n\ -ends are closed. Closing an already closed end is a noop."); +Release a reference to the queue.\n\ +The queue is destroyed once there are no references left."); static PyObject * -queuesmod_get_info(PyObject *self, PyObject *args, PyObject *kwds) +queuesmod_get_count(PyObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"cid", NULL}; - struct channel_id_converter_data cid_data = { - .module = self, - }; + static char *kwlist[] = {"qid", NULL}; + qidarg_converter_data qidarg; if (!PyArg_ParseTupleAndKeywords(args, kwds, - "O&:_get_info", kwlist, - channel_id_converter, &cid_data)) { + "O&:get_count", kwlist, + qidarg_converter, &qidarg)) { return NULL; } - int64_t cid = cid_data.cid; + int64_t qid = qidarg.id; - struct channel_info info; - int err = _channel_get_info(&_globals.channels, cid, &info); - if (handle_channel_error(err, self, cid)) { + Py_ssize_t count = -1; + int err = queue_get_count(&_globals.queues, qid, &count); + if (handle_queue_error(err, self, qid)) { return NULL; } - return new_channel_info(self, &info); + assert(count >= 0); + return PyLong_FromSsize_t(count); } -PyDoc_STRVAR(queuesmod_get_info_doc, -"get_info(cid)\n\ +PyDoc_STRVAR(queuesmod_get_count_doc, +"get_count(qid)\n\ \n\ -Return details about the channel."); - -static PyObject * -queuesmod__queue_id(PyObject *self, PyObject *args, PyObject *kwds) -{ - module_state *state = get_module_state(self); - if (state == NULL) { - return NULL; - } - PyTypeObject *cls = state->QueueIDType; - - PyObject *mod = get_module_from_owned_type(cls); - assert(mod == self); - Py_DECREF(mod); - - return _queueid_new(self, cls, args, kwds); -} - -static PyObject * -queuesmod__register_queue_type(PyObject *self, PyObject *args, PyObject *kwds) -{ - static char *kwlist[] = {"send", "recv", NULL}; - PyObject *send; - PyObject *recv; - if (!PyArg_ParseTupleAndKeywords(args, kwds, - "OO:_register_queue_type", kwlist, - &send, &recv)) { - return NULL; - } - if (!PyType_Check(send)) { - PyErr_SetString(PyExc_TypeError, "expected a type for 'send'"); - return NULL; - } - if (!PyType_Check(recv)) { - PyErr_SetString(PyExc_TypeError, "expected a type for 'recv'"); - return NULL; - } - PyTypeObject *cls_send = (PyTypeObject *)send; - PyTypeObject *cls_recv = (PyTypeObject *)recv; - - if (set_channelend_types(self, cls_send, cls_recv) < 0) { - return NULL; - } - - Py_RETURN_NONE; -} +Return the number of items in the queue."); static PyMethodDef module_functions[] = { {"create", queuesmod_create, @@ -3402,16 +1256,12 @@ static PyMethodDef module_functions[] = { METH_VARARGS | METH_KEYWORDS, queuesmod_put_doc}, {"get", _PyCFunction_CAST(queuesmod_get), METH_VARARGS | METH_KEYWORDS, queuesmod_get_doc}, - {"close", _PyCFunction_CAST(queuesmod_close), - METH_VARARGS | METH_KEYWORDS, queuesmod_close_doc}, + {"bind", _PyCFunction_CAST(queuesmod_bind), + METH_VARARGS | METH_KEYWORDS, queuesmod_bind_doc}, {"release", _PyCFunction_CAST(queuesmod_release), METH_VARARGS | METH_KEYWORDS, queuesmod_release_doc}, - {"get_info", _PyCFunction_CAST(queuesmod_get_info), - METH_VARARGS | METH_KEYWORDS, queuesmod_get_info_doc}, - {"_queue_id", _PyCFunction_CAST(queuesmod__queue_id), - METH_VARARGS | METH_KEYWORDS, NULL}, -// {"_register_queue_type", _PyCFunction_CAST(queuesmod__register_queue_type), -// METH_VARARGS | METH_KEYWORDS, NULL}, + {"get_count", _PyCFunction_CAST(queuesmod_get_count), + METH_VARARGS | METH_KEYWORDS, queuesmod_get_count_doc}, {NULL, NULL} /* sentinel */ }; @@ -3429,59 +1279,24 @@ module_exec(PyObject *mod) if (_globals_init() != 0) { return -1; } - struct xid_class_registry *xid_classes = NULL; module_state *state = get_module_state(mod); if (state == NULL) { goto error; } - xid_classes = &state->xid_classes; /* Add exception types */ if (exceptions_init(mod) != 0) { goto error; } - /* Add other types */ - - // ChannelInfo - state->ChannelInfoType = PyStructSequence_NewType(&channel_info_desc); - if (state->ChannelInfoType == NULL) { - goto error; - } - if (PyModule_AddType(mod, state->ChannelInfoType) < 0) { - goto error; - } - - // ChannelID - state->QueueIDType = add_new_type( - mod, &queueid_typespec, _queueid_shared, xid_classes); - if (state->QueueIDType == NULL) { - goto error; - } - - // XIBufferView - state->XIBufferViewType = add_new_type(mod, &XIBufferViewType_spec, NULL, - xid_classes); - if (state->XIBufferViewType == NULL) { - goto error; - } - - // Register external types. - if (register_builtin_xid_types(xid_classes) < 0) { - goto error; - } - - /* Make sure chnnels drop objects owned by this interpreter. */ + /* Make sure queues drop objects owned by this interpreter. */ PyInterpreterState *interp = _get_current_interp(); PyUnstable_AtExit(interp, clear_interpreter, (void *)interp); return 0; error: - if (xid_classes != NULL) { - clear_xid_class_registry(xid_classes); - } _globals_fini(); return -1; } @@ -3507,9 +1322,6 @@ module_clear(PyObject *mod) module_state *state = get_module_state(mod); assert(state != NULL); - // Before clearing anything, we unregister the various XID types. */ - clear_xid_class_registry(&state->xid_classes); - // Now we clear the module state. clear_module_state(state); return 0; @@ -3521,9 +1333,6 @@ module_free(void *mod) module_state *state = get_module_state(mod); assert(state != NULL); - // Before clearing anything, we unregister the various XID types. */ - clear_xid_class_registry(&state->xid_classes); - // Now we clear the module state. clear_module_state(state); From 269453e21b3ca3f273de09136fd820100fecd314 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 Dec 2023 16:12:03 -0700 Subject: [PATCH 03/14] Fix _get_current_channel_type(). --- Modules/_xxinterpchannelsmodule.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/_xxinterpchannelsmodule.c b/Modules/_xxinterpchannelsmodule.c index 97729ec269cb62..4e9b8a82a3f630 100644 --- a/Modules/_xxinterpchannelsmodule.c +++ b/Modules/_xxinterpchannelsmodule.c @@ -2629,10 +2629,11 @@ _get_current_channelend_type(int end) cls = state->recv_channel_type; } if (cls == NULL) { - PyObject *highlevel = PyImport_ImportModule("interpreters"); + // Force the module to be loaded, to register the type. + PyObject *highlevel = PyImport_ImportModule("interpreters.channel"); if (highlevel == NULL) { PyErr_Clear(); - highlevel = PyImport_ImportModule("test.support.interpreters"); + highlevel = PyImport_ImportModule("test.support.interpreters.channel"); if (highlevel == NULL) { return NULL; } From 01ef6c6ff63046940fae4478d9e48da2d718c120 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 5 Dec 2023 17:10:23 -0700 Subject: [PATCH 04/14] Make Queue shareable. --- Lib/test/support/interpreters/queues.py | 3 +- Lib/test/test_interpreters/test_queues.py | 39 ++++- Modules/_xxinterpqueuesmodule.c | 196 ++++++++++++++++++++++ 3 files changed, 231 insertions(+), 7 deletions(-) diff --git a/Lib/test/support/interpreters/queues.py b/Lib/test/support/interpreters/queues.py index cd18c14a477b60..e1e9c56b9de645 100644 --- a/Lib/test/support/interpreters/queues.py +++ b/Lib/test/support/interpreters/queues.py @@ -157,5 +157,4 @@ def get_nowait(self, *, _sentinel=object()): return obj -# XXX add this: -#_channels._register_queue_type(Queue) +_queues._register_queue_type(Queue) diff --git a/Lib/test/test_interpreters/test_queues.py b/Lib/test/test_interpreters/test_queues.py index 2a7f3246e2c461..388ed0be063372 100644 --- a/Lib/test/test_interpreters/test_queues.py +++ b/Lib/test/test_interpreters/test_queues.py @@ -47,13 +47,42 @@ def test_create(self): with self.assertRaises(TypeError): queues.create('1') - @unittest.expectedFailure def test_shareable(self): queue1 = queues.create() - queue2 = queues.create() - queue1.put(queue2) - queue3 = queue1.get() - self.assertIs(queue3, queue1) + + interp = interpreters.create() + interp.exec_sync(dedent(f""" + from test.support.interpreters import queues + queue1 = queues.Queue({queue1.id}) + """)); + + with self.subTest('same interpreter'): + queue2 = queues.create() + queue1.put(queue2) + queue3 = queue1.get() + self.assertIs(queue3, queue2) + + with self.subTest('from current interpreter'): + queue4 = queues.create() + queue1.put(queue4) + out = _run_output(interp, dedent(""" + queue4 = queue1.get() + print(queue4.id) + """)) + qid = int(out) + self.assertEqual(qid, queue4.id) + + with self.subTest('from subinterpreter'): + out = _run_output(interp, dedent(""" + queue5 = queues.create() + queue1.put(queue5) + print(queue5.id) + """)) + qid = int(out) + queue5 = queue1.get() + self.assertEqual(queue5.id, qid) + + # XXX check with maxsize def test_id_type(self): queue = queues.create() diff --git a/Modules/_xxinterpqueuesmodule.c b/Modules/_xxinterpqueuesmodule.c index c5fe52929d98e1..533d3567d69a9f 100644 --- a/Modules/_xxinterpqueuesmodule.c +++ b/Modules/_xxinterpqueuesmodule.c @@ -61,6 +61,22 @@ _get_current_interp(void) return PyInterpreterState_Get(); } +static PyObject * +_get_current_module(void) +{ + PyObject *name = PyUnicode_FromString(MODULE_NAME); + if (name == NULL) { + return NULL; + } + PyObject *mod = PyImport_GetModule(name); + Py_DECREF(name); + if (mod == NULL) { + return NULL; + } + assert(mod != Py_None); + return mod; +} + static PyObject * add_new_exception(PyObject *mod, const char *name, PyObject *base) { @@ -130,6 +146,9 @@ idarg_int64_converter(PyObject *arg, void *ptr) /* module state *************************************************************/ typedef struct { + /* external types (added at runtime by interpreters module) */ + PyTypeObject *queue_type; + /* exceptions */ PyObject *QueueError; PyObject *QueueNotFoundError; @@ -144,9 +163,27 @@ get_module_state(PyObject *mod) return state; } +static module_state * +_get_current_module_state(void) +{ + PyObject *mod = _get_current_module(); + if (mod == NULL) { + // XXX import it? + PyErr_SetString(PyExc_RuntimeError, + MODULE_NAME " module not imported yet"); + return NULL; + } + module_state *state = get_module_state(mod); + Py_DECREF(mod); + return state; +} + static int traverse_module_state(module_state *state, visitproc visit, void *arg) { + /* external types */ + Py_VISIT(state->queue_type); + /* exceptions */ Py_VISIT(state->QueueError); Py_VISIT(state->QueueNotFoundError); @@ -157,6 +194,9 @@ traverse_module_state(module_state *state, visitproc visit, void *arg) static int clear_module_state(module_state *state) { + /* external types */ + Py_CLEAR(state->queue_type); + /* exceptions */ Py_CLEAR(state->QueueError); Py_CLEAR(state->QueueNotFoundError); @@ -937,6 +977,116 @@ queue_get_count(_queues *queues, int64_t qid, Py_ssize_t *p_count) } +/* external objects *********************************************************/ + +// XXX Use a new __xid__ protocol instead? + +static PyTypeObject * +_get_current_queue_type(void) +{ + module_state *state = _get_current_module_state(); + assert(state != NULL); + + PyTypeObject *cls = state->queue_type; + if (cls == NULL) { + // Force the module to be loaded, to register the type. + PyObject *highlevel = PyImport_ImportModule("interpreters.queue"); + if (highlevel == NULL) { + PyErr_Clear(); + highlevel = PyImport_ImportModule("test.support.interpreters.queue"); + if (highlevel == NULL) { + return NULL; + } + } + Py_DECREF(highlevel); + cls = state->queue_type; + assert(cls != NULL); + } + return cls; +} + +struct _queueid_xid { + int64_t qid; +}; + +static _queues * _get_global_queues(void); + +static void * +_queueid_xid_new(int64_t qid) +{ + _queues *queues = _get_global_queues(); + if (_queues_incref(queues, qid) < 0) { + return NULL; + } + + struct _queueid_xid *data = PyMem_RawMalloc(sizeof(struct _queueid_xid)); + if (data == NULL) { + _queues_decref(queues, qid); + return NULL; + } + data->qid = qid; + return (void *)data; +} + +static void +_queueid_xid_free(void *data) +{ + int64_t qid = ((struct _queueid_xid *)data)->qid; + PyMem_RawFree(data); + _queues *queues = _get_global_queues(); + _queues_decref(queues, qid); +} + +static PyObject * +_queueobj_from_xid(_PyCrossInterpreterData *data) +{ + int64_t qid = *(int64_t *)data->data; + PyObject *qidobj = PyLong_FromLongLong(qid); + if (qidobj == NULL) { + return NULL; + } + + PyTypeObject *cls = _get_current_queue_type(); + if (cls == NULL) { + Py_DECREF(qidobj); + return NULL; + } + PyObject *obj = PyObject_CallOneArg((PyObject *)cls, (PyObject *)qidobj); + Py_DECREF(qidobj); + return obj; +} + +static int +_queueobj_shared(PyThreadState *tstate, PyObject *queueobj, + _PyCrossInterpreterData *data) +{ + PyObject *qidobj = PyObject_GetAttrString(queueobj, "_id"); + if (qidobj == NULL) { + return -1; + } + struct idarg_int64_converter_data converted = { + .label = "queue ID", + }; + int res = idarg_int64_converter(qidobj, &converted); + Py_DECREF(qidobj); + if (!res) { + assert(PyErr_Occurred()); + return -1; + } + + void *raw = _queueid_xid_new(converted.id); + if (raw == NULL) { + Py_DECREF(qidobj); + return -1; + } + _PyCrossInterpreterData_Init(data, tstate->interp, raw, NULL, + _queueobj_from_xid); + Py_DECREF(qidobj); + data->free = _queueid_xid_free; + return 0; +} + + /* module level code ********************************************************/ /* globals is the process-global state for the module. It holds all @@ -978,6 +1128,12 @@ _globals_fini(void) _queues_fini(&_globals.queues); } +static _queues * +_get_global_queues(void) +{ + return &_globals.queues; +} + static void clear_interpreter(void *data) @@ -1245,6 +1401,40 @@ PyDoc_STRVAR(queuesmod_get_count_doc, \n\ Return the number of items in the queue."); +static PyObject * +queuesmod__register_queue_type(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"queuetype", NULL}; + PyObject *queuetype; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O:_register_queue_type", kwlist, + &queuetype)) { + return NULL; + } + if (!PyType_Check(queuetype)) { + PyErr_SetString(PyExc_TypeError, "expected a type for 'queuetype'"); + return NULL; + } + PyTypeObject *cls_queue = (PyTypeObject *)queuetype; + + module_state *state = get_module_state(self); + if (state == NULL) { + return NULL; + } + + if (state->queue_type != NULL) { + PyErr_SetString(PyExc_TypeError, "already registered"); + return NULL; + } + state->queue_type = (PyTypeObject *)Py_NewRef(cls_queue); + + if (_PyCrossInterpreterData_RegisterClass(cls_queue, _queueobj_shared) < 0) { + return NULL; + } + + Py_RETURN_NONE; +} + static PyMethodDef module_functions[] = { {"create", queuesmod_create, METH_NOARGS, queuesmod_create_doc}, @@ -1262,6 +1452,8 @@ static PyMethodDef module_functions[] = { METH_VARARGS | METH_KEYWORDS, queuesmod_release_doc}, {"get_count", _PyCFunction_CAST(queuesmod_get_count), METH_VARARGS | METH_KEYWORDS, queuesmod_get_count_doc}, + {"_register_queue_type", _PyCFunction_CAST(queuesmod__register_queue_type), + METH_VARARGS | METH_KEYWORDS, NULL}, {NULL, NULL} /* sentinel */ }; @@ -1322,6 +1514,10 @@ module_clear(PyObject *mod) module_state *state = get_module_state(mod); assert(state != NULL); + if (state->queue_type != NULL) { + (void)_PyCrossInterpreterData_UnregisterClass(state->queue_type); + } + // Now we clear the module state. clear_module_state(state); return 0; From 1dbef37584af5d3be04ebd161ebd495207ce73d3 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Fri, 8 Dec 2023 11:09:36 -0700 Subject: [PATCH 05/14] Fix a deadlock. --- Modules/_xxinterpqueuesmodule.c | 590 +++++++++++++++----------------- 1 file changed, 280 insertions(+), 310 deletions(-) diff --git a/Modules/_xxinterpqueuesmodule.c b/Modules/_xxinterpqueuesmodule.c index 533d3567d69a9f..37648604e937cf 100644 --- a/Modules/_xxinterpqueuesmodule.c +++ b/Modules/_xxinterpqueuesmodule.c @@ -163,21 +163,6 @@ get_module_state(PyObject *mod) return state; } -static module_state * -_get_current_module_state(void) -{ - PyObject *mod = _get_current_module(); - if (mod == NULL) { - // XXX import it? - PyErr_SetString(PyExc_RuntimeError, - MODULE_NAME " module not imported yet"); - return NULL; - } - module_state *state = get_module_state(mod); - Py_DECREF(mod); - return state; -} - static int traverse_module_state(module_state *state, visitproc visit, void *arg) { @@ -205,14 +190,12 @@ clear_module_state(module_state *state) } -/* queue-specific code ******************************************************/ - -/* queue errors */ +/* queue errors *************************************************************/ #define ERR_QUEUE_NOT_FOUND -2 #define ERR_QUEUE_EMPTY -5 -#define ERR_QUEUE_MUTEX_INIT -7 -#define ERR_QUEUES_MUTEX_INIT -8 +#define ERR_QUEUE_ALLOC -7 +#define ERR_QUEUES_ALLOC -8 #define ERR_NO_NEXT_QUEUE_ID -9 static int @@ -261,13 +244,13 @@ handle_queue_error(int err, PyObject *mod, int64_t qid) //PyErr_Format(state->QueueEmpty, "queue %" PRId64 " is empty", qid); } - else if (err == ERR_QUEUE_MUTEX_INIT) { + else if (err == ERR_QUEUE_ALLOC) { PyErr_SetString(state->QueueError, - "can't initialize mutex for new queue"); + "can't allocate memory for new queue"); } - else if (err == ERR_QUEUES_MUTEX_INIT) { + else if (err == ERR_QUEUES_ALLOC) { PyErr_SetString(state->QueueError, - "can't initialize mutex for queue management"); + "can't allocate memory for queue management"); } else if (err == ERR_NO_NEXT_QUEUE_ID) { PyErr_SetString(state->QueueError, @@ -280,9 +263,7 @@ handle_queue_error(int err, PyObject *mod, int64_t qid) } -/* the channel queue */ - -typedef uintptr_t _queueitem_id_t; +/* the basic queue **********************************************************/ struct _queueitem; @@ -291,12 +272,6 @@ typedef struct _queueitem { struct _queueitem *next; } _queueitem; -static inline _queueitem_id_t -_queueitem_ID(_queueitem *item) -{ - return (_queueitem_id_t)item; -} - static void _queueitem_init(_queueitem *item, _PyCrossInterpreterData *data) { @@ -311,7 +286,7 @@ _queueitem_clear(_queueitem *item) item->next = NULL; if (item->data != NULL) { - // It was allocated in channel_send(). + // It was allocated in queue_put(). (void)_release_xid_data(item->data, XID_IGNORE_EXC & XID_FREE); item->data = NULL; } @@ -355,187 +330,210 @@ _queueitem_popped(_queueitem *item, _PyCrossInterpreterData **p_data) _queueitem_free(item); } -typedef struct _queueitems { - int64_t count; - _queueitem *first; - _queueitem *last; -} _queueitems; -static _queueitems * -_queueitems_new(void) +/* the queue */ +typedef struct _queue { + Py_ssize_t num_waiters; // protected by global lock + PyThread_type_lock mutex; + int alive; + struct _queueitems { + int64_t count; + _queueitem *first; + _queueitem *last; + } items; +} _queue; + +static int +_queue_init(_queue *queue) { - _queueitems *queue = GLOBAL_MALLOC(_queueitems); - if (queue == NULL) { - PyErr_NoMemory(); - return NULL; + PyThread_type_lock mutex = PyThread_allocate_lock(); + if (mutex == NULL) { + return ERR_QUEUE_ALLOC; } - queue->count = 0; - queue->first = NULL; - queue->last = NULL; - return queue; + *queue = (_queue){ + .mutex = mutex, + .alive = 1, + }; + return 0; } static void -_queueitems_clear(_queueitems *queue) +_queue_clear(_queue *queue) { - _queueitem_free_all(queue->first); - queue->count = 0; - queue->first = NULL; - queue->last = NULL; + assert(!queue->alive); + assert(queue->num_waiters == 0); + _queueitem_free_all(queue->items.first); + assert(queue->mutex != NULL); + PyThread_free_lock(queue->mutex); + *queue = (_queue){0}; } static void -_queueitems_free(_queueitems *queue) +_queue_kill_and_wait(_queue *queue) { - _queueitems_clear(queue); - GLOBAL_FREE(queue); + // Mark it as dead. + PyThread_acquire_lock(queue->mutex, WAIT_LOCK); + assert(queue->alive); + queue->alive = 0; + PyThread_release_lock(queue->mutex); + + // Wait for all waiters to fail. + while (queue->num_waiters > 0) { + PyThread_acquire_lock(queue->mutex, WAIT_LOCK); + PyThread_release_lock(queue->mutex); + }; +} + +static void +_queue_mark_waiter(_queue *queue, PyThread_type_lock parent_mutex) +{ + if (parent_mutex != NULL) { + PyThread_acquire_lock(parent_mutex, WAIT_LOCK); + queue->num_waiters += 1; + PyThread_release_lock(parent_mutex); + } + else { + // The caller must be holding the parent lock already. + queue->num_waiters += 1; + } +} + +static void +_queue_unmark_waiter(_queue *queue, PyThread_type_lock parent_mutex) +{ + if (parent_mutex != NULL) { + PyThread_acquire_lock(parent_mutex, WAIT_LOCK); + queue->num_waiters -= 1; + PyThread_release_lock(parent_mutex); + } + else { + // The caller must be holding the parent lock already. + queue->num_waiters -= 1; + } +} + +static int +_queue_lock(_queue *queue) +{ + // The queue must be marked as a waiter already. + PyThread_acquire_lock(queue->mutex, WAIT_LOCK); + if (!queue->alive) { + PyThread_release_lock(queue->mutex); + return ERR_QUEUE_NOT_FOUND; + } + return 0; +} + +static void +_queue_unlock(_queue *queue) +{ + PyThread_release_lock(queue->mutex); } static int -_queueitems_put(_queueitems *queue, - _PyCrossInterpreterData *data) +_queue_add(_queue *queue, _PyCrossInterpreterData *data) { + int err = _queue_lock(queue); + if (err < 0) { + return err; + } + _queueitem *item = _queueitem_new(data); if (item == NULL) { + _queue_unlock(queue); return -1; } - queue->count += 1; - if (queue->first == NULL) { - queue->first = item; + queue->items.count += 1; + if (queue->items.first == NULL) { + queue->items.first = item; } else { - queue->last->next = item; + queue->items.last->next = item; } - queue->last = item; + queue->items.last = item; + _queue_unlock(queue); return 0; } static int -_queueitems_get(_queueitems *queue, _PyCrossInterpreterData **p_data) +_queue_next(_queue *queue, _PyCrossInterpreterData **p_data) { - _queueitem *item = queue->first; + int err = _queue_lock(queue); + if (err < 0) { + return err; + } + + _queueitem *item = queue->items.first; if (item == NULL) { + _queue_unlock(queue); return ERR_QUEUE_EMPTY; } - queue->first = item->next; - if (queue->last == item) { - queue->last = NULL; + queue->items.first = item->next; + if (queue->items.last == item) { + queue->items.last = NULL; } - queue->count -= 1; + queue->items.count -= 1; _queueitem_popped(item, p_data); + + _queue_unlock(queue); + return 0; +} + +static int +_queue_get_count(_queue *queue, Py_ssize_t *p_count) +{ + int err = _queue_lock(queue); + if (err < 0) { + return err; + } + + // Get the number of queued objects. + assert(queue->items.count <= PY_SSIZE_T_MAX); + *p_count = (Py_ssize_t)queue->items.count; + + _queue_unlock(queue); return 0; } static void -_queueitems_clear_interpreter(_queueitems *queue, int64_t interpid) +_queue_clear_interpreter(_queue *queue, int64_t interpid) { + int err = _queue_lock(queue); + if (err == ERR_QUEUE_NOT_FOUND) { + // The queue is already destroyed, so there's nothing to clear. + assert(!PyErr_Occurred()); + return; + } + assert(err == 0); // There should be no other errors. + _queueitem *prev = NULL; - _queueitem *next = queue->first; + _queueitem *next = queue->items.first; while (next != NULL) { _queueitem *item = next; next = item->next; if (item->data->interpid == interpid) { if (prev == NULL) { - queue->first = item->next; + queue->items.first = item->next; } else { prev->next = item->next; } _queueitem_free(item); - queue->count -= 1; + queue->items.count -= 1; } else { prev = item; } } -} - - -/* each channel's state */ - -struct _queue; - -typedef struct _queue { - PyThread_type_lock mutex; - _queueitems *items; -} _queue_state; - -static _queue_state * -_queue_new(PyThread_type_lock mutex) -{ - _queue_state *queue = GLOBAL_MALLOC(_queue_state); - if (queue == NULL) { - return NULL; - } - queue->mutex = mutex; - queue->items = _queueitems_new(); - if (queue->items == NULL) { - GLOBAL_FREE(queue); - return NULL; - } - return queue; -} - -static void -_queue_free(_queue_state *queue) -{ - PyThread_acquire_lock(queue->mutex, WAIT_LOCK); - _queueitems_free(queue->items); - PyThread_release_lock(queue->mutex); - - PyThread_free_lock(queue->mutex); - GLOBAL_FREE(queue); -} - -static int -_queue_add(_queue_state *queue, _PyCrossInterpreterData *data) -{ - int res = -1; - PyThread_acquire_lock(queue->mutex, WAIT_LOCK); - - if (_queueitems_put(queue->items, data) != 0) { - goto done; - } - - res = 0; -done: - PyThread_release_lock(queue->mutex); - return res; -} -static int -_queue_next(_queue_state *queue, _PyCrossInterpreterData **p_data) -{ - int err = 0; - PyThread_acquire_lock(queue->mutex, WAIT_LOCK); - -#ifdef NDEBUG - (void)_queueitems_get(queue->items, p_data); -#else - int empty = _queueitems_get(queue->items, p_data); - assert(empty == 0 || empty == ERR_QUEUE_EMPTY); -#endif - assert(!PyErr_Occurred()); - - PyThread_release_lock(queue->mutex); - return err; + _queue_unlock(queue); } -static void -_queue_clear_interpreter(_queue_state *queue, int64_t interpid) -{ - PyThread_acquire_lock(queue->mutex, WAIT_LOCK); - _queueitems_clear_interpreter(queue->items, interpid); - - PyThread_release_lock(queue->mutex); -} - - -/* the set of channels */ +/* external queue references ************************************************/ struct _queueref; @@ -543,31 +541,9 @@ typedef struct _queueref { struct _queueref *next; int64_t qid; Py_ssize_t refcount; - _queue_state *queue; + _queue *queue; } _queueref; -static _queueref * -_queueref_new(int64_t qid, _queue_state *queue) -{ - _queueref *ref = GLOBAL_MALLOC(_queueref); - if (ref == NULL) { - return NULL; - } - ref->next = NULL; - ref->qid = qid; - ref->refcount = 0; - ref->queue = queue; - return ref; -} - -static void -_queueref_free(_queueref *ref) -{ - assert(ref->next == NULL); - // ref->queue is freed by the caller. - GLOBAL_FREE(ref); -} - static _queueref * _queuerefs_find(_queueref *first, int64_t qid, _queueref **pprev) { @@ -587,6 +563,8 @@ _queuerefs_find(_queueref *first, int64_t qid, _queueref **pprev) } +/* a collection of queues ***************************************************/ + typedef struct _queues { PyThread_type_lock mutex; _queueref *head; @@ -627,41 +605,28 @@ _queues_next_id(_queues *queues) // needs lock } static int -_queues_lookup(_queues *queues, int64_t qid, PyThread_type_lock *pmutex, - _queue_state **res) +_queues_lookup(_queues *queues, int64_t qid, _queue **res) { - int err = -1; - _queue_state *queue = NULL; PyThread_acquire_lock(queues->mutex, WAIT_LOCK); - if (pmutex != NULL) { - *pmutex = NULL; - } _queueref *ref = _queuerefs_find(queues->head, qid, NULL); if (ref == NULL) { - err = ERR_QUEUE_NOT_FOUND; - goto done; + PyThread_release_lock(queues->mutex); + return ERR_QUEUE_NOT_FOUND; } assert(ref->queue != NULL); + _queue *queue = ref->queue; + _queue_mark_waiter(queue, NULL); + // The caller must unmark it. - if (pmutex != NULL) { - // The mutex will be closed by the caller. - *pmutex = queues->mutex; - } - - queue = ref->queue; - err = 0; + PyThread_release_lock(queues->mutex); -done: - if (pmutex == NULL || *pmutex == NULL) { - PyThread_release_lock(queues->mutex); - } *res = queue; - return err; + return 0; } static int64_t -_queues_add(_queues *queues, _queue_state *queue) +_queues_add(_queues *queues, _queue *queue) { int64_t qid = -1; PyThread_acquire_lock(queues->mutex, WAIT_LOCK); @@ -672,13 +637,18 @@ _queues_add(_queues *queues, _queue_state *queue) qid = ERR_NO_NEXT_QUEUE_ID; goto done; } - _queueref *ref = _queueref_new(_qid, queue); + _queueref *ref = GLOBAL_MALLOC(_queueref); if (ref == NULL) { + qid = ERR_QUEUE_ALLOC; goto done; } + *ref = (_queueref){ + .qid = _qid, + .queue = queue, + }; // Add it to the list. - // We assume that the channel is a new one (not already in the list). + // We assume that the queue is a new one (not already in the list). ref->next = queues->head; queues->head = ref; queues->count += 1; @@ -691,8 +661,10 @@ _queues_add(_queues *queues, _queue_state *queue) static void _queues_remove_ref(_queues *queues, _queueref *ref, _queueref *prev, - _queue_state **p_queue) + _queue **p_queue) { + assert(ref->queue != NULL); + if (ref == queues->head) { queues->head = ref->next; } @@ -702,35 +674,27 @@ _queues_remove_ref(_queues *queues, _queueref *ref, _queueref *prev, ref->next = NULL; queues->count -= 1; - if (p_queue != NULL) { - *p_queue = ref->queue; - } - _queueref_free(ref); + *p_queue = ref->queue; + ref->queue = NULL; + GLOBAL_FREE(ref); } static int -_queues_remove(_queues *queues, int64_t qid, _queue_state **p_queue) +_queues_remove(_queues *queues, int64_t qid, _queue **p_queue) { - int res = -1; PyThread_acquire_lock(queues->mutex, WAIT_LOCK); - if (p_queue != NULL) { - *p_queue = NULL; - } - _queueref *prev = NULL; _queueref *ref = _queuerefs_find(queues->head, qid, &prev); if (ref == NULL) { - res = ERR_QUEUE_NOT_FOUND; - goto done; + PyThread_release_lock(queues->mutex); + return ERR_QUEUE_NOT_FOUND; } _queues_remove_ref(queues, ref, prev, p_queue); - - res = 0; -done: PyThread_release_lock(queues->mutex); - return res; + + return 0; } static int @@ -754,6 +718,8 @@ _queues_incref(_queues *queues, int64_t qid) return res; } +static void _queue_free(_queue *); + static void _queues_decref(_queues *queues, int64_t qid) { @@ -765,21 +731,24 @@ _queues_decref(_queues *queues, int64_t qid) assert(!PyErr_Occurred()); // Already destroyed. // XXX Warn? - goto done; + goto finally; } assert(ref->refcount > 0); ref->refcount -= 1; // Destroy if no longer used. + assert(ref->queue != NULL); if (ref->refcount == 0) { - _queue_state *queue = NULL; + _queue *queue = NULL; _queues_remove_ref(queues, ref, prev, &queue); - if (queue != NULL) { - _queue_free(queue); - } + PyThread_release_lock(queues->mutex); + + _queue_kill_and_wait(queue); + _queue_free(queue); + return; } -done: +finally: PyThread_release_lock(queues->mutex); } @@ -819,40 +788,47 @@ _queues_clear_interpreter(_queues *queues, int64_t interpid) } -/* "high"-level channel-related functions */ +/* "high"-level queue-related functions *************************************/ -// Create a new channel. +static void +_queue_free(_queue *queue) +{ + _queue_clear(queue); + GLOBAL_FREE(queue); +} + +// Create a new queue. static int64_t queue_create(_queues *queues) { - PyThread_type_lock mutex = PyThread_allocate_lock(); - if (mutex == NULL) { - return ERR_QUEUE_MUTEX_INIT; - } - _queue_state *queue = _queue_new(mutex); + _queue *queue = GLOBAL_MALLOC(_queue); if (queue == NULL) { - PyThread_free_lock(mutex); - return -1; + return ERR_QUEUE_ALLOC; + } + int err = _queue_init(queue); + if (err < 0) { + GLOBAL_FREE(queue); + return (int64_t)err; } int64_t qid = _queues_add(queues, queue); if (qid < 0) { - _queue_free(queue); + _queue_clear(queue); + GLOBAL_FREE(queue); } return qid; } -// Completely destroy the channel. +// Completely destroy the queue. static int queue_destroy(_queues *queues, int64_t qid) { - _queue_state *queue = NULL; + _queue *queue = NULL; int err = _queues_remove(queues, qid, &queue); - if (err != 0) { + if (err < 0) { return err; } - if (queue != NULL) { - _queue_free(queue); - } + _queue_kill_and_wait(queue); + _queue_free(queue); return 0; } @@ -860,31 +836,29 @@ queue_destroy(_queues *queues, int64_t qid) static int queue_put(_queues *queues, int64_t qid, PyObject *obj) { - // Look up the channel. - PyThread_type_lock mutex = NULL; - _queue_state *queue = NULL; - int err = _queues_lookup(queues, qid, &mutex, &queue); + // Look up the queue. + _queue *queue = NULL; + int err = _queues_lookup(queues, qid, &queue); if (err != 0) { return err; } assert(queue != NULL); - // Past this point we are responsible for releasing the mutex. // Convert the object to cross-interpreter data. _PyCrossInterpreterData *data = GLOBAL_MALLOC(_PyCrossInterpreterData); if (data == NULL) { - PyThread_release_lock(mutex); + _queue_unmark_waiter(queue, queues->mutex); return -1; } if (_PyObject_GetCrossInterpreterData(obj, data) != 0) { - PyThread_release_lock(mutex); + _queue_unmark_waiter(queue, queues->mutex); GLOBAL_FREE(data); return -1; } - // Add the data to the channel. + // Add the data to the queue. int res = _queue_add(queue, data); - PyThread_release_lock(mutex); + _queue_unmark_waiter(queue, queues->mutex); if (res != 0) { // We may chain an exception here: (void)_release_xid_data(data, 0); @@ -895,8 +869,7 @@ queue_put(_queues *queues, int64_t qid, PyObject *obj) return 0; } -// Pop the next object off the channel. Fail if empty. -// The current interpreter gets associated with the recv end of the channel. +// Pop the next object off the queue. Fail if empty. // XXX Support a "wait" mutex? static int queue_get(_queues *queues, int64_t qid, PyObject **res) @@ -904,20 +877,19 @@ queue_get(_queues *queues, int64_t qid, PyObject **res) int err; *res = NULL; - // Look up the channel. - PyThread_type_lock mutex = NULL; - _queue_state *queue = NULL; - err = _queues_lookup(queues, qid, &mutex, &queue); + // Look up the queue. + _queue *queue = NULL; + err = _queues_lookup(queues, qid, &queue); if (err != 0) { return err; } - assert(queue != NULL); // Past this point we are responsible for releasing the mutex. + assert(queue != NULL); - // Pop off the next item from the channel. + // Pop off the next item from the queue. _PyCrossInterpreterData *data = NULL; err = _queue_next(queue, &data); - PyThread_release_lock(mutex); + _queue_unmark_waiter(queue, queues->mutex); if (err != 0) { return err; } @@ -930,11 +902,11 @@ queue_get(_queues *queues, int64_t qid, PyObject **res) PyObject *obj = _PyCrossInterpreterData_NewObject(data); if (obj == NULL) { assert(PyErr_Occurred()); - // It was allocated in channel_send(), so we free it. + // It was allocated in queue_put(), so we free it. (void)_release_xid_data(data, XID_IGNORE_EXC | XID_FREE); return -1; } - // It was allocated in channel_send(), so we free it. + // It was allocated in queue_put(), so we free it. int release_res = _release_xid_data(data, XID_FREE); if (release_res < 0) { // The source interpreter has been destroyed already. @@ -948,43 +920,48 @@ queue_get(_queues *queues, int64_t qid, PyObject **res) } -/* channel info */ - static int queue_get_count(_queues *queues, int64_t qid, Py_ssize_t *p_count) { - int err = 0; + _queue *queue = NULL; + int err = _queues_lookup(queues, qid, &queue); + if (err < 0) { + return err; + } + err = _queue_get_count(queue, p_count); + _queue_unmark_waiter(queue, queues->mutex); + return err; +} - // Hold the global lock until we're done. - PyThread_acquire_lock(queues->mutex, WAIT_LOCK); - // Find the channel. - _queueref *ref = _queuerefs_find(queues->head, qid, NULL); - if (ref == NULL) { - err = ERR_QUEUE_NOT_FOUND; - goto finally; - } - _queue_state *queue = ref->queue; - assert(queue != NULL); +/* external Queue objects ***************************************************/ - // Get the number of queued objects. - assert(queue->items->count <= PY_SSIZE_T_MAX); - *p_count = (Py_ssize_t)queue->items->count; +static int _queueobj_shared(PyThreadState *, + PyObject *, _PyCrossInterpreterData *); -finally: - PyThread_release_lock(queues->mutex); - return err; -} +static int +set_external_queue_type(PyObject *module, PyTypeObject *queue_type) +{ + module_state *state = get_module_state(module); + assert(state != NULL); + if (state->queue_type != NULL) { + PyErr_SetString(PyExc_TypeError, "already registered"); + return -1; + } + state->queue_type = (PyTypeObject *)Py_NewRef(queue_type); -/* external objects *********************************************************/ + if (_PyCrossInterpreterData_RegisterClass(queue_type, _queueobj_shared) < 0) { + return -1; + } -// XXX Use a new __xid__ protocol instead? + return 0; +} static PyTypeObject * -_get_current_queue_type(void) +get_external_queue_type(PyObject *module) { - module_state *state = _get_current_module_state(); + module_state *state = get_module_state(module); assert(state != NULL); PyTypeObject *cls = state->queue_type; @@ -1005,6 +982,9 @@ _get_current_queue_type(void) return cls; } + +// XXX Use a new __xid__ protocol instead? + struct _queueid_xid { int64_t qid; }; @@ -1021,7 +1001,7 @@ _queueid_xid_new(int64_t qid) struct _queueid_xid *data = PyMem_RawMalloc(sizeof(struct _queueid_xid)); if (data == NULL) { - _queues_decref(queues, qid); + _queues_incref(queues, qid); return NULL; } data->qid = qid; @@ -1046,7 +1026,16 @@ _queueobj_from_xid(_PyCrossInterpreterData *data) return NULL; } - PyTypeObject *cls = _get_current_queue_type(); + PyObject *mod = _get_current_module(); + if (mod == NULL) { + // XXX import it? + PyErr_SetString(PyExc_RuntimeError, + MODULE_NAME " module not imported yet"); + return NULL; + } + + PyTypeObject *cls = get_external_queue_type(mod); + Py_DECREF(mod); if (cls == NULL) { Py_DECREF(qidobj); return NULL; @@ -1110,7 +1099,7 @@ _globals_init(void) assert(_globals.queues.mutex == NULL); PyThread_type_lock mutex = PyThread_allocate_lock(); if (mutex == NULL) { - return ERR_QUEUES_MUTEX_INIT; + return ERR_QUEUES_ALLOC; } _queues_init(&_globals.queues, mutex); return 0; @@ -1169,10 +1158,7 @@ queuesmod_create(PyObject *self, PyObject *Py_UNUSED(ignored)) (void)handle_queue_error(-1, self, qid); return NULL; } - module_state *state = get_module_state(self); - if (state == NULL) { - return NULL; - } + PyObject *qidobj = PyLong_FromLongLong(qid); if (qidobj == NULL) { int err = queue_destroy(&_globals.queues, qid); @@ -1181,6 +1167,7 @@ queuesmod_create(PyObject *self, PyObject *Py_UNUSED(ignored)) } return NULL; } + return qidobj; } @@ -1229,12 +1216,6 @@ queuesmod_list_all(PyObject *self, PyObject *Py_UNUSED(ignored)) if (ids == NULL) { goto finally; } - module_state *state = get_module_state(self); - if (state == NULL) { - Py_DECREF(ids); - ids = NULL; - goto finally; - } int64_t *cur = qids; for (int64_t i=0; i < count; cur++, i++) { PyObject *qidobj = PyLong_FromLongLong(*cur); @@ -1417,18 +1398,7 @@ queuesmod__register_queue_type(PyObject *self, PyObject *args, PyObject *kwds) } PyTypeObject *cls_queue = (PyTypeObject *)queuetype; - module_state *state = get_module_state(self); - if (state == NULL) { - return NULL; - } - - if (state->queue_type != NULL) { - PyErr_SetString(PyExc_TypeError, "already registered"); - return NULL; - } - state->queue_type = (PyTypeObject *)Py_NewRef(cls_queue); - - if (_PyCrossInterpreterData_RegisterClass(cls_queue, _queueobj_shared) < 0) { + if (set_external_queue_type(self, cls_queue) < 0) { return NULL; } From 611f60c64091cc0e892c62ed6f9a86d17b730fe1 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Fri, 8 Dec 2023 11:31:34 -0700 Subject: [PATCH 06/14] Fix _queues.get() default handling. --- Modules/_xxinterpqueuesmodule.c | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/Modules/_xxinterpqueuesmodule.c b/Modules/_xxinterpqueuesmodule.c index 37648604e937cf..d520de6f4a32a4 100644 --- a/Modules/_xxinterpqueuesmodule.c +++ b/Modules/_xxinterpqueuesmodule.c @@ -1276,19 +1276,13 @@ queuesmod_get(PyObject *self, PyObject *args, PyObject *kwds) PyObject *obj = NULL; int err = queue_get(&_globals.queues, qid, &obj); - if (handle_queue_error(err, self, qid)) { - return NULL; - } - Py_XINCREF(dflt); - if (obj == NULL) { - // Use the default. - if (dflt == NULL) { - (void)handle_queue_error(ERR_QUEUE_EMPTY, self, qid); - return NULL; - } + if (err == ERR_QUEUE_EMPTY && dflt != NULL) { + assert(obj == NULL); obj = Py_NewRef(dflt); } - Py_XDECREF(dflt); + else if (handle_queue_error(err, self, qid)) { + return NULL; + } return obj; } From e4d18ffdcce250cd66187be4a74b94f0ae0a2084 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 11 Dec 2023 09:49:19 -0700 Subject: [PATCH 07/14] Move most exception types to the interpreters module. --- Lib/test/support/interpreters/queues.py | 88 ++++++-- Modules/_xxinterpqueuesmodule.c | 279 ++++++++++++++++-------- 2 files changed, 257 insertions(+), 110 deletions(-) diff --git a/Lib/test/support/interpreters/queues.py b/Lib/test/support/interpreters/queues.py index e1e9c56b9de645..d3c8e8da2fe412 100644 --- a/Lib/test/support/interpreters/queues.py +++ b/Lib/test/support/interpreters/queues.py @@ -7,7 +7,7 @@ # aliases: from _xxinterpqueues import ( - QueueError, QueueNotFoundError, + QueueError, ) __all__ = [ @@ -17,6 +17,31 @@ ] +class QueueNotFoundError(QueueError): + """Raised any time a requrested queue is missing.""" + + +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. + """ + + +def _apply_subclass(exc): + if exc.errcode is _queues.ERR_QUEUE_NOT_FOUND: + exc.__class__ = QueueNotFoundError + elif exc.errcode is _queues.ERR_QUEUE_EMPTY: + exc.__class__ = QueueEmpty + + def create(maxsize=0): """Return a new cross-interpreter queue. @@ -33,19 +58,6 @@ def list_all(): for qid in _queues.list_all()] -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. - """ - _known_queues = weakref.WeakValueDictionary() @@ -78,14 +90,23 @@ def __new__(cls, id, /, *, _maxsize=None): self._id = id self._maxsize = maxsize _known_queues[id] = self - _queues.bind(id) + try: + _queues.bind(id) + except QueueError as exc: + _apply_subclass(exc) + raise # re-raise else: if _maxsize is not None: raise Exception('maxsize may not be changed') return self def __del__(self): - _queues.release(self._id) + try: + _queues.release(self._id) + except QueueError as exc: + if exc.errcode is not _queues.ERR_QUEUE_NOT_FOUND: + _apply_subclass(exc) + raise # re-raise try: del _known_queues[self._id] except KeyError: @@ -114,15 +135,27 @@ def full(self): return self.qsize() >= self._maxsize def qsize(self): - return _queues.get_count(self._id) + try: + return _queues.get_count(self._id) + except QueueError as exc: + _apply_subclass(exc) + raise # re-raise def put(self, obj, timeout=None): # XXX block if full - _queues.put(self._id, obj) + try: + _queues.put(self._id, obj) + except QueueError as exc: + _apply_subclass(exc) + raise # re-raise def put_nowait(self, obj): # XXX raise QueueFull if full - return _queues.put(self._id, obj) + try: + return _queues.put(self._id, obj) + except QueueError as exc: + _apply_subclass(exc) + raise # re-raise def get(self, timeout=None, *, _sentinel=object(), @@ -137,12 +170,17 @@ def get(self, timeout=None, *, if timeout < 0: raise ValueError(f'timeout value must be non-negative') end = time.time() + timeout - obj = _queues.get(self._id, _sentinel) - while obj is _sentinel: + while True: + try: + obj = _queues.get(self._id, _sentinel) + except QueueError as exc: + _apply_subclass(exc) + raise # re-raise + if obj is not _sentinel: + break time.sleep(_delay) if timeout is not None and time.time() >= end: raise QueueEmpty - obj = _queues.get(self._id, _sentinel) return obj def get_nowait(self, *, _sentinel=object()): @@ -151,7 +189,11 @@ def get_nowait(self, *, _sentinel=object()): If the queue is empty then raise QueueEmpty. Otherwise this is the same as get(). """ - obj = _queues.get(self._id, _sentinel) + try: + obj = _queues.get(self._id, _sentinel) + except QueueError as exc: + _apply_subclass(exc) + raise # re-raise if obj is _sentinel: raise QueueEmpty return obj diff --git a/Modules/_xxinterpqueuesmodule.c b/Modules/_xxinterpqueuesmodule.c index d520de6f4a32a4..990d3538685d48 100644 --- a/Modules/_xxinterpqueuesmodule.c +++ b/Modules/_xxinterpqueuesmodule.c @@ -77,25 +77,6 @@ _get_current_module(void) return mod; } -static PyObject * -add_new_exception(PyObject *mod, const char *name, PyObject *base) -{ - assert(!PyObject_HasAttrStringWithError(mod, name)); - PyObject *exctype = PyErr_NewException(name, base, NULL); - if (exctype == NULL) { - return NULL; - } - int res = PyModule_AddType(mod, (PyTypeObject *)exctype); - if (res < 0) { - Py_DECREF(exctype); - return NULL; - } - return exctype; -} - -#define ADD_NEW_EXCEPTION(MOD, NAME, BASE) \ - add_new_exception(MOD, MODULE_NAME "." Py_STRINGIFY(NAME), BASE) - struct idarg_int64_converter_data { // input: @@ -149,9 +130,14 @@ typedef struct { /* external types (added at runtime by interpreters module) */ PyTypeObject *queue_type; - /* exceptions */ + /* QueueError (and its error codes) */ PyObject *QueueError; - PyObject *QueueNotFoundError; + struct module_errcodes { + // Only some of the error codes are exposed by the module. + PyObject *obj_ERR_NO_NEXT_QUEUE_ID; + PyObject *obj_ERR_QUEUE_NOT_FOUND; + PyObject *obj_ERR_QUEUE_EMPTY; + } errcodes; } module_state; static inline module_state * @@ -169,9 +155,11 @@ traverse_module_state(module_state *state, visitproc visit, void *arg) /* external types */ Py_VISIT(state->queue_type); - /* exceptions */ + /* QueueError */ Py_VISIT(state->QueueError); - Py_VISIT(state->QueueNotFoundError); + Py_VISIT(state->errcodes.obj_ERR_NO_NEXT_QUEUE_ID); + Py_VISIT(state->errcodes.obj_ERR_QUEUE_NOT_FOUND); + Py_VISIT(state->errcodes.obj_ERR_QUEUE_EMPTY); return 0; } @@ -182,46 +170,183 @@ clear_module_state(module_state *state) /* external types */ Py_CLEAR(state->queue_type); - /* exceptions */ + /* QueueError */ Py_CLEAR(state->QueueError); - Py_CLEAR(state->QueueNotFoundError); + Py_CLEAR(state->errcodes.obj_ERR_NO_NEXT_QUEUE_ID); + Py_CLEAR(state->errcodes.obj_ERR_QUEUE_NOT_FOUND); + Py_CLEAR(state->errcodes.obj_ERR_QUEUE_EMPTY); return 0; } -/* queue errors *************************************************************/ +/* error codes **************************************************************/ -#define ERR_QUEUE_NOT_FOUND -2 -#define ERR_QUEUE_EMPTY -5 -#define ERR_QUEUE_ALLOC -7 -#define ERR_QUEUES_ALLOC -8 -#define ERR_NO_NEXT_QUEUE_ID -9 +#define ERR_EXCEPTION_RAISED (-1) +// multi-queue errors +#define ERR_QUEUES_ALLOC (-11) +#define ERR_QUEUE_ALLOC (-12) +#define ERR_NO_NEXT_QUEUE_ID (-13) +#define ERR_QUEUE_NOT_FOUND (-14) +// single-queue errors +#define ERR_QUEUE_EMPTY (-21) static int -exceptions_init(PyObject *mod) +_add_module_errcodes(PyObject *mod, struct module_errcodes *state) +{ +#define ADD(ERRCODE) \ + do { \ + assert(state->obj_##ERRCODE == NULL); \ + assert(!PyObject_HasAttrStringWithError(mod, #ERRCODE)); \ + PyObject *obj = PyLong_FromLong(ERRCODE); \ + if (obj == NULL) { \ + return -1; \ + } \ + state->obj_##ERRCODE = obj; \ + if (PyModule_AddObjectRef(mod, #ERRCODE, obj) < 0) { \ + return -1; \ + } \ + } while (0) + ADD(ERR_NO_NEXT_QUEUE_ID); + ADD(ERR_QUEUE_NOT_FOUND); + ADD(ERR_QUEUE_EMPTY); +#undef ADD + return 0; +} + +static PyObject * +get_module_errcode(struct module_errcodes *state, int errcode, int64_t qid, + PyObject **p_msgobj) +{ + PyObject *obj = NULL; + PyObject *msg = NULL; + switch (errcode) { +#define CASE(ERRCODE) \ + case ERRCODE: \ + obj = Py_NewRef(state->obj_##ERRCODE); + CASE(ERR_NO_NEXT_QUEUE_ID) + msg = PyUnicode_FromString("ran out of queue IDs"); + break; + CASE(ERR_QUEUE_NOT_FOUND) + msg = PyUnicode_FromFormat("queue %" PRId64 " not found", qid); + break; + CASE(ERR_QUEUE_EMPTY) + msg = PyUnicode_FromFormat("queue %" PRId64 " is empty", qid); + break; +#undef CASE + default: + PyErr_Format(PyExc_ValueError, + "unsupported error code %d", errcode); + return NULL; + } + + if (msg == NULL) { + assert(PyErr_Occurred()); + return NULL; + } + *p_msgobj = msg; + return obj; +} + + +/* QueueError ***************************************************************/ + +#define QueueError_NAME MODULE_NAME ".QueueError" +PyDoc_STRVAR(QueueError_doc, +"Indicates that a queue-related error happened.\n\ +\n\ +The \"errcode\" attribute indicates the specific error, if known.\n\ +It may be one of:\n\ +\n\ + * _queues.ERR_NO_NEXT_QUEUE_ID\n\ + * _queues.ERR_QUEUE_NOT_FOUND\n\ + * _queues.ERR_QUEUE_EMPTY\n\ + * None (error code not known)\n\ +\n\ +The \"id\" attribute identifies the targeted queue, if applicable and known.\n\ +It defaults to None.\n\ +"); + +static PyObject * +add_QueueError(PyObject *mod) { module_state *state = get_module_state(mod); - if (state == NULL) { - return -1; + assert(state->QueueError == NULL); + assert(!PyObject_HasAttrStringWithError(mod, QueueError_NAME)); + + if (_add_module_errcodes(mod, &state->errcodes) < 0) { + return NULL; } -#define ADD(NAME, BASE) \ - do { \ - assert(state->NAME == NULL); \ - state->NAME = ADD_NEW_EXCEPTION(mod, NAME, BASE); \ - if (state->NAME == NULL) { \ - return -1; \ - } \ - } while (0) + PyObject *exctype = PyErr_NewExceptionWithDoc( + QueueError_NAME, + QueueError_doc, + PyExc_RuntimeError, // base + NULL // definition namespace + ); + if (exctype == NULL) { + return NULL; + } - // A queue-related operation failed. - ADD(QueueError, PyExc_RuntimeError); - // An operation tried to use a queue that doesn't exist. - ADD(QueueNotFoundError, state->QueueError); -#undef ADD + // Set a default "errcode" attribute value. + if (PyObject_SetAttrString(exctype, "errcode", Py_None) < 0) { + Py_DECREF(exctype); + return NULL; + } - return 0; + // Set a default "id" attribute value. + if (PyObject_SetAttrString(exctype, "id", Py_None) < 0) { + Py_DECREF(exctype); + return NULL; + } + + // Add QueueError. + if (PyModule_AddType(mod, (PyTypeObject *)exctype) < 0) { + Py_DECREF(exctype); + return NULL; + } + state->QueueError = exctype; + + return exctype; +} + +static PyObject * +new_QueueError(PyObject *exctype, struct module_errcodes *errcodes, + int errcode, int64_t qid) +{ + int err = 0; + PyObject *msg_obj = NULL; + PyObject *errcode_obj = NULL; + PyObject *exc_obj = NULL; + + // We do errcode first so we can fail early for an unsupported error code. + errcode_obj = get_module_errcode(errcodes, errcode, qid, &msg_obj); + if (errcode_obj == NULL) { + goto error; + } + + exc_obj = PyObject_CallOneArg(exctype, msg_obj); + if (exc_obj == NULL) { + goto error; + } + err = PyObject_SetAttrString(exc_obj, "msg", msg_obj); + Py_CLEAR(msg_obj); + if (err < 0) { + goto error; + } + err = PyObject_SetAttrString(exc_obj, "errcode", errcode_obj); + Py_CLEAR(errcode_obj); + if (err < 0) { + goto error; + } + + return exc_obj; + +error: + Py_XDECREF(msg_obj); + Py_XDECREF(errcode_obj); + Py_XDECREF(exc_obj); + return NULL; } static int @@ -232,32 +357,23 @@ handle_queue_error(int err, PyObject *mod, int64_t qid) return 0; } assert(err < 0); - module_state *state = get_module_state(mod); - assert(state != NULL); - if (err == ERR_QUEUE_NOT_FOUND) { - PyErr_Format(state->QueueNotFoundError, - "queue %" PRId64 " not found", qid); - } - else if (err == ERR_QUEUE_EMPTY) { - // XXX - PyErr_Format(state->QueueError, - //PyErr_Format(state->QueueEmpty, - "queue %" PRId64 " is empty", qid); - } - else if (err == ERR_QUEUE_ALLOC) { - PyErr_SetString(state->QueueError, - "can't allocate memory for new queue"); - } - else if (err == ERR_QUEUES_ALLOC) { - PyErr_SetString(state->QueueError, - "can't allocate memory for queue management"); - } - else if (err == ERR_NO_NEXT_QUEUE_ID) { - PyErr_SetString(state->QueueError, - "ran out of queue IDs"); - } - else { - assert(PyErr_Occurred()); + assert((err == -1) == (PyErr_Occurred() != NULL)); + + module_state *state; + switch (err) { + case ERR_QUEUE_ALLOC: // fall through + case ERR_QUEUES_ALLOC: + PyErr_NoMemory(); + break; + default: + state = get_module_state(mod); + assert(state->QueueError != NULL); + PyObject *exctype = state->QueueError; + PyObject *exc = new_QueueError(exctype, &state->errcodes, err, qid); + if (exc != NULL) { + PyErr_SetObject(exctype, exc); + Py_DECREF(exc); + } } return 1; } @@ -598,7 +714,7 @@ _queues_next_id(_queues *queues) // needs lock int64_t qid = queues->next_id; if (qid < 0) { /* overflow */ - return -1; + return ERR_NO_NEXT_QUEUE_ID; } queues->next_id += 1; return qid; @@ -634,7 +750,6 @@ _queues_add(_queues *queues, _queue *queue) // Create a new ref. int64_t _qid = _queues_next_id(queues); if (_qid < 0) { - qid = ERR_NO_NEXT_QUEUE_ID; goto done; } _queueref *ref = GLOBAL_MALLOC(_queueref); @@ -943,7 +1058,6 @@ static int set_external_queue_type(PyObject *module, PyTypeObject *queue_type) { module_state *state = get_module_state(module); - assert(state != NULL); if (state->queue_type != NULL) { PyErr_SetString(PyExc_TypeError, "already registered"); @@ -962,7 +1076,6 @@ static PyTypeObject * get_external_queue_type(PyObject *module) { module_state *state = get_module_state(module); - assert(state != NULL); PyTypeObject *cls = state->queue_type; if (cls == NULL) { @@ -1436,13 +1549,8 @@ module_exec(PyObject *mod) return -1; } - module_state *state = get_module_state(mod); - if (state == NULL) { - goto error; - } - /* Add exception types */ - if (exceptions_init(mod) != 0) { + if (add_QueueError(mod) == NULL) { goto error; } @@ -1467,7 +1575,6 @@ static int module_traverse(PyObject *mod, visitproc visit, void *arg) { module_state *state = get_module_state(mod); - assert(state != NULL); traverse_module_state(state, visit, arg); return 0; } @@ -1476,7 +1583,6 @@ static int module_clear(PyObject *mod) { module_state *state = get_module_state(mod); - assert(state != NULL); if (state->queue_type != NULL) { (void)_PyCrossInterpreterData_UnregisterClass(state->queue_type); @@ -1491,7 +1597,6 @@ static void module_free(void *mod) { module_state *state = get_module_state(mod); - assert(state != NULL); // Now we clear the module state. clear_module_state(state); From 75c7a682b79fe2789bceabd8a7a19d255b645185 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 11 Dec 2023 11:21:36 -0700 Subject: [PATCH 08/14] Handle maxsize in the extension module. --- Lib/test/support/interpreters/queues.py | 75 +++++----- Lib/test/test_interpreters/test_queues.py | 10 +- Modules/_xxinterpqueuesmodule.c | 166 ++++++++++++++++++++-- 3 files changed, 196 insertions(+), 55 deletions(-) diff --git a/Lib/test/support/interpreters/queues.py b/Lib/test/support/interpreters/queues.py index d3c8e8da2fe412..46a007dfd9ca19 100644 --- a/Lib/test/support/interpreters/queues.py +++ b/Lib/test/support/interpreters/queues.py @@ -40,16 +40,17 @@ def _apply_subclass(exc): exc.__class__ = QueueNotFoundError elif exc.errcode is _queues.ERR_QUEUE_EMPTY: exc.__class__ = QueueEmpty + elif exc.errcode is _queues.ERR_QUEUE_FULL: + exc.__class__ = QueueFull -def create(maxsize=0): +def create(maxsize=-1): """Return a new cross-interpreter queue. The queue may be used to pass data safely between interpreters. """ - # XXX honor maxsize - qid = _queues.create() - return Queue(qid, _maxsize=maxsize) + qid = _queues.create(maxsize) + return Queue(qid) def list_all(): @@ -64,19 +65,7 @@ def list_all(): class Queue: """A cross-interpreter queue.""" - @classmethod - def _resolve_maxsize(cls, maxsize): - if maxsize is None: - maxsize = 0 - elif not isinstance(maxsize, int): - raise TypeError(f'maxsize must be an int, got {maxsize!r}') - elif maxsize < 0: - maxsize = 0 - else: - maxsize = int(maxsize) - return maxsize - - def __new__(cls, id, /, *, _maxsize=None): + def __new__(cls, id, /): # There is only one instance for any given ID. if isinstance(id, int): id = int(id) @@ -85,19 +74,14 @@ def __new__(cls, id, /, *, _maxsize=None): try: self = _known_queues[id] except KeyError: - maxsize = cls._resolve_maxsize(_maxsize) self = super().__new__(cls) self._id = id - self._maxsize = maxsize _known_queues[id] = self try: _queues.bind(id) except QueueError as exc: _apply_subclass(exc) raise # re-raise - else: - if _maxsize is not None: - raise Exception('maxsize may not be changed') return self def __del__(self): @@ -124,15 +108,17 @@ def id(self): @property def maxsize(self): - return self._maxsize + 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): - if self._maxsize <= 0: - return False - return self.qsize() >= self._maxsize + return _queues.is_full(self._id) def qsize(self): try: @@ -141,13 +127,30 @@ def qsize(self): _apply_subclass(exc) raise # re-raise - def put(self, obj, timeout=None): - # XXX block if full - try: - _queues.put(self._id, obj) - except QueueError as exc: - _apply_subclass(exc) - raise # re-raise + def put(self, obj, timeout=None, *, + _delay=10 / 1000, # 10 milliseconds + ): + """Add the object to the queue. + + This blocks while the queue is full. + """ + 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: + _queues.put(self._id, obj) + except QueueError as exc: + if exc.errcode == _queues.ERR_QUEUE_FULL: + if timeout is None or time.time() < end: + time.sleep(_delay) + continue + _apply_subclass(exc) + raise # re-raise + else: + break def put_nowait(self, obj): # XXX raise QueueFull if full @@ -158,9 +161,9 @@ def put_nowait(self, obj): raise # re-raise def get(self, timeout=None, *, - _sentinel=object(), - _delay=10 / 1000, # 10 milliseconds - ): + _sentinel=object(), + _delay=10 / 1000, # 10 milliseconds + ): """Return the next object from the queue. This blocks while the queue is empty. diff --git a/Lib/test/test_interpreters/test_queues.py b/Lib/test/test_interpreters/test_queues.py index 388ed0be063372..d35a1710d7ca06 100644 --- a/Lib/test/test_interpreters/test_queues.py +++ b/Lib/test/test_interpreters/test_queues.py @@ -25,7 +25,7 @@ class QueueTests(TestBase): def test_create(self): with self.subTest('vanilla'): queue = queues.create() - self.assertEqual(queue.maxsize, 0) + self.assertEqual(queue.maxsize, -1) with self.subTest('small maxsize'): queue = queues.create(3) @@ -40,8 +40,8 @@ def test_create(self): self.assertEqual(queue.maxsize, 0) with self.subTest('negative maxsize'): - queue = queues.create(-1) - self.assertEqual(queue.maxsize, 0) + queue = queues.create(-10) + self.assertEqual(queue.maxsize, -10) with self.subTest('bad maxsize'): with self.assertRaises(TypeError): @@ -82,8 +82,6 @@ def test_shareable(self): queue5 = queue1.get() self.assertEqual(queue5.id, qid) - # XXX check with maxsize - def test_id_type(self): queue = queues.create() self.assertIsInstance(queue.id, int) @@ -174,7 +172,6 @@ def test_put_get_main(self): self.assertEqual(actual, expected) - @unittest.expectedFailure def test_put_timeout(self): queue = queues.create(2) queue.put(None) @@ -184,7 +181,6 @@ def test_put_timeout(self): queue.get() queue.put(None) - @unittest.expectedFailure def test_put_nowait(self): queue = queues.create(2) queue.put_nowait(None) diff --git a/Modules/_xxinterpqueuesmodule.c b/Modules/_xxinterpqueuesmodule.c index 990d3538685d48..4c81ed36236851 100644 --- a/Modules/_xxinterpqueuesmodule.c +++ b/Modules/_xxinterpqueuesmodule.c @@ -137,6 +137,7 @@ typedef struct { PyObject *obj_ERR_NO_NEXT_QUEUE_ID; PyObject *obj_ERR_QUEUE_NOT_FOUND; PyObject *obj_ERR_QUEUE_EMPTY; + PyObject *obj_ERR_QUEUE_FULL; } errcodes; } module_state; @@ -160,6 +161,7 @@ traverse_module_state(module_state *state, visitproc visit, void *arg) Py_VISIT(state->errcodes.obj_ERR_NO_NEXT_QUEUE_ID); Py_VISIT(state->errcodes.obj_ERR_QUEUE_NOT_FOUND); Py_VISIT(state->errcodes.obj_ERR_QUEUE_EMPTY); + Py_VISIT(state->errcodes.obj_ERR_QUEUE_FULL); return 0; } @@ -175,6 +177,7 @@ clear_module_state(module_state *state) Py_CLEAR(state->errcodes.obj_ERR_NO_NEXT_QUEUE_ID); Py_CLEAR(state->errcodes.obj_ERR_QUEUE_NOT_FOUND); Py_CLEAR(state->errcodes.obj_ERR_QUEUE_EMPTY); + Py_CLEAR(state->errcodes.obj_ERR_QUEUE_FULL); return 0; } @@ -190,6 +193,7 @@ clear_module_state(module_state *state) #define ERR_QUEUE_NOT_FOUND (-14) // single-queue errors #define ERR_QUEUE_EMPTY (-21) +#define ERR_QUEUE_FULL (-22) static int _add_module_errcodes(PyObject *mod, struct module_errcodes *state) @@ -210,6 +214,7 @@ _add_module_errcodes(PyObject *mod, struct module_errcodes *state) ADD(ERR_NO_NEXT_QUEUE_ID); ADD(ERR_QUEUE_NOT_FOUND); ADD(ERR_QUEUE_EMPTY); + ADD(ERR_QUEUE_FULL); #undef ADD return 0; } @@ -233,6 +238,9 @@ get_module_errcode(struct module_errcodes *state, int errcode, int64_t qid, CASE(ERR_QUEUE_EMPTY) msg = PyUnicode_FromFormat("queue %" PRId64 " is empty", qid); break; + CASE(ERR_QUEUE_FULL) + msg = PyUnicode_FromFormat("queue %" PRId64 " is full", qid); + break; #undef CASE default: PyErr_Format(PyExc_ValueError, @@ -261,6 +269,7 @@ It may be one of:\n\ * _queues.ERR_NO_NEXT_QUEUE_ID\n\ * _queues.ERR_QUEUE_NOT_FOUND\n\ * _queues.ERR_QUEUE_EMPTY\n\ + * _queues.ERR_QUEUE_FULL\n\ * None (error code not known)\n\ \n\ The \"id\" attribute identifies the targeted queue, if applicable and known.\n\ @@ -453,14 +462,15 @@ typedef struct _queue { PyThread_type_lock mutex; int alive; struct _queueitems { - int64_t count; + Py_ssize_t maxsize; + Py_ssize_t count; _queueitem *first; _queueitem *last; } items; } _queue; static int -_queue_init(_queue *queue) +_queue_init(_queue *queue, Py_ssize_t maxsize) { PyThread_type_lock mutex = PyThread_allocate_lock(); if (mutex == NULL) { @@ -469,6 +479,9 @@ _queue_init(_queue *queue) *queue = (_queue){ .mutex = mutex, .alive = 1, + .items = { + .maxsize = maxsize, + }, }; return 0; } @@ -554,6 +567,15 @@ _queue_add(_queue *queue, _PyCrossInterpreterData *data) return err; } + Py_ssize_t maxsize = queue->items.maxsize; + if (maxsize <= 0) { + maxsize = PY_SSIZE_T_MAX; + } + if (queue->items.count >= maxsize) { + _queue_unlock(queue); + return ERR_QUEUE_FULL; + } + _queueitem *item = _queueitem_new(data); if (item == NULL) { _queue_unlock(queue); @@ -581,6 +603,7 @@ _queue_next(_queue *queue, _PyCrossInterpreterData **p_data) return err; } + assert(queue->items.count >= 0); _queueitem *item = queue->items.first; if (item == NULL) { _queue_unlock(queue); @@ -598,6 +621,35 @@ _queue_next(_queue *queue, _PyCrossInterpreterData **p_data) return 0; } +static int +_queue_get_maxsize(_queue *queue, Py_ssize_t *p_maxsize) +{ + int err = _queue_lock(queue); + if (err < 0) { + return err; + } + + *p_maxsize = queue->items.maxsize; + + _queue_unlock(queue); + return 0; +} + +static int +_queue_is_full(_queue *queue, int *p_is_full) +{ + int err = _queue_lock(queue); + if (err < 0) { + return err; + } + + assert(queue->items.count <= queue->items.maxsize); + *p_is_full = queue->items.count == queue->items.maxsize; + + _queue_unlock(queue); + return 0; +} + static int _queue_get_count(_queue *queue, Py_ssize_t *p_count) { @@ -606,9 +658,7 @@ _queue_get_count(_queue *queue, Py_ssize_t *p_count) return err; } - // Get the number of queued objects. - assert(queue->items.count <= PY_SSIZE_T_MAX); - *p_count = (Py_ssize_t)queue->items.count; + *p_count = queue->items.count; _queue_unlock(queue); return 0; @@ -914,13 +964,13 @@ _queue_free(_queue *queue) // Create a new queue. static int64_t -queue_create(_queues *queues) +queue_create(_queues *queues, Py_ssize_t maxsize) { _queue *queue = GLOBAL_MALLOC(_queue); if (queue == NULL) { return ERR_QUEUE_ALLOC; } - int err = _queue_init(queue); + int err = _queue_init(queue, maxsize); if (err < 0) { GLOBAL_FREE(queue); return (int64_t)err; @@ -1034,6 +1084,31 @@ queue_get(_queues *queues, int64_t qid, PyObject **res) return 0; } +static int +queue_get_maxsize(_queues *queues, int64_t qid, Py_ssize_t *p_maxsize) +{ + _queue *queue = NULL; + int err = _queues_lookup(queues, qid, &queue); + if (err < 0) { + return err; + } + err = _queue_get_maxsize(queue, p_maxsize); + _queue_unmark_waiter(queue, queues->mutex); + return err; +} + +static int +queue_is_full(_queues *queues, int64_t qid, int *p_is_full) +{ + _queue *queue = NULL; + int err = _queues_lookup(queues, qid, &queue); + if (err < 0) { + return err; + } + err = _queue_is_full(queue, p_is_full); + _queue_unmark_waiter(queue, queues->mutex); + return err; +} static int queue_get_count(_queues *queues, int64_t qid, Py_ssize_t *p_count) @@ -1264,20 +1339,30 @@ qidarg_converter(PyObject *arg, void *ptr) static PyObject * -queuesmod_create(PyObject *self, PyObject *Py_UNUSED(ignored)) +queuesmod_create(PyObject *self, PyObject *args, PyObject *kwds) { - int64_t qid = queue_create(&_globals.queues); + static char *kwlist[] = {"maxsize", NULL}; + Py_ssize_t maxsize = -1; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|n:create", kwlist, + &maxsize)) { + return NULL; + } + + int64_t qid = queue_create(&_globals.queues, maxsize); if (qid < 0) { - (void)handle_queue_error(-1, self, qid); + (void)handle_queue_error((int)qid, self, qid); return NULL; } PyObject *qidobj = PyLong_FromLongLong(qid); if (qidobj == NULL) { + PyObject *exc = PyErr_GetRaisedException(); int err = queue_destroy(&_globals.queues, qid); if (handle_queue_error(err, self, qid)) { // XXX issue a warning? + PyErr_Clear(); } + PyErr_SetRaisedException(exc); return NULL; } @@ -1463,6 +1548,59 @@ PyDoc_STRVAR(queuesmod_release_doc, Release a reference to the queue.\n\ The queue is destroyed once there are no references left."); +static PyObject * +queuesmod_get_maxsize(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"qid", NULL}; + qidarg_converter_data qidarg; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O&:get_maxsize", kwlist, + qidarg_converter, &qidarg)) { + return NULL; + } + int64_t qid = qidarg.id; + + Py_ssize_t maxsize = -1; + int err = queue_get_maxsize(&_globals.queues, qid, &maxsize); + if (handle_queue_error(err, self, qid)) { + return NULL; + } + return PyLong_FromLongLong(maxsize); +} + +PyDoc_STRVAR(queuesmod_get_maxsize_doc, +"get_maxsize(qid)\n\ +\n\ +Return the maximum number of items in the queue."); + +static PyObject * +queuesmod_is_full(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"qid", NULL}; + qidarg_converter_data qidarg; + if (!PyArg_ParseTupleAndKeywords(args, kwds, + "O&:is_full", kwlist, + qidarg_converter, &qidarg)) { + return NULL; + } + int64_t qid = qidarg.id; + + int is_full; + int err = queue_is_full(&_globals.queues, qid, &is_full); + if (handle_queue_error(err, self, qid)) { + return NULL; + } + if (is_full) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; +} + +PyDoc_STRVAR(queuesmod_is_full_doc, +"is_full(qid)\n\ +\n\ +Return true if the queue has a maxsize and has reached it."); + static PyObject * queuesmod_get_count(PyObject *self, PyObject *args, PyObject *kwds) { @@ -1513,8 +1651,8 @@ queuesmod__register_queue_type(PyObject *self, PyObject *args, PyObject *kwds) } static PyMethodDef module_functions[] = { - {"create", queuesmod_create, - METH_NOARGS, queuesmod_create_doc}, + {"create", _PyCFunction_CAST(queuesmod_create), + METH_VARARGS | METH_KEYWORDS, queuesmod_create_doc}, {"destroy", _PyCFunction_CAST(queuesmod_destroy), METH_VARARGS | METH_KEYWORDS, queuesmod_destroy_doc}, {"list_all", queuesmod_list_all, @@ -1527,6 +1665,10 @@ static PyMethodDef module_functions[] = { METH_VARARGS | METH_KEYWORDS, queuesmod_bind_doc}, {"release", _PyCFunction_CAST(queuesmod_release), METH_VARARGS | METH_KEYWORDS, queuesmod_release_doc}, + {"get_maxsize", _PyCFunction_CAST(queuesmod_get_maxsize), + METH_VARARGS | METH_KEYWORDS, queuesmod_get_maxsize_doc}, + {"is_full", _PyCFunction_CAST(queuesmod_is_full), + METH_VARARGS | METH_KEYWORDS, queuesmod_is_full_doc}, {"get_count", _PyCFunction_CAST(queuesmod_get_count), METH_VARARGS | METH_KEYWORDS, queuesmod_get_count_doc}, {"_register_queue_type", _PyCFunction_CAST(queuesmod__register_queue_type), From 8bb4290af564b4c5a5f9c43412b6558c9b3f8d1e Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 11 Dec 2023 13:23:47 -0700 Subject: [PATCH 09/14] Move the exception types to the extension module. --- Lib/test/support/interpreters/queues.py | 91 ++-------- Modules/_xxinterpqueuesmodule.c | 212 ++++++++---------------- 2 files changed, 83 insertions(+), 220 deletions(-) diff --git a/Lib/test/support/interpreters/queues.py b/Lib/test/support/interpreters/queues.py index 46a007dfd9ca19..2b9b36f5c80b59 100644 --- a/Lib/test/support/interpreters/queues.py +++ b/Lib/test/support/interpreters/queues.py @@ -7,7 +7,7 @@ # aliases: from _xxinterpqueues import ( - QueueError, + QueueError, QueueNotFoundError, QueueFull, QueueEmpty, ) __all__ = [ @@ -17,33 +17,6 @@ ] -class QueueNotFoundError(QueueError): - """Raised any time a requrested queue is missing.""" - - -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. - """ - - -def _apply_subclass(exc): - if exc.errcode is _queues.ERR_QUEUE_NOT_FOUND: - exc.__class__ = QueueNotFoundError - elif exc.errcode is _queues.ERR_QUEUE_EMPTY: - exc.__class__ = QueueEmpty - elif exc.errcode is _queues.ERR_QUEUE_FULL: - exc.__class__ = QueueFull - - def create(maxsize=-1): """Return a new cross-interpreter queue. @@ -77,20 +50,14 @@ def __new__(cls, id, /): self = super().__new__(cls) self._id = id _known_queues[id] = self - try: - _queues.bind(id) - except QueueError as exc: - _apply_subclass(exc) - raise # re-raise + _queues.bind(id) return self def __del__(self): try: _queues.release(self._id) - except QueueError as exc: - if exc.errcode is not _queues.ERR_QUEUE_NOT_FOUND: - _apply_subclass(exc) - raise # re-raise + except QueueNotFoundError: + pass try: del _known_queues[self._id] except KeyError: @@ -121,11 +88,7 @@ def full(self): return _queues.is_full(self._id) def qsize(self): - try: - return _queues.get_count(self._id) - except QueueError as exc: - _apply_subclass(exc) - raise # re-raise + return _queues.get_count(self._id) def put(self, obj, timeout=None, *, _delay=10 / 1000, # 10 milliseconds @@ -142,26 +105,17 @@ def put(self, obj, timeout=None, *, while True: try: _queues.put(self._id, obj) - except QueueError as exc: - if exc.errcode == _queues.ERR_QUEUE_FULL: - if timeout is None or time.time() < end: - time.sleep(_delay) - continue - _apply_subclass(exc) - raise # re-raise + except QueueFull: + if timeout is not None and time.time() >= end: + raise # re-raise + time.sleep(_delay) else: break def put_nowait(self, obj): - # XXX raise QueueFull if full - try: - return _queues.put(self._id, obj) - except QueueError as exc: - _apply_subclass(exc) - raise # re-raise + return _queues.put(self._id, obj) def get(self, timeout=None, *, - _sentinel=object(), _delay=10 / 1000, # 10 milliseconds ): """Return the next object from the queue. @@ -175,31 +129,20 @@ def get(self, timeout=None, *, end = time.time() + timeout while True: try: - obj = _queues.get(self._id, _sentinel) - except QueueError as exc: - _apply_subclass(exc) - raise # re-raise - if obj is not _sentinel: - break - time.sleep(_delay) - if timeout is not None and time.time() >= end: - raise QueueEmpty + return _queues.get(self._id) + except QueueEmpty: + if timeout is not None and time.time() >= end: + raise # re-raise + time.sleep(_delay) return obj - def get_nowait(self, *, _sentinel=object()): + 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 = _queues.get(self._id, _sentinel) - except QueueError as exc: - _apply_subclass(exc) - raise # re-raise - if obj is _sentinel: - raise QueueEmpty - return obj + return _queues.get(self._id) _queues._register_queue_type(Queue) diff --git a/Modules/_xxinterpqueuesmodule.c b/Modules/_xxinterpqueuesmodule.c index 4c81ed36236851..2cc3a2ac5dc85f 100644 --- a/Modules/_xxinterpqueuesmodule.c +++ b/Modules/_xxinterpqueuesmodule.c @@ -130,15 +130,11 @@ typedef struct { /* external types (added at runtime by interpreters module) */ PyTypeObject *queue_type; - /* QueueError (and its error codes) */ + /* QueueError (and its subclasses) */ PyObject *QueueError; - struct module_errcodes { - // Only some of the error codes are exposed by the module. - PyObject *obj_ERR_NO_NEXT_QUEUE_ID; - PyObject *obj_ERR_QUEUE_NOT_FOUND; - PyObject *obj_ERR_QUEUE_EMPTY; - PyObject *obj_ERR_QUEUE_FULL; - } errcodes; + PyObject *QueueNotFoundError; + PyObject *QueueEmpty; + PyObject *QueueFull; } module_state; static inline module_state * @@ -158,10 +154,9 @@ traverse_module_state(module_state *state, visitproc visit, void *arg) /* QueueError */ Py_VISIT(state->QueueError); - Py_VISIT(state->errcodes.obj_ERR_NO_NEXT_QUEUE_ID); - Py_VISIT(state->errcodes.obj_ERR_QUEUE_NOT_FOUND); - Py_VISIT(state->errcodes.obj_ERR_QUEUE_EMPTY); - Py_VISIT(state->errcodes.obj_ERR_QUEUE_FULL); + Py_VISIT(state->QueueNotFoundError); + Py_VISIT(state->QueueEmpty); + Py_VISIT(state->QueueFull); return 0; } @@ -174,10 +169,9 @@ clear_module_state(module_state *state) /* QueueError */ Py_CLEAR(state->QueueError); - Py_CLEAR(state->errcodes.obj_ERR_NO_NEXT_QUEUE_ID); - Py_CLEAR(state->errcodes.obj_ERR_QUEUE_NOT_FOUND); - Py_CLEAR(state->errcodes.obj_ERR_QUEUE_EMPTY); - Py_CLEAR(state->errcodes.obj_ERR_QUEUE_FULL); + Py_CLEAR(state->QueueNotFoundError); + Py_CLEAR(state->QueueEmpty); + Py_CLEAR(state->QueueFull); return 0; } @@ -196,166 +190,86 @@ clear_module_state(module_state *state) #define ERR_QUEUE_FULL (-22) static int -_add_module_errcodes(PyObject *mod, struct module_errcodes *state) -{ -#define ADD(ERRCODE) \ - do { \ - assert(state->obj_##ERRCODE == NULL); \ - assert(!PyObject_HasAttrStringWithError(mod, #ERRCODE)); \ - PyObject *obj = PyLong_FromLong(ERRCODE); \ - if (obj == NULL) { \ - return -1; \ - } \ - state->obj_##ERRCODE = obj; \ - if (PyModule_AddObjectRef(mod, #ERRCODE, obj) < 0) { \ - return -1; \ - } \ - } while (0) - ADD(ERR_NO_NEXT_QUEUE_ID); - ADD(ERR_QUEUE_NOT_FOUND); - ADD(ERR_QUEUE_EMPTY); - ADD(ERR_QUEUE_FULL); -#undef ADD - return 0; -} - -static PyObject * -get_module_errcode(struct module_errcodes *state, int errcode, int64_t qid, - PyObject **p_msgobj) +resolve_module_errcode(module_state *state, int errcode, int64_t qid, + PyObject **p_exctype, PyObject **p_msgobj) { - PyObject *obj = NULL; + PyObject *exctype = NULL; PyObject *msg = NULL; switch (errcode) { -#define CASE(ERRCODE) \ - case ERRCODE: \ - obj = Py_NewRef(state->obj_##ERRCODE); - CASE(ERR_NO_NEXT_QUEUE_ID) + case ERR_NO_NEXT_QUEUE_ID: + exctype = state->QueueError; msg = PyUnicode_FromString("ran out of queue IDs"); break; - CASE(ERR_QUEUE_NOT_FOUND) + case ERR_QUEUE_NOT_FOUND: + exctype = state->QueueNotFoundError; msg = PyUnicode_FromFormat("queue %" PRId64 " not found", qid); break; - CASE(ERR_QUEUE_EMPTY) + case ERR_QUEUE_EMPTY: + exctype = state->QueueEmpty; msg = PyUnicode_FromFormat("queue %" PRId64 " is empty", qid); break; - CASE(ERR_QUEUE_FULL) + case ERR_QUEUE_FULL: + exctype = state->QueueFull; msg = PyUnicode_FromFormat("queue %" PRId64 " is full", qid); break; -#undef CASE default: PyErr_Format(PyExc_ValueError, "unsupported error code %d", errcode); - return NULL; + return -1; } if (msg == NULL) { assert(PyErr_Occurred()); - return NULL; + return -1; } + *p_exctype = exctype; *p_msgobj = msg; - return obj; + return 0; } /* QueueError ***************************************************************/ -#define QueueError_NAME MODULE_NAME ".QueueError" -PyDoc_STRVAR(QueueError_doc, -"Indicates that a queue-related error happened.\n\ -\n\ -The \"errcode\" attribute indicates the specific error, if known.\n\ -It may be one of:\n\ -\n\ - * _queues.ERR_NO_NEXT_QUEUE_ID\n\ - * _queues.ERR_QUEUE_NOT_FOUND\n\ - * _queues.ERR_QUEUE_EMPTY\n\ - * _queues.ERR_QUEUE_FULL\n\ - * None (error code not known)\n\ -\n\ -The \"id\" attribute identifies the targeted queue, if applicable and known.\n\ -It defaults to None.\n\ -"); - -static PyObject * -add_QueueError(PyObject *mod) -{ - module_state *state = get_module_state(mod); - assert(state->QueueError == NULL); - assert(!PyObject_HasAttrStringWithError(mod, QueueError_NAME)); - - if (_add_module_errcodes(mod, &state->errcodes) < 0) { - return NULL; - } - - PyObject *exctype = PyErr_NewExceptionWithDoc( - QueueError_NAME, - QueueError_doc, - PyExc_RuntimeError, // base - NULL // definition namespace - ); +static int +add_exctype(PyObject *mod, PyObject **p_state_field, + const char *qualname, const char *doc, PyObject *base) +{ + const char *dot = strrchr(qualname, '.'); + assert(dot != NULL); + const char *name = dot+1; + assert(*p_state_field == NULL); + assert(!PyObject_HasAttrStringWithError(mod, name)); + PyObject *exctype = PyErr_NewExceptionWithDoc(qualname, doc, base, NULL); if (exctype == NULL) { - return NULL; - } - - // Set a default "errcode" attribute value. - if (PyObject_SetAttrString(exctype, "errcode", Py_None) < 0) { - Py_DECREF(exctype); - return NULL; - } - - // Set a default "id" attribute value. - if (PyObject_SetAttrString(exctype, "id", Py_None) < 0) { - Py_DECREF(exctype); - return NULL; + return -1; } - - // Add QueueError. if (PyModule_AddType(mod, (PyTypeObject *)exctype) < 0) { Py_DECREF(exctype); - return NULL; + return -1; } - state->QueueError = exctype; - - return exctype; + *p_state_field = exctype; + return 0; } -static PyObject * -new_QueueError(PyObject *exctype, struct module_errcodes *errcodes, - int errcode, int64_t qid) +static int +add_QueueError(PyObject *mod) { - int err = 0; - PyObject *msg_obj = NULL; - PyObject *errcode_obj = NULL; - PyObject *exc_obj = NULL; - - // We do errcode first so we can fail early for an unsupported error code. - errcode_obj = get_module_errcode(errcodes, errcode, qid, &msg_obj); - if (errcode_obj == NULL) { - goto error; - } + module_state *state = get_module_state(mod); - exc_obj = PyObject_CallOneArg(exctype, msg_obj); - if (exc_obj == NULL) { - goto error; - } - err = PyObject_SetAttrString(exc_obj, "msg", msg_obj); - Py_CLEAR(msg_obj); - if (err < 0) { - goto error; +#define PREFIX "test.support.interpreters." +#define ADD_EXCTYPE(NAME, BASE, DOC) \ + if (add_exctype(mod, &state->NAME, PREFIX #NAME, DOC, BASE) < 0) { \ + return -1; \ } - err = PyObject_SetAttrString(exc_obj, "errcode", errcode_obj); - Py_CLEAR(errcode_obj); - if (err < 0) { - goto error; - } - - return exc_obj; + ADD_EXCTYPE(QueueError, PyExc_RuntimeError, + "Indicates that a queue-related error happened.") + ADD_EXCTYPE(QueueNotFoundError, state->QueueError, NULL) + ADD_EXCTYPE(QueueEmpty, state->QueueError, NULL) + ADD_EXCTYPE(QueueFull, state->QueueError, NULL) +#undef ADD_EXCTYPE +#undef PREFIX -error: - Py_XDECREF(msg_obj); - Py_XDECREF(errcode_obj); - Py_XDECREF(exc_obj); - return NULL; + return 0; } static int @@ -377,12 +291,18 @@ handle_queue_error(int err, PyObject *mod, int64_t qid) default: state = get_module_state(mod); assert(state->QueueError != NULL); - PyObject *exctype = state->QueueError; - PyObject *exc = new_QueueError(exctype, &state->errcodes, err, qid); - if (exc != NULL) { - PyErr_SetObject(exctype, exc); - Py_DECREF(exc); + PyObject *exctype = NULL; + PyObject *msg = NULL; + if (resolve_module_errcode(state, err, qid, &exctype, &msg) < 0) { + return -1; + } + PyObject *exc = PyObject_CallOneArg(exctype, msg); + Py_DECREF(msg); + if (exc == NULL) { + return -1; } + PyErr_SetObject(exctype, exc); + Py_DECREF(exc); } return 1; } @@ -1692,7 +1612,7 @@ module_exec(PyObject *mod) } /* Add exception types */ - if (add_QueueError(mod) == NULL) { + if (add_QueueError(mod) < 0) { goto error; } From e65c8b80ff0ff9c7143a9103680f96838b7b03b5 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 11 Dec 2023 13:35:15 -0700 Subject: [PATCH 10/14] QueueFull and QueueEmpty inherit from queue.Full and queue.Empty. --- Lib/test/support/interpreters/queues.py | 34 +++++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/Lib/test/support/interpreters/queues.py b/Lib/test/support/interpreters/queues.py index 2b9b36f5c80b59..89d5dc96031d10 100644 --- a/Lib/test/support/interpreters/queues.py +++ b/Lib/test/support/interpreters/queues.py @@ -7,7 +7,7 @@ # aliases: from _xxinterpqueues import ( - QueueError, QueueNotFoundError, QueueFull, QueueEmpty, + QueueError, QueueNotFoundError, ) __all__ = [ @@ -17,6 +17,20 @@ ] +class QueueEmpty(_queues.QueueEmpty, queue.Empty): + """Raised from get_nowait() when the queue is empty. + + It is also raised from get() if it times out. + """ + + +class QueueFull(_queues.QueueFull, queue.Full): + """Raised from put_nowait() when the queue is full. + + It is also raised from put() if it times out. + """ + + def create(maxsize=-1): """Return a new cross-interpreter queue. @@ -105,15 +119,20 @@ def put(self, obj, timeout=None, *, while True: try: _queues.put(self._id, obj) - except QueueFull: + except _queues.QueueFull as exc: if timeout is not None and time.time() >= end: + exc.__class__ = QueueFull raise # re-raise time.sleep(_delay) else: break def put_nowait(self, obj): - return _queues.put(self._id, obj) + try: + return _queues.put(self._id, obj) + except _queues.QueueFull as exc: + exc.__class__ = QueueFull + raise # re-raise def get(self, timeout=None, *, _delay=10 / 1000, # 10 milliseconds @@ -130,8 +149,9 @@ def get(self, timeout=None, *, while True: try: return _queues.get(self._id) - except QueueEmpty: + except _queues.QueueEmpty as exc: if timeout is not None and time.time() >= end: + exc.__class__ = QueueEmpty raise # re-raise time.sleep(_delay) return obj @@ -142,7 +162,11 @@ def get_nowait(self): If the queue is empty then raise QueueEmpty. Otherwise this is the same as get(). """ - return _queues.get(self._id) + try: + return _queues.get(self._id) + except _queues.QueueEmpty as exc: + exc.__class__ = QueueEmpty + raise # re-raise _queues._register_queue_type(Queue) From 313643a16e702c89ec2a7f1ba524f3133bcfd590 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 11 Dec 2023 13:39:49 -0700 Subject: [PATCH 11/14] maxsize defaults to 0. --- Lib/test/support/interpreters/queues.py | 2 +- Lib/test/test_interpreters/test_queues.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/support/interpreters/queues.py b/Lib/test/support/interpreters/queues.py index 89d5dc96031d10..aead0c40ca9667 100644 --- a/Lib/test/support/interpreters/queues.py +++ b/Lib/test/support/interpreters/queues.py @@ -31,7 +31,7 @@ class QueueFull(_queues.QueueFull, queue.Full): """ -def create(maxsize=-1): +def create(maxsize=0): """Return a new cross-interpreter queue. The queue may be used to pass data safely between interpreters. diff --git a/Lib/test/test_interpreters/test_queues.py b/Lib/test/test_interpreters/test_queues.py index d35a1710d7ca06..2a8ca99c1f6e3f 100644 --- a/Lib/test/test_interpreters/test_queues.py +++ b/Lib/test/test_interpreters/test_queues.py @@ -25,7 +25,7 @@ class QueueTests(TestBase): def test_create(self): with self.subTest('vanilla'): queue = queues.create() - self.assertEqual(queue.maxsize, -1) + self.assertEqual(queue.maxsize, 0) with self.subTest('small maxsize'): queue = queues.create(3) From e756b9b3508e1f919a512ed52c52861c1e67f4be Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 12 Dec 2023 08:42:17 -0700 Subject: [PATCH 12/14] Ignore _globals var in _xxinterpqueuesmodule.c. --- Tools/c-analyzer/cpython/ignored.tsv | 1 + 1 file changed, 1 insertion(+) diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index ff6e1ef4f993ba..2f9e80d6ab6737 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -165,6 +165,7 @@ Python/pylifecycle.c fatal_error reentrant - # explicitly protected, internal-only Modules/_xxinterpchannelsmodule.c - _globals - +Modules/_xxinterpqueuesmodule.c - _globals - # set once during module init Modules/_decimal/_decimal.c - minalloc_is_set - From 134acb3c9debce6ce4c913deaaa1ce406923399c Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 12 Dec 2023 09:09:08 -0700 Subject: [PATCH 13/14] Fix configure. --- configure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure b/configure index b5dcad96f35414..0d7493f807f701 100755 --- a/configure +++ b/configure @@ -30786,7 +30786,7 @@ if test -z "${MODULE__XXINTERPCHANNELS_TRUE}" && test -z "${MODULE__XXINTERPCHAN Usually this means the macro was only invoked conditionally." "$LINENO" 5 fi if test -z "${MODULE__XXINTERPQUEUES_TRUE}" && test -z "${MODULE__XXINTERPQUEUES_FALSE}"; then - as_fn_error $? "conditional \"MODULE__XXINTERPQUEUESS\" was never defined. + as_fn_error $? "conditional \"MODULE__XXINTERPQUEUES\" was never defined. Usually this means the macro was only invoked conditionally." "$LINENO" 5 fi if test -z "${MODULE__ZONEINFO_TRUE}" && test -z "${MODULE__ZONEINFO_FALSE}"; then From 0da1a11570af97c9ad7007a4a22137ce41d39813 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 12 Dec 2023 09:44:46 -0700 Subject: [PATCH 14/14] Fix configure. --- configure | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configure b/configure index 0d7493f807f701..668a0efd77db0e 100755 --- a/configure +++ b/configure @@ -769,10 +769,10 @@ MODULE__MULTIPROCESSING_FALSE MODULE__MULTIPROCESSING_TRUE MODULE__ZONEINFO_FALSE MODULE__ZONEINFO_TRUE -MODULE__XXINTERPCHANNELS_FALSE -MODULE__XXINTERPCHANNELS_TRUE MODULE__XXINTERPQUEUES_FALSE MODULE__XXINTERPQUEUES_TRUE +MODULE__XXINTERPCHANNELS_FALSE +MODULE__XXINTERPCHANNELS_TRUE MODULE__XXSUBINTERPRETERS_FALSE MODULE__XXSUBINTERPRETERS_TRUE MODULE__TYPING_FALSE pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy