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