Endless loop calling PL/Python set returning functions

Started by Alexey Grishchenkoalmost 10 years ago10 messages
#1Alexey Grishchenko
agrishchenko@pivotal.io
1 attachment(s)

Hello

There is a bug in implementation of set-returning functions in PL/Python.
When you call the same set-returning function twice in a single query, the
executor falls to infinite loop which causes OOM. Here is a simple
reproduction for this issue:

CREATE OR REPLACE FUNCTION func(iter int) RETURNS SETOF int AS $$
return xrange(iter)
$$ LANGUAGE plpythonu;

select func(3), func(4);

The endless loop is caused by the fact that PL/Python uses PLyProcedure
structure for each of the functions, containing information specific for
the function. This structure is used to store the result set iterator
returned by the Python function call. But in fact, when we call the same
function twice, PL/Python uses the same structure for both calls, and the
same result set iterator (PLyProcedure.setof), which is being constantly
updated by one function after another. When the iterator reaches the end,
the first function sets it to null. Then Postgres calls the second
function, it receives NULL iterator and calls Python function once again,
receiving another iterator. This is an endless loop

In fact, for set-returning functions in Postgres we should use a set
of SRF_* functions, which gives us an access to function call context
(FuncCallContext). In my implementation this context is used to store the
iterator for function result set, so these two calls would have separate
iterators and the query would succeed.

Another issue with calling the same set-returning function twice in the
same query, is that it would delete the input parameter of the function
from the global variables dictionary at the end of execution. With calling
the function twice, this code attempts to delete the same entry from global
variables dict twice, thus causing KeyError. This is why the
function PLy_function_delete_args is modified as well to check whether the
key we intend to delete is in the globals dictionary.

New regression test is included in the patch.

--
Best regards,
Alexey Grishchenko

Attachments:

0001-Fix-endless-loop-in-plpython-set-returning-function.patchapplication/octet-stream; name=0001-Fix-endless-loop-in-plpython-set-returning-function.patchDownload
From ac48cc9ca7ceb8b1ed0e51a3902b8879673fbf73 Mon Sep 17 00:00:00 2001
From: Alexey Grishchenko <agrishchenko@pivotal.io>
Date: Thu, 10 Mar 2016 12:56:06 +0000
Subject: [PATCH] Fix endless loop in plpython set-returning function

The issue occurs when the same set-returning function is called twice within a single
query. The issue is caused by the fact that result iterator is stored in the procedure
data structure, which is reused for different calls of the same procedure. But in fact,
when you execute the same SRF twice in the same query, the result iterator is being
continuously reinitialized by two different instances of the same function, the
execution never ends and process dies with OOM

Also this fix changes the PLy_function_delete_args, because with calling two SRF in a
single query the old implementation would attempt to delete the same input parameter
twice, which would lead to key error

Regression test added for this case
---
 src/pl/plpython/expected/plpython_setof.out | 12 ++++++++
 src/pl/plpython/plpy_exec.c                 | 45 ++++++++++++++++++++++-------
 src/pl/plpython/plpy_procedure.c            |  1 -
 src/pl/plpython/plpy_procedure.h            |  1 -
 src/pl/plpython/sql/plpython_setof.sql      |  3 ++
 5 files changed, 49 insertions(+), 13 deletions(-)

diff --git a/src/pl/plpython/expected/plpython_setof.out b/src/pl/plpython/expected/plpython_setof.out
index 62b8a45..d35f9c2 100644
--- a/src/pl/plpython/expected/plpython_setof.out
+++ b/src/pl/plpython/expected/plpython_setof.out
@@ -124,6 +124,18 @@ SELECT test_setof_spi_in_iterator();
  World
 (4 rows)
 
+-- Calling the same set-returning function twice in a single query
+select test_setof_as_list(2, 'list'), test_setof_as_list(3, 'list');
+ test_setof_as_list | test_setof_as_list 
+--------------------+--------------------
+ list               | list
+ list               | list
+ list               | list
+ list               | list
+ list               | list
+ list               | list
+(6 rows)
+
 -- returns set of named-composite-type tuples
 CREATE OR REPLACE FUNCTION get_user_records()
 RETURNS SETOF users
diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c
index 24aed01..f68f310 100644
--- a/src/pl/plpython/plpy_exec.c
+++ b/src/pl/plpython/plpy_exec.c
@@ -47,11 +47,22 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 	Datum		rv;
 	PyObject   *volatile plargs = NULL;
 	PyObject   *volatile plrv = NULL;
+	FuncCallContext	*volatile funcctx = NULL;
 	ErrorContextCallback plerrcontext;
 
 	PG_TRY();
 	{
-		if (!proc->is_setof || proc->setof == NULL)
+		if (proc->is_setof)
+		{
+			/* First Call setup */
+			if (SRF_IS_FIRSTCALL())
+				funcctx = SRF_FIRSTCALL_INIT();
+			/* Every call setup */
+			funcctx = SRF_PERCALL_SETUP();
+			Assert(funcctx != NULL);
+		}
+
+		if (!proc->is_setof || funcctx->user_fctx == NULL)
 		{
 			/*
 			 * Simple type returning function or first time for SETOF
@@ -80,7 +91,7 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 			bool		has_error = false;
 			ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo;
 
-			if (proc->setof == NULL)
+			if (funcctx->user_fctx == NULL)
 			{
 				/* first time -- do checks and setup */
 				if (!rsi || !IsA(rsi, ReturnSetInfo) ||
@@ -94,11 +105,11 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 				rsi->returnMode = SFRM_ValuePerCall;
 
 				/* Make iterator out of returned object */
-				proc->setof = PyObject_GetIter(plrv);
+				funcctx->user_fctx = (void*) PyObject_GetIter(plrv);
 				Py_DECREF(plrv);
 				plrv = NULL;
 
-				if (proc->setof == NULL)
+				if (funcctx->user_fctx == NULL)
 					ereport(ERROR,
 							(errcode(ERRCODE_DATATYPE_MISMATCH),
 							 errmsg("returned object cannot be iterated"),
@@ -106,7 +117,7 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 			}
 
 			/* Fetch next from iterator */
-			plrv = PyIter_Next(proc->setof);
+			plrv = PyIter_Next(funcctx->user_fctx);
 			if (plrv)
 				rsi->isDone = ExprMultipleResult;
 			else
@@ -118,8 +129,8 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 			if (rsi->isDone == ExprEndResult)
 			{
 				/* Iterator is exhausted or error happened */
-				Py_DECREF(proc->setof);
-				proc->setof = NULL;
+				Py_DECREF( (PyObject*) funcctx->user_fctx);
+				funcctx->user_fctx = NULL;
 
 				Py_XDECREF(plargs);
 				Py_XDECREF(plrv);
@@ -134,7 +145,7 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 					elog(ERROR, "SPI_finish failed");
 
 				fcinfo->isnull = true;
-				return (Datum) NULL;
+				SRF_RETURN_DONE(funcctx);
 			}
 		}
 
@@ -213,8 +224,11 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 		 * yet. Set it to NULL so the next invocation of the function will
 		 * start the iteration again.
 		 */
-		Py_XDECREF(proc->setof);
-		proc->setof = NULL;
+		if (proc->is_setof && funcctx->user_fctx != NULL)
+		{
+			Py_XDECREF( (PyObject*) funcctx->user_fctx );
+			funcctx->user_fctx = NULL;
+		}
 
 		PG_RE_THROW();
 	}
@@ -435,13 +449,22 @@ static void
 PLy_function_delete_args(PLyProcedure *proc)
 {
 	int			i;
+	PyObject	*arg;
 
 	if (!proc->argnames)
 		return;
 
 	for (i = 0; i < proc->nargs; i++)
 		if (proc->argnames[i])
-			PyDict_DelItemString(proc->globals, proc->argnames[i]);
+		{
+			arg = PyString_FromString(proc->argnames[i]);
+
+			/* Deleting the item only if it exists in the dictionaty */
+			if (PyDict_Contains(proc->globals, arg))
+				PyDict_DelItem(proc->globals, arg);
+
+			Py_DECREF(arg);
+		}
 }
 
 static void
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index a0d0792..5cad06d 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -203,7 +203,6 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 		proc->code = proc->statics = NULL;
 		proc->globals = NULL;
 		proc->is_setof = procStruct->proretset;
-		proc->setof = NULL;
 		proc->src = NULL;
 		proc->argnames = NULL;
 
diff --git a/src/pl/plpython/plpy_procedure.h b/src/pl/plpython/plpy_procedure.h
index 9fc8db0..2ec10d3 100644
--- a/src/pl/plpython/plpy_procedure.h
+++ b/src/pl/plpython/plpy_procedure.h
@@ -24,7 +24,6 @@ typedef struct PLyProcedure
 	PLyTypeInfo result;			/* also used to store info for trigger tuple
 								 * type */
 	bool		is_setof;		/* true, if procedure returns result set */
-	PyObject   *setof;			/* contents of result set. */
 	char	   *src;			/* textual procedure code, after mangling */
 	char	  **argnames;		/* Argument names */
 	PLyTypeInfo args[FUNC_MAX_ARGS];
diff --git a/src/pl/plpython/sql/plpython_setof.sql b/src/pl/plpython/sql/plpython_setof.sql
index fe034fb..63ca466 100644
--- a/src/pl/plpython/sql/plpython_setof.sql
+++ b/src/pl/plpython/sql/plpython_setof.sql
@@ -63,6 +63,9 @@ SELECT test_setof_as_iterator(2, null);
 
 SELECT test_setof_spi_in_iterator();
 
+-- Calling the same set-returning function twice in a single query
+select test_setof_as_list(2, 'list'), test_setof_as_list(3, 'list');
+
 
 -- returns set of named-composite-type tuples
 CREATE OR REPLACE FUNCTION get_user_records()
-- 
1.9.5 (Apple Git-50.3)

#2Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alexey Grishchenko (#1)
Re: Endless loop calling PL/Python set returning functions

Alexey Grishchenko <agrishchenko@pivotal.io> writes:

There is a bug in implementation of set-returning functions in PL/Python.
When you call the same set-returning function twice in a single query, the
executor falls to infinite loop which causes OOM.

Ugh.

Another issue with calling the same set-returning function twice in the
same query, is that it would delete the input parameter of the function
from the global variables dictionary at the end of execution. With calling
the function twice, this code attempts to delete the same entry from global
variables dict twice, thus causing KeyError. This is why the
function PLy_function_delete_args is modified as well to check whether the
key we intend to delete is in the globals dictionary.

That whole business with putting a function's parameters into a global
dictionary makes me itch. Doesn't it mean problems if one plpython
function calls another (presumably via SPI)?

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#3Alexey Grishchenko
agrishchenko@pivotal.io
In reply to: Tom Lane (#2)
Re: Endless loop calling PL/Python set returning functions

I agree that passing function parameters through globals is not the best
solution

It works in a following way - executing custom code (in our case Python
function invocation) in Python is made with PyEval_EvalCode
<https://docs.python.org/2/c-api/veryhigh.html&gt;. As an input to this C
function you specify dictionary of globals that would be available to this
code. The structure PLyProcedure stores "PyObject *globals;", which is the
dictionary of globals for specific function. So SPI works pretty fine, as
each function has a separate dictionary of globals and they don't conflict
with each other

One scenario when the problem occurs, is when you are calling the same
set-returning function in a single query twice. This way they share the
same "globals" which is not a bad thing, but when one function finishes
execution and deallocates input parameter's global, the second will fail
trying to do the same. I included the fix for this problem in my patch

The second scenario when the problem occurs is when you want to call the
same PL/Python function in recursion. For example, this code will not work:

create or replace function test(a int) returns int as $BODY$
r = 0
if a > 1:
r = plpy.execute("SELECT test(%d) as a" % (a-1))[0]['a']
return a + r
$BODY$ language plpythonu;

select test(10);

The function "test" has a single PLyProcedure object allocated to handle
it, thus it has a single "globals" dictionary. When internal function call
finishes, it removes the key "a" from the dictionary, and the outer
function fails with "NameError: global name 'a' is not defined" when it
tries to execute "return a + r"

But the second issue is a separate story and I think it is worth a separate
patch

On Thu, Mar 10, 2016 at 3:35 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alexey Grishchenko <agrishchenko@pivotal.io> writes:

There is a bug in implementation of set-returning functions in PL/Python.
When you call the same set-returning function twice in a single query,

the

executor falls to infinite loop which causes OOM.

Ugh.

Another issue with calling the same set-returning function twice in the
same query, is that it would delete the input parameter of the function
from the global variables dictionary at the end of execution. With

calling

the function twice, this code attempts to delete the same entry from

global

variables dict twice, thus causing KeyError. This is why the
function PLy_function_delete_args is modified as well to check whether

the

key we intend to delete is in the globals dictionary.

That whole business with putting a function's parameters into a global
dictionary makes me itch. Doesn't it mean problems if one plpython
function calls another (presumably via SPI)?

regards, tom lane

--
Best regards,
Alexey Grishchenko

#4Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alexey Grishchenko (#3)
Re: Endless loop calling PL/Python set returning functions

Alexey Grishchenko <agrishchenko@pivotal.io> writes:

One scenario when the problem occurs, is when you are calling the same
set-returning function in a single query twice. This way they share the
same "globals" which is not a bad thing, but when one function finishes
execution and deallocates input parameter's global, the second will fail
trying to do the same. I included the fix for this problem in my patch

The second scenario when the problem occurs is when you want to call the
same PL/Python function in recursion. For example, this code will not work:

Right, the recursion case is what's not being covered by this patch.
I would rather have a single patch that deals with both of those cases,
perhaps by *not* sharing the same dictionary across calls. I think
what you've done here is not so much a fix as a band-aid. In fact,
it doesn't even really fix the problem for the two-calls-per-query
case does it? It'll work if the first execution of the SRF is run to
completion before starting the second one, but not if the two executions
are interleaved. I believe you can test that type of scenario with
something like

select set_returning_function_1(...), set_returning_function_2(...);

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#5Alexey Grishchenko
agrishchenko@pivotal.io
In reply to: Tom Lane (#4)
Re: Endless loop calling PL/Python set returning functions

No, my fix handles this well.

In fact, with the first function call you allocate global variables
representing Python function input parameters, call the function and
receive iterator over the function results. Then in a series of Postgres
calls to PL/Python handler you just fetch next value from the iterator, you
are not calling the Python function anymore. When the iterator reaches the
end, PL/Python call handler deallocates the global variable representing
function input parameter.

Regardless of the number of parallel invocations of the same function, each
of them in my patch would set its own input parameters to the Python
function, call the function and receive separate iterators. When the first
function's result iterator would reach its end, it would deallocate the
input global variable. But it won't affect other functions as they no
longer need to invoke any Python code. Even if they need - they would
reallocate global variable (it would be set before the Python function
invocation). The issue here was in the fact that they tried to deallocate
the global input variable multiple times independently, which caused error
that I fixed.

Regarding the patch for the second case with recursion - not caching the
"globals" between function calls would have a performance impact, as you
would have to construct "globals" object before each function call. And you
need globals as it contains references to "plpy" module and global
variables and global dictionary ("GD"). I will think on this, maybe there
might be a better design for this scenario. But I still think the second
scenario requires a separate patch

On Thu, Mar 10, 2016 at 4:33 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alexey Grishchenko <agrishchenko@pivotal.io> writes:

One scenario when the problem occurs, is when you are calling the same
set-returning function in a single query twice. This way they share the
same "globals" which is not a bad thing, but when one function finishes
execution and deallocates input parameter's global, the second will fail
trying to do the same. I included the fix for this problem in my patch

The second scenario when the problem occurs is when you want to call the
same PL/Python function in recursion. For example, this code will not

work:

Right, the recursion case is what's not being covered by this patch.
I would rather have a single patch that deals with both of those cases,
perhaps by *not* sharing the same dictionary across calls. I think
what you've done here is not so much a fix as a band-aid. In fact,
it doesn't even really fix the problem for the two-calls-per-query
case does it? It'll work if the first execution of the SRF is run to
completion before starting the second one, but not if the two executions
are interleaved. I believe you can test that type of scenario with
something like

select set_returning_function_1(...), set_returning_function_2(...);

regards, tom lane

--
Best regards,
Alexey Grishchenko

#6Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alexey Grishchenko (#5)
Re: Endless loop calling PL/Python set returning functions

Alexey Grishchenko <agrishchenko@pivotal.io> writes:

No, my fix handles this well.
In fact, with the first function call you allocate global variables
representing Python function input parameters, call the function and
receive iterator over the function results. Then in a series of Postgres
calls to PL/Python handler you just fetch next value from the iterator, you
are not calling the Python function anymore. When the iterator reaches the
end, PL/Python call handler deallocates the global variable representing
function input parameter.

Regardless of the number of parallel invocations of the same function, each
of them in my patch would set its own input parameters to the Python
function, call the function and receive separate iterators. When the first
function's result iterator would reach its end, it would deallocate the
input global variable. But it won't affect other functions as they no
longer need to invoke any Python code.

Well, if you think that works, why not undo the global-dictionary changes
at the end of the first call, rather than later? Then there's certainly
no overlap in their lifespan.

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#7Alexey Grishchenko
agrishchenko@pivotal.io
In reply to: Tom Lane (#6)
Re: Endless loop calling PL/Python set returning functions

Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alexey Grishchenko <agrishchenko@pivotal.io <javascript:;>> writes:

No, my fix handles this well.
In fact, with the first function call you allocate global variables
representing Python function input parameters, call the function and
receive iterator over the function results. Then in a series of Postgres
calls to PL/Python handler you just fetch next value from the iterator,

you

are not calling the Python function anymore. When the iterator reaches

the

end, PL/Python call handler deallocates the global variable representing
function input parameter.

Regardless of the number of parallel invocations of the same function,

each

of them in my patch would set its own input parameters to the Python
function, call the function and receive separate iterators. When the

first

function's result iterator would reach its end, it would deallocate the
input global variable. But it won't affect other functions as they no
longer need to invoke any Python code.

Well, if you think that works, why not undo the global-dictionary changes
at the end of the first call, rather than later? Then there's certainly
no overlap in their lifespan.

regards, tom lane

Could you elaborate more on this? In general, stack-like solution would
work - if before the function call there is a global variable with the name
matching input variable name, push its value to the stack, and pop it after
the function execution. Would implement it tomorrow and see how it works

--

Sent from handheld device

#8Alexey Grishchenko
agrishchenko@pivotal.io
In reply to: Alexey Grishchenko (#7)
1 attachment(s)
Re: Endless loop calling PL/Python set returning functions

Alexey Grishchenko <agrishchenko@pivotal.io> wrote:

Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alexey Grishchenko <agrishchenko@pivotal.io> writes:

No, my fix handles this well.
In fact, with the first function call you allocate global variables
representing Python function input parameters, call the function and
receive iterator over the function results. Then in a series of Postgres
calls to PL/Python handler you just fetch next value from the iterator,

you

are not calling the Python function anymore. When the iterator reaches

the

end, PL/Python call handler deallocates the global variable representing
function input parameter.

Regardless of the number of parallel invocations of the same function,

each

of them in my patch would set its own input parameters to the Python
function, call the function and receive separate iterators. When the

first

function's result iterator would reach its end, it would deallocate the
input global variable. But it won't affect other functions as they no
longer need to invoke any Python code.

Well, if you think that works, why not undo the global-dictionary changes
at the end of the first call, rather than later? Then there's certainly
no overlap in their lifespan.

regards, tom lane

Could you elaborate more on this? In general, stack-like solution would
work - if before the function call there is a global variable with the name
matching input variable name, push its value to the stack, and pop it after
the function execution. Would implement it tomorrow and see how it works

--

Sent from handheld device

I have improved the code using proposed approach. The second version of
patch is in attachment

It works in a following way - the procedure object PLyProcedure stores
information about the call stack depth (calldepth field) and the stack
itself (argstack field). When the call stack depth is zero we don't make
any additional processing, i.e. there won't be any performance impact for
existing enduser functions. Stack manipulations are put in action only when
the calldepth is greater than zero, which can be achieved either when the
function is called recursively with SPI, or when you are calling the same
set-returning function in a single query twice or more.

Example of multiple calls to SRF within a single function:

CREATE OR REPLACE FUNCTION func(iter int) RETURNS SETOF int AS $$
return xrange(iter)
$$ LANGUAGE plpythonu;

select func(3), func(4);

Before the patch query caused endless loop finishing with OOM. Now it works
as it should

Example of recursion with SPI:

CREATE OR REPLACE FUNCTION test(a int) RETURNS int AS $BODY$
r = 0
if a > 1:
r = plpy.execute("SELECT test(%d) as a" % (a-1))[0]['a']
return a + r
$BODY$ LANGUAGE plpythonu;

select test(10);

Before the patch query failed with "NameError: global name 'a' is not
defined". Now it works correctly and returns 55

--
Best regards,
Alexey Grishchenko

Attachments:

0002-Fix-endless-loop-in-plpython-set-returning-function.patchapplication/octet-stream; name=0002-Fix-endless-loop-in-plpython-set-returning-function.patchDownload
From 688c60a4e29b426cfe713030226836bcd1cc37ff Mon Sep 17 00:00:00 2001
From: Alexey Grishchenko <agrishchenko@pivotal.io>
Date: Thu, 10 Mar 2016 12:56:06 +0000
Subject: [PATCH] Fix endless loop in plpython set-returning function

The issue occurs when the same set-returning function is called twice within a single
query. The issue is caused by the fact that result iterator is stored in the procedure
data structure, which is reused for different calls of the same procedure. But in fact,
when you execute the same SRF twice in the same query, the result iterator is being
continuously reinitialized by two different instances of the same function, the
execution never ends and process dies with OOM

Also this fix changes the PLy_function_delete_args, because with calling two SRF in a
single query the old implementation would attempt to delete the same input parameter
twice, which would lead to key error

The fix implementation allows to fix another issue with recursive SPI function calls

Regression test added for these cases
---
 src/pl/plpython/expected/plpython_setof.out |  12 +++
 src/pl/plpython/expected/plpython_spi.out   |  14 +++
 src/pl/plpython/plpy_exec.c                 | 139 ++++++++++++++++++++++++++--
 src/pl/plpython/plpy_procedure.c            |   3 +-
 src/pl/plpython/plpy_procedure.h            |   9 +-
 src/pl/plpython/sql/plpython_setof.sql      |   3 +
 src/pl/plpython/sql/plpython_spi.sql        |  10 +-
 7 files changed, 177 insertions(+), 13 deletions(-)

diff --git a/src/pl/plpython/expected/plpython_setof.out b/src/pl/plpython/expected/plpython_setof.out
index 62b8a45..d35f9c2 100644
--- a/src/pl/plpython/expected/plpython_setof.out
+++ b/src/pl/plpython/expected/plpython_setof.out
@@ -124,6 +124,18 @@ SELECT test_setof_spi_in_iterator();
  World
 (4 rows)
 
+-- Calling the same set-returning function twice in a single query
+select test_setof_as_list(2, 'list'), test_setof_as_list(3, 'list');
+ test_setof_as_list | test_setof_as_list 
+--------------------+--------------------
+ list               | list
+ list               | list
+ list               | list
+ list               | list
+ list               | list
+ list               | list
+(6 rows)
+
 -- returns set of named-composite-type tuples
 CREATE OR REPLACE FUNCTION get_user_records()
 RETURNS SETOF users
diff --git a/src/pl/plpython/expected/plpython_spi.out b/src/pl/plpython/expected/plpython_spi.out
index e715ee5..f0dd376 100644
--- a/src/pl/plpython/expected/plpython_spi.out
+++ b/src/pl/plpython/expected/plpython_spi.out
@@ -57,6 +57,14 @@ for r in rv:
 return seq
 '
 	LANGUAGE plpythonu;
+CREATE FUNCTION spi_recursive_sum(a int) RETURNS int
+	AS
+'r = 0
+if a > 1:
+    r = plpy.execute("SELECT spi_recursive_sum(%d) as a" % (a-1))[0]["a"]
+return a + r
+'
+	LANGUAGE plpythonu;
 -- spi and nested calls
 --
 select nested_call_one('pass this along');
@@ -112,6 +120,12 @@ SELECT join_sequences(sequences) FROM sequences
 ----------------
 (0 rows)
 
+SELECT spi_recursive_sum(10);
+ spi_recursive_sum 
+-------------------
+                55
+(1 row)
+
 --
 -- plan and result objects
 --
diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c
index 24aed01..536046d 100644
--- a/src/pl/plpython/plpy_exec.c
+++ b/src/pl/plpython/plpy_exec.c
@@ -39,6 +39,8 @@ static void plpython_trigger_error_callback(void *arg);
 static PyObject *PLy_procedure_call(PLyProcedure *proc, char *kargs, PyObject *vargs);
 static void PLy_abort_open_subtransactions(int save_subxact_level);
 
+static void PLy_global_args_push(PLyProcedure *proc);
+static void PLy_global_args_pop(PLyProcedure *proc);
 
 /* function subhandler */
 Datum
@@ -47,17 +49,33 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 	Datum		rv;
 	PyObject   *volatile plargs = NULL;
 	PyObject   *volatile plrv = NULL;
+	FuncCallContext	*volatile funcctx = NULL;
 	ErrorContextCallback plerrcontext;
 
 	PG_TRY();
 	{
-		if (!proc->is_setof || proc->setof == NULL)
+		if (proc->is_setof)
+		{
+			/* First Call setup */
+			if (SRF_IS_FIRSTCALL())
+				funcctx = SRF_FIRSTCALL_INIT();
+			/* Every call setup */
+			funcctx = SRF_PERCALL_SETUP();
+			Assert(funcctx != NULL);
+		}
+
+		if (!proc->is_setof || funcctx->user_fctx == NULL)
 		{
 			/*
 			 * Simple type returning function or first time for SETOF
 			 * function: actually execute the function.
 			 */
 			plargs = PLy_function_build_args(fcinfo, proc);
+			/*
+			 * In case of recursive call or SRF we might need to push old version
+			 * of arguments into the stack
+			 */
+			PLy_global_args_push(proc);
 			plrv = PLy_procedure_call(proc, "args", plargs);
 			if (!proc->is_setof)
 			{
@@ -80,7 +98,7 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 			bool		has_error = false;
 			ReturnSetInfo *rsi = (ReturnSetInfo *) fcinfo->resultinfo;
 
-			if (proc->setof == NULL)
+			if (funcctx->user_fctx == NULL)
 			{
 				/* first time -- do checks and setup */
 				if (!rsi || !IsA(rsi, ReturnSetInfo) ||
@@ -94,11 +112,11 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 				rsi->returnMode = SFRM_ValuePerCall;
 
 				/* Make iterator out of returned object */
-				proc->setof = PyObject_GetIter(plrv);
+				funcctx->user_fctx = (void*) PyObject_GetIter(plrv);
 				Py_DECREF(plrv);
 				plrv = NULL;
 
-				if (proc->setof == NULL)
+				if (funcctx->user_fctx == NULL)
 					ereport(ERROR,
 							(errcode(ERRCODE_DATATYPE_MISMATCH),
 							 errmsg("returned object cannot be iterated"),
@@ -106,7 +124,7 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 			}
 
 			/* Fetch next from iterator */
-			plrv = PyIter_Next(proc->setof);
+			plrv = PyIter_Next(funcctx->user_fctx);
 			if (plrv)
 				rsi->isDone = ExprMultipleResult;
 			else
@@ -118,8 +136,8 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 			if (rsi->isDone == ExprEndResult)
 			{
 				/* Iterator is exhausted or error happened */
-				Py_DECREF(proc->setof);
-				proc->setof = NULL;
+				Py_DECREF( (PyObject*) funcctx->user_fctx);
+				funcctx->user_fctx = NULL;
 
 				Py_XDECREF(plargs);
 				Py_XDECREF(plrv);
@@ -134,7 +152,7 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 					elog(ERROR, "SPI_finish failed");
 
 				fcinfo->isnull = true;
-				return (Datum) NULL;
+				SRF_RETURN_DONE(funcctx);
 			}
 		}
 
@@ -213,8 +231,11 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 		 * yet. Set it to NULL so the next invocation of the function will
 		 * start the iteration again.
 		 */
-		Py_XDECREF(proc->setof);
-		proc->setof = NULL;
+		if (proc->is_setof && funcctx->user_fctx != NULL)
+		{
+			Py_XDECREF( (PyObject*) funcctx->user_fctx );
+			funcctx->user_fctx = NULL;
+		}
 
 		PG_RE_THROW();
 	}
@@ -442,6 +463,9 @@ PLy_function_delete_args(PLyProcedure *proc)
 	for (i = 0; i < proc->nargs; i++)
 		if (proc->argnames[i])
 			PyDict_DelItemString(proc->globals, proc->argnames[i]);
+
+	/* Pop global arguments from the stack if they were pushed there before */
+	PLy_global_args_pop(proc);
 }
 
 static void
@@ -855,3 +879,98 @@ PLy_abort_open_subtransactions(int save_subxact_level)
 		pfree(subtransactiondata);
 	}
 }
+
+static void
+PLy_global_args_push(PLyProcedure *proc)
+{
+	PLyArgsStack *node;
+	PyObject     *arglist;
+	MemoryContext oldcxt;
+
+	/*
+	 * Action is needed only if the same function was already called either
+	 * with SPI (recursive call) or with multiple set-returning functions in
+	 * a single query
+	 */
+	proc->calldepth += 1;
+	if (proc->calldepth > 1) {
+		/* Nothing to do if the function has no input parameters */
+		if (!proc->argnames)
+			return;
+
+		/* Fetch the argument list (Python list object) from the globals */
+		arglist = PyDict_GetItemString(proc->globals, "args");
+		if (arglist == NULL)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("\"args\" object is not defined prior to function call"),
+					 errdetail("PL/Python function does not allow removal of global \"args\" object")));
+		}
+		else
+		{
+			Py_INCREF(arglist);
+
+			/*
+			 * Push the function argument list into the stack in procedure
+			 * memory context
+			 */
+			oldcxt = MemoryContextSwitchTo(proc->mcxt);
+			node = (PLyArgsStack *) palloc0(sizeof(PLyArgsStack));
+			node->args = arglist;
+			node->next = proc->argstack;
+			proc->argstack = node;
+			MemoryContextSwitchTo(oldcxt);
+		}
+	}
+}
+
+static void
+PLy_global_args_pop(PLyProcedure *proc)
+{
+	int			  i;
+	PLyArgsStack *ptr;
+	PyObject     *arg;
+
+	/*
+	 * Action is needed only if the same function was already called either
+	 * with SPI (recursive call) or with multiple set-returning functions in
+	 * a single query
+	 */
+	if (proc->calldepth > 1) {
+		/* Nothing to do if the function has no input parameters */
+		if (!proc->argnames)
+			return;
+
+		/* If stack entry exist we do the pop */
+		if (proc->argstack != NULL) {
+			for (i = 0; i < proc->nargs; i++)
+				if (proc->argnames[i]) {
+
+					/* Get the argument object from saved argument list */
+					arg = PyList_GetItem(proc->argstack->args, i);
+					if (arg == NULL)
+						PLy_elog(ERROR, "PyList_GetItem() failed, while processing "
+										"arguments from the call cache");
+
+					/* Push the argument object back to the function globals */
+					if (PyDict_SetItemString(proc->globals, proc->argnames[i], arg) == -1)
+						PLy_elog(ERROR, "PyDict_SetItemString() failed, while setting up "
+										"arguments from the call cache");
+				}
+
+			/* Set the list of arguments back to globals */
+			if (PyDict_SetItemString(proc->globals, "args", proc->argstack->args) == -1)
+				PLy_elog(ERROR, "PyDict_SetItemString() failed, while setting up "
+								"arguments list object from the call cache");
+
+			/* Free stack entry */
+			ptr = proc->argstack->next;
+			Py_DECREF(proc->argstack->args);
+			pfree(proc->argstack);
+			proc->argstack = ptr;
+		}
+	}
+
+	proc->calldepth -= 1;
+}
\ No newline at end of file
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index a0d0792..948b976 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -203,9 +203,10 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 		proc->code = proc->statics = NULL;
 		proc->globals = NULL;
 		proc->is_setof = procStruct->proretset;
-		proc->setof = NULL;
 		proc->src = NULL;
 		proc->argnames = NULL;
+		proc->argstack = NULL;
+		proc->calldepth = 0;
 
 		/*
 		 * get information required for output conversion of the return value,
diff --git a/src/pl/plpython/plpy_procedure.h b/src/pl/plpython/plpy_procedure.h
index 9fc8db0..8a8b4cb 100644
--- a/src/pl/plpython/plpy_procedure.h
+++ b/src/pl/plpython/plpy_procedure.h
@@ -10,6 +10,12 @@
 
 extern void init_procedure_caches(void);
 
+/* stack of function call arguments */
+typedef struct PLyArgsStack
+{
+	PyObject			*args;	/* Python "list" object with call arguments */
+	struct PLyArgsStack *next;	/* pointer to the next stack element */
+} PLyArgsStack;
 
 /* cached procedure data */
 typedef struct PLyProcedure
@@ -24,7 +30,6 @@ typedef struct PLyProcedure
 	PLyTypeInfo result;			/* also used to store info for trigger tuple
 								 * type */
 	bool		is_setof;		/* true, if procedure returns result set */
-	PyObject   *setof;			/* contents of result set. */
 	char	   *src;			/* textual procedure code, after mangling */
 	char	  **argnames;		/* Argument names */
 	PLyTypeInfo args[FUNC_MAX_ARGS];
@@ -34,6 +39,8 @@ typedef struct PLyProcedure
 	PyObject   *code;			/* compiled procedure code */
 	PyObject   *statics;		/* data saved across calls, local scope */
 	PyObject   *globals;		/* data saved across calls, global scope */
+	int         calldepth;		/* depth of function recursive calls */
+	PLyArgsStack *argstack;		/* stack of function call arguments */
 } PLyProcedure;
 
 /* the procedure cache key */
diff --git a/src/pl/plpython/sql/plpython_setof.sql b/src/pl/plpython/sql/plpython_setof.sql
index fe034fb..63ca466 100644
--- a/src/pl/plpython/sql/plpython_setof.sql
+++ b/src/pl/plpython/sql/plpython_setof.sql
@@ -63,6 +63,9 @@ SELECT test_setof_as_iterator(2, null);
 
 SELECT test_setof_spi_in_iterator();
 
+-- Calling the same set-returning function twice in a single query
+select test_setof_as_list(2, 'list'), test_setof_as_list(3, 'list');
+
 
 -- returns set of named-composite-type tuples
 CREATE OR REPLACE FUNCTION get_user_records()
diff --git a/src/pl/plpython/sql/plpython_spi.sql b/src/pl/plpython/sql/plpython_spi.sql
index a882738..61fb614 100644
--- a/src/pl/plpython/sql/plpython_spi.sql
+++ b/src/pl/plpython/sql/plpython_spi.sql
@@ -69,7 +69,14 @@ return seq
 	LANGUAGE plpythonu;
 
 
-
+CREATE FUNCTION spi_recursive_sum(a int) RETURNS int
+	AS
+'r = 0
+if a > 1:
+    r = plpy.execute("SELECT spi_recursive_sum(%d) as a" % (a-1))[0]["a"]
+return a + r
+'
+	LANGUAGE plpythonu;
 
 
 -- spi and nested calls
@@ -88,6 +95,7 @@ SELECT join_sequences(sequences) FROM sequences
 SELECT join_sequences(sequences) FROM sequences
 	WHERE join_sequences(sequences) ~* '^B';
 
+SELECT spi_recursive_sum(10);
 
 --
 -- plan and result objects
-- 
1.9.5 (Apple Git-50.3)

#9Alexey Grishchenko
agrishchenko@pivotal.io
In reply to: Alexey Grishchenko (#8)
Re: Endless loop calling PL/Python set returning functions

Alexey Grishchenko <agrishchenko@pivotal.io> wrote:

Alexey Grishchenko <agrishchenko@pivotal.io> wrote:

Tom Lane <tgl@sss.pgh.pa.us> wrote:

Alexey Grishchenko <agrishchenko@pivotal.io> writes:

No, my fix handles this well.
In fact, with the first function call you allocate global variables
representing Python function input parameters, call the function and
receive iterator over the function results. Then in a series of

Postgres

calls to PL/Python handler you just fetch next value from the

iterator, you

are not calling the Python function anymore. When the iterator reaches

the

end, PL/Python call handler deallocates the global variable

representing

function input parameter.

Regardless of the number of parallel invocations of the same function,

each

of them in my patch would set its own input parameters to the Python
function, call the function and receive separate iterators. When the

first

function's result iterator would reach its end, it would deallocate the
input global variable. But it won't affect other functions as they no
longer need to invoke any Python code.

Well, if you think that works, why not undo the global-dictionary changes
at the end of the first call, rather than later? Then there's certainly
no overlap in their lifespan.

regards, tom lane

Could you elaborate more on this? In general, stack-like solution would
work - if before the function call there is a global variable with the name
matching input variable name, push its value to the stack, and pop it after
the function execution. Would implement it tomorrow and see how it works

--

Sent from handheld device

I have improved the code using proposed approach. The second version of
patch is in attachment

It works in a following way - the procedure object PLyProcedure stores
information about the call stack depth (calldepth field) and the stack
itself (argstack field). When the call stack depth is zero we don't make
any additional processing, i.e. there won't be any performance impact for
existing enduser functions. Stack manipulations are put in action only when
the calldepth is greater than zero, which can be achieved either when the
function is called recursively with SPI, or when you are calling the same
set-returning function in a single query twice or more.

Example of multiple calls to SRF within a single function:

CREATE OR REPLACE FUNCTION func(iter int) RETURNS SETOF int AS $$
return xrange(iter)
$$ LANGUAGE plpythonu;

select func(3), func(4);

Before the patch query caused endless loop finishing with OOM. Now it
works as it should

Example of recursion with SPI:

CREATE OR REPLACE FUNCTION test(a int) RETURNS int AS $BODY$
r = 0
if a > 1:
r = plpy.execute("SELECT test(%d) as a" % (a-1))[0]['a']
return a + r
$BODY$ LANGUAGE plpythonu;

select test(10);

Before the patch query failed with "NameError: global name 'a' is not
defined". Now it works correctly and returns 55

--
Best regards,
Alexey Grishchenko

Hi

Any comments on this patch?

Regarding passing parameters to the Python function using globals - it was
in initial design of PL/Python (code
<https://github.com/postgres/postgres/blob/0bef7ba549977154572bdbf5682a32a07839fd82/src/pl/plpython/plpython.c#L783&gt;,
documentation
<http://www.postgresql.org/docs/7.2/static/plpython-using.html&gt;).
Originally you had to work with "args" global list of input parameters and
wasn't able to access the named parameters directly. And you can do so even
with the latest release. Going away from global input parameters would
require switching to PyObject_CallFunctionObjArgs
<https://docs.python.org/2/c-api/object.html#c.PyObject_CallFunctionObjArgs&gt;,
which should be possible by changing the function declaration to include
input parameters plus "args" (for backward compatibility). However,
triggers are a bit different - they depend on modifying the global "TD"
dictionary inside the Python function, and they return only the status
string. For them, there is no option of modifying the code to avoid global
input parameters without breaking the backward compatibility with the old
enduser code

--
Best regards,
Alexey Grishchenko

#10Tom Lane
tgl@sss.pgh.pa.us
In reply to: Alexey Grishchenko (#9)
Re: Endless loop calling PL/Python set returning functions

Alexey Grishchenko <agrishchenko@pivotal.io> writes:

Any comments on this patch?

I felt that this was more nearly a bug fix than a new feature, so I picked
it up even though it's nominally in the next commitfest not the current
one. I did not like the code too much as it stood: you were not being
paranoid enough about ensuring that the callstack data structure stayed
in sync with the actual control flow. Also, it didn't work for functions
that modify their argument values (cf the committed regression tests);
you have to explicitly save named arguments not only the "args" version,
and you have to do it for SRF suspend/resume not just recursion cases.
But I cleaned all that up and committed it.

triggers are a bit different - they depend on modifying the global "TD"
dictionary inside the Python function, and they return only the status
string. For them, there is no option of modifying the code to avoid global
input parameters without breaking the backward compatibility with the old
enduser code.

Yeah. It might be worth the trouble to include triggers in the
save/restore logic, since at least in principle they can be invoked
recursively; but there's not that much practical use for such cases.
I didn't bother with that in the patch as-committed, but if you want
to follow up with an adjustment for it, I'd take a look.

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers