Skip to content

Commit 17629a1

Browse files
committed
Use server-side cursor for postgres in some cases
In filter, filter_iter, and _materialize_multilinks, use named cursor with postgresql. This turns of client-side cursor handling and avoids *large* roundup process (or wsgi process) in case of large results. Fixes issue2551114.
1 parent c6137c3 commit 17629a1

File tree

6 files changed

+267
-191
lines changed

6 files changed

+267
-191
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ Fixed:
6666
- in rest interface, fix uncaught exceptions when parsing invalid
6767
Content-Type and Accept headers. Document response formats more
6868
fully in doc/rest.txt.
69+
- in filter, filter_iter and _materialize_multilinks, use named cursor
70+
with postgresql. This turns of client-side cursor handling and avoids
71+
*large* roundup process (or wsgi process) in case of large results.
72+
Fixes issue2551114.
6973

7074
Features:
7175
- issue2550522 - Add 'filter' command to command-line

roundup/backends/back_postgresql.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def connection_dict(config, dbnamestr=None):
4747
def db_create(config):
4848
"""Clear all database contents and drop database itself"""
4949
command = "CREATE DATABASE \"%s\" WITH ENCODING='UNICODE'"%config.RDBMS_NAME
50-
if config.RDBMS_TEMPLATE :
50+
if config.RDBMS_TEMPLATE:
5151
command = command + " TEMPLATE=%s" % config.RDBMS_TEMPLATE
5252
logging.getLogger('roundup.hyperdb').info(command)
5353
db_command(config, command)
@@ -99,13 +99,13 @@ def pg_command(cursor, command):
9999
cursor.execute(command)
100100
except psycopg2.DatabaseError as err:
101101
response = str(err).split('\n')[0]
102-
if "FATAL" not in response :
102+
if "FATAL" not in response:
103103
msgs = (
104104
'is being accessed by other users',
105105
'could not serialize access due to concurrent update',
106106
)
107-
for msg in msgs :
108-
if msg in response :
107+
for msg in msgs:
108+
if msg in response:
109109
time.sleep(0.1)
110110
return 0
111111
raise RuntimeError (response)
@@ -167,6 +167,18 @@ def sql_open_connection(self):
167167

168168
return (conn, cursor)
169169

170+
def sql_new_cursor(self, name='default', conn=None, *args, **kw):
171+
""" Create new cursor, this may need additional parameters for
172+
performance optimization for different backends.
173+
"""
174+
use_name = self.config.RDBMS_SERVERSIDE_CURSOR
175+
kw = {}
176+
if use_name:
177+
kw['name'] = name
178+
if conn is None:
179+
conn = self.conn
180+
return conn.cursor(*args, **kw)
181+
170182
def open_connection(self):
171183
if not db_exists(self.config):
172184
db_create(self.config)

roundup/backends/rdbms_common.py

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,14 +1146,14 @@ def _materialize_multilink(self, classname, nodeid, node, propname):
11461146
joi = ', %s' % tn2
11471147
w = ' and %s.%s=%s.id and %s.__retired__=0'%(tn, lid,
11481148
tn2, tn2)
1149+
cursor = self.sql_new_cursor(name='_materialize_multilink')
11491150
sql = 'select %s from %s%s where %s=%s%s' %(lid, tn, joi, nid,
11501151
self.arg, w)
1151-
self.sql(sql, (nodeid,))
1152-
# extract the first column from the result
1153-
# XXX numeric ids
1154-
items = [int(x[0]) for x in self.cursor.fetchall()]
1155-
items.sort()
1156-
node[propname] = [str(x) for x in items]
1152+
self.sql(sql, (nodeid,), cursor)
1153+
# Reduce this to only the first row (the ID), this can save a
1154+
# lot of space for large query results (not using fetchall)
1155+
node[propname] = [str(x) for x in sorted(int(r[0]) for r in cursor)]
1156+
cursor.close()
11571157

11581158
def _materialize_multilinks(self, classname, nodeid, node, props=None):
11591159
""" get all Multilinks of a node (lazy eval may have skipped this)
@@ -1461,6 +1461,14 @@ def sql_commit(self):
14611461
# open a new cursor for subsequent work
14621462
self.cursor = self.conn.cursor()
14631463

1464+
def sql_new_cursor(self, conn=None, *args, **kw):
1465+
""" Create new cursor, this may need additional parameters for
1466+
performance optimization for different backends.
1467+
"""
1468+
if conn is None:
1469+
conn = self.conn
1470+
return conn.cursor()
1471+
14641472
def commit(self):
14651473
""" Commit the current transactions.
14661474
@@ -2854,18 +2862,26 @@ def filter(self, search_matches, filterspec, sort=[], group=[],
28542862
return []
28552863
proptree, sql, args = sq
28562864

2857-
self.db.sql(sql, args)
2858-
l = self.db.sql_fetchall()
2865+
cursor = self.db.sql_new_cursor(name='filter')
2866+
self.db.sql(sql, args, cursor)
2867+
# Reduce this to only the first row (the ID), this can save a
2868+
# lot of space for large query results (not using fetchall)
2869+
# We cannot do this if sorting by multilink
2870+
if proptree.tree_sort_done:
2871+
l = [str(row[0]) for row in cursor]
2872+
else:
2873+
l = cursor.fetchall()
2874+
cursor.close()
28592875

2876+
# Multilink sorting
28602877
# Compute values needed for sorting in proptree.sort
2861-
for p in proptree:
2862-
if hasattr(p, 'auxcol'):
2863-
p.sort_ids = [row[p.auxcol] for row in l]
2864-
p.sort_result = p._sort_repr (p.propclass.sort_repr, p.sort_ids)
2865-
# return the IDs (the first column)
2866-
# XXX numeric ids
2867-
l = [str(row[0]) for row in l]
2868-
l = proptree.sort (l)
2878+
if not proptree.tree_sort_done:
2879+
for p in proptree:
2880+
if hasattr(p, 'auxcol'):
2881+
p.sort_ids = [row[p.auxcol] for row in l]
2882+
p.sort_result = p._sort_repr \
2883+
(p.propclass.sort_repr, p.sort_ids)
2884+
l = proptree.sort ([str(row[0]) for row in l])
28692885

28702886
if __debug__:
28712887
self.db.stats['filtering'] += (time.time() - start_t)
@@ -2890,7 +2906,7 @@ def filter_iter(self, search_matches, filterspec, sort=[], group=[],
28902906
if sq is None:
28912907
return
28922908
proptree, sql, args = sq
2893-
cursor = self.db.conn.cursor()
2909+
cursor = self.db.sql_new_cursor(name='filter_iter')
28942910
self.db.sql(sql, args, cursor)
28952911
classes = {}
28962912
for p in proptree:
@@ -2922,6 +2938,7 @@ def filter_iter(self, search_matches, filterspec, sort=[], group=[],
29222938
node[propname] = value
29232939
self.db._cache_save(key, node)
29242940
yield str(row[0])
2941+
cursor.close()
29252942

29262943
def filter_sql(self, sql):
29272944
"""Return a list of the ids of the items in this class that match

roundup/configuration.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,10 @@ def str2value(self, value):
10821082
"Database isolation level, currently supported for\n"
10831083
"PostgreSQL and mysql. See, e.g.,\n"
10841084
"http://www.postgresql.org/docs/9.1/static/transaction-iso.html"),
1085+
(BooleanOption, "serverside_cursor", "yes",
1086+
"Set the database cursor for filter queries to serverside\n"
1087+
"cursor, this avoids caching large amounts of data in the\n"
1088+
"client. This option only applies for the postgresql backend."),
10851089
), "Settings in this section (except for backend) are used"
10861090
" by RDBMS backends only."
10871091
),

0 commit comments

Comments
 (0)