Skip to content

Commit 0dcf9af

Browse files
author
Ralf Schlatterbeck
committed
Process each message through the mail gateway as a separate transaction.
The mail-gateway used to process messages fetched, e.g., via imap in a single big transaction. Now we process each message coming in via the mail-gateway in its own transaction. Regression-tests passed. See also discussion: http://thread.gmane.org/gmane.comp.bug-tracking.roundup.user/9500
1 parent 25860d7 commit 0dcf9af

File tree

6 files changed

+144
-98
lines changed

6 files changed

+144
-98
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Fixes:
88
- fixed action taken in response to invalid GET request
99
- fixed classic tracker template to submit POST requests when appropriate
1010
- fix problems with french and german locale files (issue 2550546)
11+
- Run each message of the mail-gateway in a separate transaction,
12+
see http://thread.gmane.org/gmane.comp.bug-tracking.roundup.user/9500
1113

1214

1315
2009-03-18 1.4.8 (r4209)

doc/upgrading.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ steps.
1616
Migrating from 1.4.x to 1.4.9
1717
=============================
1818

19+
Customized MailGW Class
20+
-----------------------
21+
22+
If you have customized the MailGW class in your tracker: The new MailGW
23+
class opens the database for each message in the method handle_message
24+
(instance.open) instead of passing the opened database as a parameter to
25+
the MailGW constructor. The old handle_message has been renamed to
26+
_handle_message. The new method opens the database and wraps the call to
27+
the old method into a try/finally.
28+
29+
Your customized MailGW class needs to mirror this behavior.
30+
1931
Fix the "remove" button in issue files and messages lists
2032
---------------------------------------------------------
2133

roundup/instance.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ def __init__(self, tracker_home, optimize=0):
4646
"""
4747
self.tracker_home = tracker_home
4848
self.optimize = optimize
49+
# if set, call schema_hook after executing schema.py will get
50+
# same variables (in particular db) as schema.py main purpose is
51+
# for regression tests
52+
self.schema_hook = None
4953
self.config = configuration.CoreConfig(tracker_home)
5054
self.actions = {}
5155
self.cgi_actions = {}
@@ -106,6 +110,8 @@ def open(self, name=None):
106110
if self.optimize:
107111
# execute preloaded schema object
108112
exec(self.schema, vars)
113+
if callable (self.schema_hook):
114+
self.schema_hook(**vars)
109115
# use preloaded detectors
110116
detectors = self.detectors
111117
else:
@@ -114,6 +120,8 @@ def open(self, name=None):
114120
sys.path.insert(1, libdir)
115121
# execute the schema file
116122
self._load_python('schema.py', vars)
123+
if callable (self.schema_hook):
124+
self.schema_hook(**vars)
117125
# reload extensions and detectors
118126
for extension in self.get_extensions('extensions'):
119127
extension(self)

roundup/mailgw.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -524,9 +524,8 @@ def verify_signature(self, author):
524524

525525
class MailGW:
526526

527-
def __init__(self, instance, db, arguments=()):
527+
def __init__(self, instance, arguments=()):
528528
self.instance = instance
529-
self.db = db
530529
self.arguments = arguments
531530
self.default_class = None
532531
for option, value in self.arguments:
@@ -802,6 +801,21 @@ def handle_message(self, message):
802801
803802
Parse the message as per the module docstring.
804803
'''
804+
# get database handle for handling one email
805+
self.db = self.instance.open ('admin')
806+
try:
807+
return self._handle_message (message)
808+
finally:
809+
self.db.close()
810+
811+
def _handle_message(self, message):
812+
''' message - a Message instance
813+
814+
Parse the message as per the module docstring.
815+
816+
The implementation expects an opened database and a try/finally
817+
that closes the database.
818+
'''
805819
# detect loops
806820
if message.getheader('x-roundup-loop', ''):
807821
raise IgnoreLoop

roundup/scripts/roundup_mailgw.py

Lines changed: 56 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -138,73 +138,65 @@ def main(argv):
138138
import roundup.instance
139139
instance = roundup.instance.open(instance_home)
140140

141-
# get a mail handler
142-
db = instance.open('admin')
141+
if hasattr(instance, 'MailGW'):
142+
handler = instance.MailGW(instance, optionsList)
143+
else:
144+
handler = mailgw.MailGW(instance, optionsList)
145+
146+
# if there's no more arguments, read a single message from stdin
147+
if len(args) == 1:
148+
return handler.do_pipe()
149+
150+
# otherwise, figure what sort of mail source to handle
151+
if len(args) < 3:
152+
return usage(argv, _('Error: not enough source specification information'))
153+
source, specification = args[1:3]
154+
155+
# time out net connections after a minute if we can
156+
if source not in ('mailbox', 'imaps'):
157+
if hasattr(socket, 'setdefaulttimeout'):
158+
socket.setdefaulttimeout(60)
159+
160+
if source == 'mailbox':
161+
return handler.do_mailbox(specification)
143162

144-
# now wrap in try/finally so we always close the database
163+
# the source will be a network server, so obtain the credentials to
164+
# use in connecting to the server
145165
try:
146-
if hasattr(instance, 'MailGW'):
147-
handler = instance.MailGW(instance, db, optionsList)
166+
# attempt to obtain credentials from a ~/.netrc file
167+
authenticator = netrc.netrc().authenticators(specification)
168+
username = authenticator[0]
169+
password = authenticator[2]
170+
server = specification
171+
# IOError if no ~/.netrc file, TypeError if the hostname
172+
# not found in the ~/.netrc file:
173+
except (IOError, TypeError):
174+
match = re.match(r'((?P<user>[^:]+)(:(?P<pass>.+))?@)?(?P<server>.+)',
175+
specification)
176+
if match:
177+
username = match.group('user')
178+
password = match.group('pass')
179+
server = match.group('server')
148180
else:
149-
handler = mailgw.MailGW(instance, db, optionsList)
150-
151-
# if there's no more arguments, read a single message from stdin
152-
if len(args) == 1:
153-
return handler.do_pipe()
154-
155-
# otherwise, figure what sort of mail source to handle
156-
if len(args) < 3:
157-
return usage(argv, _('Error: not enough source specification information'))
158-
source, specification = args[1:3]
159-
160-
# time out net connections after a minute if we can
161-
if source not in ('mailbox', 'imaps'):
162-
if hasattr(socket, 'setdefaulttimeout'):
163-
socket.setdefaulttimeout(60)
164-
165-
if source == 'mailbox':
166-
return handler.do_mailbox(specification)
167-
168-
# the source will be a network server, so obtain the credentials to
169-
# use in connecting to the server
170-
try:
171-
# attempt to obtain credentials from a ~/.netrc file
172-
authenticator = netrc.netrc().authenticators(specification)
173-
username = authenticator[0]
174-
password = authenticator[2]
175-
server = specification
176-
# IOError if no ~/.netrc file, TypeError if the hostname
177-
# not found in the ~/.netrc file:
178-
except (IOError, TypeError):
179-
match = re.match(r'((?P<user>[^:]+)(:(?P<pass>.+))?@)?(?P<server>.+)',
180-
specification)
181-
if match:
182-
username = match.group('user')
183-
password = match.group('pass')
184-
server = match.group('server')
185-
else:
186-
return usage(argv, _('Error: %s specification not valid') % source)
187-
188-
# now invoke the mailgw handler depending on the server handler requested
189-
if source.startswith('pop'):
190-
ssl = source.endswith('s')
191-
if ssl and sys.version_info<(2,4):
192-
return usage(argv, _('Error: a later version of python is required'))
193-
return handler.do_pop(server, username, password, ssl)
194-
elif source == 'apop':
195-
return handler.do_apop(server, username, password)
196-
elif source.startswith('imap'):
197-
ssl = source.endswith('s')
198-
mailbox = ''
199-
if len(args) > 3:
200-
mailbox = args[3]
201-
return handler.do_imap(server, username, password, mailbox, ssl)
202-
203-
return usage(argv, _('Error: The source must be either "mailbox",'
204-
' "pop", "pops", "apop", "imap" or "imaps"'))
205-
finally:
206-
# handler might have closed the initial db and opened a new one
207-
handler.db.close()
181+
return usage(argv, _('Error: %s specification not valid') % source)
182+
183+
# now invoke the mailgw handler depending on the server handler requested
184+
if source.startswith('pop'):
185+
ssl = source.endswith('s')
186+
if ssl and sys.version_info<(2,4):
187+
return usage(argv, _('Error: a later version of python is required'))
188+
return handler.do_pop(server, username, password, ssl)
189+
elif source == 'apop':
190+
return handler.do_apop(server, username, password)
191+
elif source.startswith('imap'):
192+
ssl = source.endswith('s')
193+
mailbox = ''
194+
if len(args) > 3:
195+
mailbox = args[3]
196+
return handler.do_imap(server, username, password, mailbox, ssl)
197+
198+
return usage(argv, _('Error: The source must be either "mailbox",'
199+
' "pop", "pops", "apop", "imap" or "imaps"'))
208200

209201
def run():
210202
sys.exit(main(sys.argv))

test/test_mailgw.py

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,11 @@ def setUp(self):
116116
address='[email protected]', realname='Bork, Chef', roles='User')
117117
self.richard_id = self.db.user.create(username='richard',
118118
address='[email protected]', roles='User')
119-
self.mary_id = self.db.user.create(username='mary', address='[email protected]',
120-
roles='User', realname='Contrary, Mary')
121-
self.john_id = self.db.user.create(username='john', address='[email protected]',
122-
alternate_addresses='jondoe@test.test\n[email protected]', roles='User',
123-
realname='John Doe')
119+
self.mary_id = self.db.user.create(username='mary',
120+
address='[email protected]', roles='User', realname='Contrary, Mary')
121+
self.john_id = self.db.user.create(username='john',
122+
address='john@test.test', roles='User', realname='John Doe',
123+
alternate_addresses='[email protected]\n[email protected]')
124124

125125
def tearDown(self):
126126
if os.path.exists(SENDMAILDEBUG):
@@ -132,11 +132,15 @@ def tearDown(self):
132132
if error.errno not in (errno.ENOENT, errno.ESRCH): raise
133133

134134
def _handle_mail(self, message):
135-
handler = self.instance.MailGW(self.instance, self.db)
135+
# handler will open a new db handle. On single-threaded
136+
# databases we'll have to close our current connection
137+
self.db.commit()
138+
self.db.close()
139+
handler = self.instance.MailGW(self.instance)
136140
handler.trapExceptions = 0
137141
ret = handler.main(StringIO(message))
138-
# handler can close the db on us and open a new one
139-
self.db = handler.db
142+
# handler had its own database, open new connection
143+
self.db = self.instance.open('admin')
140144
return ret
141145

142146
def _get_mail(self):
@@ -549,6 +553,11 @@ def testPropertyChangeOnly(self):
549553
self.doNewIssue()
550554
oldvalues = self.db.getnode('issue', '1').copy()
551555
oldvalues['assignedto'] = None
556+
# reconstruct old behaviour: This would reuse the
557+
# database-handle from the doNewIssue above which has committed
558+
# as user "Chef". So we close and reopen the db as that user.
559+
self.db.close()
560+
self.db = self.instance.open('Chef')
552561
self.db.issue.set('1', assignedto=self.chef_id)
553562
self.db.commit()
554563
self.db.issue.nosymessage('1', None, oldvalues)
@@ -990,11 +999,6 @@ def testNosyRemove(self):
990999
assert not os.path.exists(SENDMAILDEBUG)
9911000

9921001
def testNewUserAuthor(self):
993-
# first without the permission
994-
# heh... just ignore the API for a second ;)
995-
self.db.security.role['anonymous'].permissions=[]
996-
anonid = self.db.user.lookup('anonymous')
997-
self.db.user.set(anonid, roles='Anonymous')
9981002

9991003
l = self.db.user.list()
10001004
l.sort()
@@ -1007,6 +1011,12 @@ def testNewUserAuthor(self):
10071011
10081012
This is a test submission of a new issue.
10091013
'''
1014+
def hook (db, **kw):
1015+
''' set up callback for db open '''
1016+
db.security.role['anonymous'].permissions=[]
1017+
anonid = db.user.lookup('anonymous')
1018+
db.user.set(anonid, roles='Anonymous')
1019+
self.instance.schema_hook = hook
10101020
try:
10111021
self._handle_mail(message)
10121022
except Unauthorized, value:
@@ -1021,15 +1031,17 @@ def testNewUserAuthor(self):
10211031
else:
10221032
raise AssertionError, "Unathorized not raised when handling mail"
10231033

1024-
# Add Web Access role to anonymous, and try again to make sure
1025-
# we get a "please register at:" message this time.
1026-
p = [
1027-
self.db.security.getPermission('Create', 'user'),
1028-
self.db.security.getPermission('Web Access', None),
1029-
]
1030-
1031-
self.db.security.role['anonymous'].permissions=p
10321034

1035+
def hook (db, **kw):
1036+
''' set up callback for db open '''
1037+
# Add Web Access role to anonymous, and try again to make sure
1038+
# we get a "please register at:" message this time.
1039+
p = [
1040+
db.security.getPermission('Create', 'user'),
1041+
db.security.getPermission('Web Access', None),
1042+
]
1043+
db.security.role['anonymous'].permissions=p
1044+
self.instance.schema_hook = hook
10331045
try:
10341046
self._handle_mail(message)
10351047
except Unauthorized, value:
@@ -1053,12 +1065,15 @@ def testNewUserAuthor(self):
10531065
m.sort()
10541066
self.assertEqual(l, m)
10551067

1056-
# now with the permission
1057-
p = [
1058-
self.db.security.getPermission('Create', 'user'),
1059-
self.db.security.getPermission('Email Access', None),
1060-
]
1061-
self.db.security.role['anonymous'].permissions=p
1068+
def hook (db, **kw):
1069+
''' set up callback for db open '''
1070+
# now with the permission
1071+
p = [
1072+
db.security.getPermission('Create', 'user'),
1073+
db.security.getPermission('Email Access', None),
1074+
]
1075+
db.security.role['anonymous'].permissions=p
1076+
self.instance.schema_hook = hook
10621077
self._handle_mail(message)
10631078
m = self.db.user.list()
10641079
m.sort()
@@ -1076,11 +1091,14 @@ def testNewUserAuthorHighBit(self):
10761091
10771092
This is a test submission of a new issue.
10781093
'''
1079-
p = [
1080-
self.db.security.getPermission('Create', 'user'),
1081-
self.db.security.getPermission('Email Access', None),
1082-
]
1083-
self.db.security.role['anonymous'].permissions=p
1094+
def hook (db, **kw):
1095+
''' set up callback for db open '''
1096+
p = [
1097+
db.security.getPermission('Create', 'user'),
1098+
db.security.getPermission('Email Access', None),
1099+
]
1100+
db.security.role['anonymous'].permissions=p
1101+
self.instance.schema_hook = hook
10841102
self._handle_mail(message)
10851103
m = set(self.db.user.list())
10861104
new = list(m - l)[0]

0 commit comments

Comments
 (0)