Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
Browse files

Always abort transactions for HTTP requests that end in an exception …

…being raised, even if that exception is an expected HTTPError that indicates some kind of known error.

git-svn-id: https://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk@9970 e27351fd-9f3e-4f54-a53b-843176b1656c
  • Loading branch information
glyph committed Oct 22, 2012
1 parent ebaf317 commit 65eecffe4250fab05622dd12dacc653909fa84b8
@@ -34,6 +34,7 @@

from twext.web2 import iweb, http, server, responsecode

from twisted.internet.defer import maybeDeferred
class RenderMixin(object):
"""
Mix-in class for L{iweb.IResource} which provides a dispatch mechanism for
@@ -106,7 +107,18 @@ def renderHTTP(self, request):
returnValue(response)

yield self.checkPreconditions(request)
returnValue((yield method(request)))
result = maybeDeferred(method, request)
result.addErrback(self.methodRaisedException)
returnValue((yield result))


def methodRaisedException(self, failure):
"""
An C{http_METHOD} method raised an exception; this is an errback for
that exception. By default, simply propagate the error up; subclasses
may override this for top-level exception handling.
"""
return failure


def http_OPTIONS(self, request):
@@ -106,7 +106,6 @@ def http_MKCALENDAR(self, request):

if got_an_error:
# Force a transaction error and proper clean-up
self.transactionError()
errors.error()
raise HTTPError(MultiStatusResponse([errors.response()]))

@@ -182,7 +182,6 @@ def http_MKCOL(self, request):

if got_an_error:
# Clean up
self.transactionError()
errors.error()
raise HTTPError(Response(
code=responsecode.FORBIDDEN,
@@ -318,8 +318,14 @@ def propagateTransaction(self, otherResource):
otherResource.associateWithTransaction(self._associatedTransaction)


def transactionError(self):
def methodRaisedException(self, failure):
"""
An C{http_METHOD} method raised an exception. Any type of exception,
including those that result in perfectly valid HTTP responses, should
abort the transaction.
"""
self._transactionError = True
return super(CalDAVResource, self).methodRaisedException(failure)


@inlineCallbacks
@@ -1611,8 +1611,6 @@ def http_PUT(self, request):
yield readStream(request.stream, t.write)
except Exception, e:
log.error("Unable to store attachment: %s" % (e,))
# Signal to abort in twistedcaldav.resource.CalDAVResource.RenderHTTP
self.transactionError()
raise HTTPError(SERVICE_UNAVAILABLE)

try:
@@ -53,6 +53,7 @@
from twistedcaldav.directory.test.test_xmlfile import XMLFileBase
from txdav.caldav.icalendarstore import ICalendarHome
from txdav.carddav.iaddressbookstore import IAddressBookHome

from txdav.caldav.datastore.file import Calendar


@@ -108,6 +109,7 @@ def populateOneObject(self, objectName, objectText):
@param objectName: The name of a calendar object.
@type objectName: str
@param objectText: Some iCalendar text to populate it with.
@type objectText: str
"""
@@ -167,7 +169,6 @@ def getResource(self, path, method='GET', user=None):
@param path: the path from the root of the site (not starting with a
slash)
@type path: C{str}
@param method: the HTTP method to initialize the request with.
@@ -266,8 +267,8 @@ def test_simpleRequest(self):

def test_createStore(self):
"""
Creating a DirectoryCalendarHomeProvisioningResource will create a paired
CalendarStore.
Creating a DirectoryCalendarHomeProvisioningResource will create a
paired CalendarStore.
"""
assertProvides(self, IDataStore, self.calendarCollection._newStore)

@@ -518,6 +519,19 @@ def test_lookupNewAddressBookObject(self):
frozenset([self.principalsResource]))


@inlineCallbacks
def assertCalendarEmpty(self, user, calendarName="calendar"):
"""
Assert that a user's calendar is empty (their default calendar by default).
"""
txn = self.calendarStore.newTransaction()
self.addCleanup(txn.commit)
home = yield txn.calendarHomeWithUID(user, create=True)
cal = yield home.calendarWithName(calendarName)
objects = yield cal.calendarObjects()
self.assertEquals(len(objects), 0)



class DatabaseWrappingTests(WrappingTests):

@@ -531,4 +545,73 @@ def createDataStore(self):
return self.calendarStore


@inlineCallbacks
def test_invalidCalendarPUT(self):
"""
Exceeding quota on an attachment returns an HTTP error code.
"""
# yield self.populateOneObject("1.ics", test_event_text)
@inlineCallbacks
def putEvt(txt):
calendarObject = yield self.getResource(
"/calendars/users/wsanchez/calendar/1.ics",
"PUT", "wsanchez"
)
self.requestUnderTest.stream = MemoryStream(txt)
returnValue(
((yield calendarObject.renderHTTP(self.requestUnderTest)),
self.requestUnderTest)
)
# see twistedcaldav/directory/test/accounts.xml
wsanchez = '6423F94A-6B76-4A3A-815B-D52CFD77935D'
cdaboo = '5A985493-EE2C-4665-94CF-4DFEA3A89500'
eventTemplate="""\
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
PRODID:-//Example Inc.//Example Calendar//EN
VERSION:2.0
BEGIN:VEVENT
UID:20060110T231240Z-4011c71-187-6f73
ORGANIZER:urn:uuid:{wsanchez}
ATTENDEE:urn:uuid:{wsanchez}
DTSTART:20110101T050000Z
DTSTAMP:20110309T185105Z
DURATION:PT1H
SUMMARY:Test
RRULE:FREQ=DAILY;COUNT=2
END:VEVENT
BEGIN:VEVENT
UID:20060110T231240Z-4011c71-187-6f73
RECURRENCE-ID:20110102T050000Z
ORGANIZER:urn:uuid:{wsanchez}
ATTENDEE:urn:uuid:{wsanchez}
ATTENDEE:urn:uuid:{cdaboo}
DTSTART:20110102T050000Z
DTSTAMP:20110309T185105Z
DURATION:PT1H
SUMMARY:Test
END:VEVENT{0}
END:VCALENDAR
"""
CR = "\n"
CRLF = "\r\n"
#validEvent = eventTemplate.format("", wsanchez=wsanchez, cdaboo=cdaboo).replace(CR, CRLF)
invalidInstance = """
BEGIN:VEVENT
UID:20060110T231240Z-4011c71-187-6f73
RECURRENCE-ID:20110110T050000Z
ORGANIZER:urn:uuid:{wsanchez}
ATTENDEE:urn:uuid:{wsanchez}
DTSTART:20110110T050000Z
DTSTAMP:20110309T185105Z
DURATION:PT1H
SUMMARY:Test
END:VEVENT""".format(wsanchez=wsanchez, cdaboo=cdaboo)
#txn = self.requestUnderTest._newStoreTransaction
invalidEvent = eventTemplate.format(invalidInstance, wsanchez=wsanchez, cdaboo=cdaboo).replace(CR, CRLF)
resp2, rsrc2 = yield putEvt(invalidEvent)
self.requestUnderTest = None
yield self.assertCalendarEmpty(wsanchez)
yield self.assertCalendarEmpty(cdaboo)


@@ -30,6 +30,8 @@
Select, Parameter, Update, Insert, TableSyntax, Delete)

from txdav.xml.parser import WebDAVDocument
from txdav.common.icommondatastore import AllRetriesFailed
from twext.python.log import LoggingMixIn
from txdav.common.datastore.sql_tables import schema
from txdav.base.propertystore.base import (AbstractPropertyStore,
PropertyName, validKey)
@@ -39,7 +41,7 @@

prop = schema.RESOURCE_PROPERTY

class PropertyStore(AbstractPropertyStore):
class PropertyStore(AbstractPropertyStore, LoggingMixIn):

_cacher = Memcacher("SQL.props", pickle=True, key_normalization=False)

@@ -255,7 +257,10 @@ def trySetItem(txn):
if hasattr(self, "_notifyCallback") and self._notifyCallback is not None:
self._notifyCallback()

self._txn.subtransaction(trySetItem)
def justLogIt(f):
f.trap(AllRetriesFailed)
self.log_error("setting a property failed; probably nothing.")
self._txn.subtransaction(trySetItem).addErrback(justLogIt)



@@ -751,15 +751,23 @@ def subtransaction(self, thunk, retries=1, failureOK=False):
block = self._sqlTxn.commandBlock()
sp = self._savepoint()
failuresToMaybeLog = []
def end():
block.end()
for f in failuresToMaybeLog:
# TODO: direct tests, to make sure error logging
# happens correctly in all cases.
log.err(f)
raise AllRetriesFailed()
triesLeft = retries
try:
while True:
yield sp.acquire(block)
try:
result = yield thunk(block)
except:
f = Failure()
if not failureOK:
failuresToMaybeLog.append(Failure())
failuresToMaybeLog.append(f)
yield sp.rollback(block)
if triesLeft:
triesLeft -= 1
@@ -774,12 +782,7 @@ def subtransaction(self, thunk, retries=1, failureOK=False):
block = newBlock
sp = self._savepoint()
else:
block.end()
for f in failuresToMaybeLog:
# TODO: direct tests, to make sure error logging
# happens correctly in all cases.
log.err(f)
raise AllRetriesFailed()
end()
else:
yield sp.release(block)
block.end()
@@ -790,9 +793,10 @@ def subtransaction(self, thunk, retries=1, failureOK=False):
# and only that case - acquire() or release() or commandBlock() may
# raise an AlreadyFinishedError (either synchronously, or in the
# case of the first two, possibly asynchronously as well). We can
# safely ignore this, because it can't have any real effect; our
# caller shouldn't be paying attention anyway.
block.end()
# safely ignore this error, because it can't have any effect on what
# gets written; our caller will just get told that it failed in a
# way they have to be prepared for anyway.
end()


@inlineCallbacks
@@ -24,6 +24,7 @@
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.task import Clock
from twisted.trial.unittest import TestCase
from twisted.internet.defer import Deferred

from txdav.common.datastore.sql import log, CommonStoreTransactionMonitor,\
CommonHome, CommonHomeChild, ECALENDARTYPE
@@ -227,7 +228,8 @@ def _test(subtxn):
@inlineCallbacks
def test_subtransactionFailSomeRetries(self):
"""
txn.subtransaction runs loop three times when all fail and two retries requested.
txn.subtransaction runs loop three times when all fail and two retries
requested.
"""

txn = self.transactionUnderTest()
@@ -251,6 +253,34 @@ def _test(subtxn):
self.fail("AllRetriesFailed not raised")
self.assertEqual(ctr[0], 3)


@inlineCallbacks
def test_subtransactionAbortOuterTransaction(self):
"""
If an outer transaction that is holding a subtransaction open is
aborted, then the L{Deferred} returned by L{subtransaction} raises
L{AllRetriesFailed}.
"""
txn = self.transactionUnderTest()
cs = schema.CALENDARSERVER
waitAMoment = Deferred()
@inlineCallbacks
def later(subtxn):
yield waitAMoment
value = yield Select([cs.VALUE], From=cs).on(subtxn)
returnValue(value)
started = txn.subtransaction(later)
txn.abort()
waitAMoment.callback(True)
try:
result = yield started
except AllRetriesFailed:
pass
else:
self.fail("AllRetriesFailed not raised, %r returned instead" %
(result,))


@inlineCallbacks
def test_changeRevision(self):
"""

0 comments on commit 65eecff

Please sign in to comment.
You can’t perform that action at this time.