Skip to content

Commit 7f98bf6

Browse files
author
Anselm Kruis
committed
merge 3.4-slp (Stackless python#93)
2 parents 3c981be + 34700b8 commit 7f98bf6

File tree

4 files changed

+329
-49
lines changed

4 files changed

+329
-49
lines changed

Doc/library/stackless/tasklets.rst

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,14 @@ The ``tasklet`` class
267267
<slp-exc-section>`, and the purpose of the :ref:`TaskletExit <slp-exc>`
268268
exception.
269269

270+
If you try to raise an exception on a tasklet, that is not alive, the method
271+
fails, except if *exc_class* is :exc:`TaskletExit` and the tasklet already ended.
272+
273+
.. versionchanged:: 3.3.7
274+
275+
In case of an error Stackless versions before 3.3.7 raise ``exc_class(*args)``.
276+
Later versions raises :exc:`RuntimeError`.
277+
270278
.. method:: tasklet.throw(exc=None, val=None, tb=None, pending=False)
271279

272280
Raise an exception on the given tasklet. The semantics are similar
@@ -277,14 +285,22 @@ The ``tasklet`` class
277285
runnable and the caller continues. Otherwise, the target will be inserted
278286
before the current tasklet in the queue and switched to immediately.
279287

288+
If you try to raise an exception on a tasklet, that is not alive, the method
289+
raises :exc:`RuntimeError` on the caller. There is one exception:
290+
you can safely raise :exc:`TaskletExit`, if the tasklet already ended.
291+
280292
.. method:: tasklet.kill(pending=False)
281293

282-
Raises the :ref:`TaskletExit <slp-exc>` exception on the tasklet.
283-
*pending* has the same meaning as for :meth:`tasklet.throw`.
294+
Terminates the tasklet and unblocks it, if the tasklet was blocked
295+
on a channel. If the tasklet already ran to its end, the method does
296+
nothing. If the tasklet has no thread, the method simply ends the
297+
tasklet. Otherwise it raises the :ref:`TaskletExit <slp-exc>` exception
298+
on the tasklet. *pending* has the same meaning as for :meth:`tasklet.throw`.
284299

285300
This can be considered to be shorthand for::
286301

287-
>>> t.throw(TaskletExit, pending=pending)
302+
>>> if t.alive:
303+
>>> t.throw(TaskletExit, pending=pending)
288304

289305
.. method:: tasklet.set_atomic(flag)
290306

Stackless/changelog.txt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,26 @@ What's New in Stackless 3.X.X?
1010

1111
*Release date: 20XX-XX-XX*
1212

13+
- https://bitbucket.org/stackless-dev/stackless/issues/93
14+
Unify tasklet.kill(), tasklet.throw() and tasklet.raise_exception().
15+
They now behave almost identically. This affects the error handling in some
16+
corner cases:
17+
+ Some crash bugs have been fixed. Stackless Python now raises RuntimeError
18+
instead of crashing.
19+
+ It is not possible to raise an exception in a dead (== not alive) tasklet.
20+
In general, if you try it, the method call fails with an appropriate
21+
RuntimeError. However there are exceptions:
22+
+ For Stackless versions before 3.3.7
23+
tasklet.raise_exception(exc_class, *args) raises exc_class(*args) instead
24+
of RuntimeError.
25+
+ If you call tasklet.kill() or tasklet.throw(TaskletExit) or
26+
tasklet.raise_exception(TaskletExit) and if the tasklet already ran to its
27+
end (tasklet.frame is None), the call succeeds.
28+
+ If you call tasklet.kill() on a tasklet, that didn't run to its end
29+
(tasklet.frame is not None), but has no thread (tasklet.thread_id == -1),
30+
Stackless drops the frame and proceeds as if the tasklet ran to its end.
31+
Most users won't notice the difference.
32+
1333
- https://bitbucket.org/stackless-dev/stackless/issues/89
1434
- https://bitbucket.org/stackless-dev/stackless/issues/81
1535
Fix (crash) bugs during thread and interpreter shutdown.
@@ -325,7 +345,7 @@ The very beginning of Stackless
325345
It turns out to make sense:
326346
Try to let the innermost interpreter resolve some recusrions.
327347
<<<<<<< local
328-
Find an equivalent of olde Py_UnwindToken. Probably the
348+
Find an equivalent of olde Py_UnwindToken. Probably the
329349
=======
330350
Find an equivalent of old Py_UnwindToken. Probably the
331351
>>>>>>> other

Stackless/module/taskletobject.c

Lines changed: 156 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,39 +1077,114 @@ PyTasklet_Throw_M(PyTaskletObject *self, int pending, PyObject *exc,
10771077
}
10781078

10791079
static PyObject *
1080-
impl_tasklet_throw(PyTaskletObject *self, int pending, PyObject *exc, PyObject *val, PyObject *tb)
1080+
#if PY_VERSION_HEX < 0x03030700
1081+
#define SLP_IMPL_THROW_BOMB_WITH_BOE 1
1082+
_impl_tasklet_throw_bomb(PyTaskletObject *self, int pending, PyObject *bomb, int bomb_on_error)
1083+
#else
1084+
#undef SLP_IMPL_THROW_BOMB_WITH_BOE
1085+
_impl_tasklet_throw_bomb(PyTaskletObject *self, int pending, PyObject *bomb)
1086+
#endif
10811087
{
10821088
STACKLESS_GETARG();
10831089
PyThreadState *ts = PyThreadState_GET();
1084-
PyObject *ret, *bomb, *tmpval;
1090+
PyObject *ret, *tmpval;
10851091
int fail;
10861092

1087-
if (ts->st.main == NULL)
1088-
return PyTasklet_Throw_M(self, pending, exc, val, tb);
1089-
1090-
bomb = slp_exc_to_bomb(exc, val, tb);
1091-
if (bomb == NULL)
1092-
return NULL;
1093-
1093+
assert(bomb != NULL);
1094+
assert(PyBomb_Check(bomb));
10941095
/* raise it directly if target is ourselves. delayed exception makes
10951096
* no sense in this case
10961097
*/
1097-
if (ts->st.current == self)
1098+
if (ts->st.current == self) {
1099+
assert(self->cstate->tstate == ts);
10981100
return slp_bomb_explode(bomb);
1101+
}
10991102

1100-
/* don't attempt to send to a dead tasklet.
1101-
* f.frame is null for the running tasklet and a dead tasklet
1102-
* A new tasklet has a CFrame
1103+
/* Handle new or dead tasklets.
11031104
*/
1104-
if (self->cstate->tstate == NULL || (self->f.frame == NULL && self != self->cstate->tstate->st.current)) {
1105-
/* however, allow tasklet exit errors for already dead tasklets */
1106-
if (PyObject_IsSubclass(((PyBombObject*)bomb)->curexc_type, PyExc_TaskletExit)) {
1105+
if (slp_get_frame(self) == NULL) {
1106+
/* The tasklet is not alive.
1107+
* There are a few special cases:
1108+
* - The purpose of raising exception TaskletExit is to end the tasklet. Therefore
1109+
* it is no error, if the tasklet already run to its end.
1110+
* - Otherwise we have to raise a RuntimeError.
1111+
*/
1112+
if (!PyObject_IsSubclass(((PyBombObject*)bomb)->curexc_type, PyExc_TaskletExit) ||
1113+
(self->cstate->tstate == NULL && self->f.frame != NULL)) {
1114+
/* Error: the exception is not TaskletExit or the tasklet did not run to its end. */
1115+
#ifdef SLP_IMPL_THROW_BOMB_WITH_BOE
1116+
if (bomb_on_error)
1117+
return slp_bomb_explode(bomb);
1118+
#endif
11071119
Py_DECREF(bomb);
1108-
Py_RETURN_NONE;
1120+
if (self->cstate->tstate == NULL) {
1121+
RUNTIME_ERROR("tasklet has no thread", NULL);
1122+
}
1123+
RUNTIME_ERROR("You cannot throw to a dead tasklet", NULL);
1124+
}
1125+
/* A TaskletExit exception. The tasklet already ended (== can't be
1126+
* resurrected by bind_thread).
1127+
* Simply end the tasklet.
1128+
*/
1129+
/* next two if()... just for test coverage mesurement */
1130+
if (self->cstate->tstate != NULL) {
1131+
assert(self->cstate->tstate != NULL);
1132+
}
1133+
if (self->f.frame == NULL) {
1134+
assert(self->f.frame == NULL);
1135+
}
1136+
Py_DECREF(bomb);
1137+
1138+
/* Now obey the post conditions of tasklet.throw:
1139+
* 1. the tasklet is not blocked
1140+
*/
1141+
if (self->next && self->flags.blocked) {
1142+
/* we claim the channel's reference */
1143+
slp_channel_remove_slow(self, NULL, NULL, NULL);
1144+
} else {
1145+
Py_INCREF(self);
11091146
}
1147+
/* Obey the post conditions of throw():
1148+
* 2. the tasklet is not scheduled. This is also a precondition,
1149+
* because a dead tasklet must not be scheduled.
1150+
*/
1151+
1152+
#if 0 /* disabled until https://bitbucket.org/stackless-dev/stackless/issues/81 is resolved */
1153+
assert(self->next == NULL && self->prev == NULL);
1154+
#endif
1155+
1156+
/* Due to bugs the above assertion my not hold.
1157+
* Try to work around.
1158+
*/
1159+
if (self->next) {
1160+
if (self->cstate->tstate != NULL) {
1161+
/* The tasklet has a tstate an is scheduled.
1162+
* we can use the regular remove.
1163+
*/
1164+
slp_current_remove_tasklet(self);
1165+
} else {
1166+
/* The tasklet has no tstate, is not blocked on a channel.
1167+
* This happens, if a thread ended, but the tasklet was
1168+
* survived killing.
1169+
*/
1170+
SLP_HEADCHAIN_REMOVE(self, prev, next);
1171+
}
1172+
Py_DECREF(self);
1173+
}
1174+
Py_DECREF(self); /* the ref from the channel */
1175+
Py_RETURN_NONE;
1176+
}
1177+
assert(self->cstate->tstate != NULL);
1178+
/* don't modify a tasklet on an uninitialised or dead thread */
1179+
if (pending && self->cstate->tstate->st.main == NULL) {
1180+
#ifdef SLP_IMPL_THROW_BOMB_WITH_BOE
1181+
if (bomb_on_error)
1182+
return slp_bomb_explode(bomb);
1183+
#endif
11101184
Py_DECREF(bomb);
1111-
RUNTIME_ERROR("You cannot throw to a dead tasklet", NULL);
1185+
RUNTIME_ERROR("Target thread isn't initialised", NULL);
11121186
}
1187+
11131188
TASKLET_CLAIMVAL(self, &tmpval);
11141189
TASKLET_SETVAL_OWN(self, bomb);
11151190
if (!pending) {
@@ -1141,6 +1216,30 @@ impl_tasklet_throw(PyTaskletObject *self, int pending, PyObject *exc, PyObject *
11411216
return ret;
11421217
}
11431218

1219+
static PyObject *
1220+
impl_tasklet_throw(PyTaskletObject *self, int pending, PyObject *exc, PyObject *val, PyObject *tb)
1221+
{
1222+
STACKLESS_GETARG();
1223+
PyThreadState *ts = PyThreadState_GET();
1224+
PyObject *ret, *bomb;
1225+
1226+
if (ts->st.main == NULL)
1227+
return PyTasklet_Throw_M(self, pending, exc, val, tb);
1228+
1229+
bomb = slp_exc_to_bomb(exc, val, tb);
1230+
if (bomb == NULL)
1231+
return NULL;
1232+
1233+
STACKLESS_PROMOTE_ALL();
1234+
#ifdef SLP_IMPL_THROW_BOMB_WITH_BOE
1235+
ret = _impl_tasklet_throw_bomb(self, pending, bomb, 0);
1236+
#else
1237+
ret = _impl_tasklet_throw_bomb(self, pending, bomb);
1238+
#endif
1239+
STACKLESS_ASSERT();
1240+
return ret;
1241+
}
1242+
11441243
int PyTasklet_Throw(PyTaskletObject *self, int pending, PyObject *exc,
11451244
PyObject *val, PyObject *tb)
11461245
{
@@ -1191,27 +1290,22 @@ impl_tasklet_raise_exception(PyTaskletObject *self, PyObject *klass, PyObject *a
11911290
{
11921291
STACKLESS_GETARG();
11931292
PyThreadState *ts = PyThreadState_GET();
1194-
PyObject *ret, *bomb, *tmpval;
1195-
int fail;
1293+
PyObject *ret, *bomb;
11961294

11971295
if (ts->st.main == NULL)
11981296
return PyTasklet_RaiseException_M(self, klass, args);
11991297
bomb = slp_make_bomb(klass, args, "tasklet.raise_exception");
12001298
if (bomb == NULL)
12011299
return NULL;
1202-
if (ts->st.current == self)
1203-
return slp_bomb_explode(bomb);
1204-
/* if the tasklet is dead, do not run it (no frame) but explode */
1205-
if (slp_get_frame(self) == NULL)
1206-
return slp_bomb_explode(bomb);
12071300

1208-
TASKLET_CLAIMVAL(self, &tmpval);
1209-
TASKLET_SETVAL_OWN(self, bomb);
1210-
fail = slp_schedule_task(&ret, ts->st.current, self, stackless, 0);
1211-
if (fail)
1212-
TASKLET_SETVAL_OWN(self, tmpval);
1213-
else
1214-
Py_DECREF(tmpval);
1301+
STACKLESS_PROMOTE_ALL();
1302+
#ifdef SLP_IMPL_THROW_BOMB_WITH_BOE
1303+
ret = _impl_tasklet_throw_bomb(self, 0, bomb, 1);
1304+
#else
1305+
ret = _impl_tasklet_throw_bomb(self, 0, bomb);
1306+
#endif
1307+
STACKLESS_ASSERT();
1308+
12151309
return ret;
12161310
}
12171311

@@ -1256,20 +1350,38 @@ impl_tasklet_kill(PyTaskletObject *task, int pending)
12561350
STACKLESS_GETARG();
12571351
PyObject *ret;
12581352

1259-
/*
1260-
* silently do nothing if the tasklet is dead.
1261-
* simple raising would kill ourself in this case.
1353+
/* We might be called without a thread state. If the tasklet
1354+
* still has a frame, impl_tasklet_throw() will raise
1355+
* RuntimeError. Therefore we need either to bind the tasklet to
1356+
* a thread or drop its frames. Both makes sense, but the documentation
1357+
* states, that Stackless does not silently change the thread of
1358+
* a tasklet. Therefore we drop the frames.
12621359
*/
1263-
if (slp_get_frame(task) == NULL) {
1264-
/* it can still be a new tasklet and not a dead one */
1265-
Py_CLEAR(task->f.cframe);
1266-
if (task->next) {
1267-
/* remove it from the run queue */
1268-
assert(!task->flags.blocked);
1269-
slp_current_remove_tasklet(task);
1270-
Py_DECREF(task);
1360+
assert(task->cstate);
1361+
if (task->cstate->tstate == NULL) {
1362+
#ifdef SLP_TASKLET_KILL_REBINDS_THREAD
1363+
/* No thread state. Silently bind the tasklet to
1364+
* the current thread or drop its frames.
1365+
* Either action prevents an error in impl_tasklet_throw().
1366+
*/
1367+
if (task->cstate->nesting_level == 0 && task->f.frame) {
1368+
/* rebind to the current thread */
1369+
PyObject *arg = PyTuple_New(0);
1370+
if (arg == NULL)
1371+
return NULL;
1372+
ret = tasklet_bind_thread((PyObject *)task, arg);
1373+
Py_DECREF(arg);
1374+
assert(ret != NULL); /* should not fail, if nesting_level is 0 */
1375+
if (ret == NULL) /* in case of a bug */
1376+
return NULL;
1377+
Py_DECREF(ret);
1378+
} else {
1379+
Py_CLEAR(task->f.frame); /* or better tasklet_clear_frames(task) ? */
12711380
}
1272-
Py_RETURN_NONE;
1381+
#else
1382+
/* drop the frame */
1383+
Py_CLEAR(task->f.frame); /* or better tasklet_clear_frames(task) ? */
1384+
#endif
12731385
}
12741386

12751387
/* we might be called after exceptions are gone */

0 commit comments

Comments
 (0)