On 4/1/15 12:08 PM, Eric Smith wrote:
> Thanks Michael -- I appreciate how responsive you are to questions.
>
> Yes, it would have been more precise to talk about pysqlite's locking
> model rather than SQLite's. I'm hesitant to increase the lock timeout
> because I don't think I want any transactions that are that long, and
> would rather root out any that are.
there's a super easy way to achieve that, as far as just being able to
identify them; having the whole app recover gracefully I'm not as sure
about. Just set a timer for the length of time from before_flush()
to the after_commit() event in the Session, and if the timer is > N,
raise an exception. That will at least identify where these things are
happening.
>
> To address the false positives, I'm looking at a different approach
> this morning. Since the engine log seems to match precisely what is
> going on with the database (the SQL being sent), why not put in a
> logging Filter instead of using events? Then I'd have something like:
>
> def filter(self, record):
> first_word = record.msg.split(' ')[0]
> if first_word in ['INSERT', 'UPDATE', 'DELETE']:
> self._begin_timing()
> elif first_word in ['COMMIT', 'ROLLBACK']:
> self._stop_timing()
> return True
>
> Of course it only works if logging is on, but this is kind of a
> debugging tool anyway.
Again, just use events. The Core also has statement execute
(before_execute, before_cursor_execute) and transaction events (begin,
commit, rollback) that you can intercept.
>
>
> On Tuesday, March 31, 2015 at 9:38:35 PM UTC-6, Michael Bayer wrote:
>
>
>
> On 3/31/15 7:23 PM, Eric Smith wrote:
>> Environment: SQLAlchemy 0.9.7, SQLite back-end, autoflush=True,
>> mostly using scoped session, multi-platform Python 2.7
>>
>> I'm working on a multi-threaded app, but with fairly low
>> concurrency (user actions and scheduled tasks are on different
>> threads, for example). I think I have a fair understanding of
>> SQLite's locking model, the implications of having
>> autoflush=True, etc. but I still occasionally get the
>> "OperationError database is locked".
>>
>> The challenge with this error is that it is raised on a thread
>> that could be doing everything right while some other thread is
>> keeping a transaction open for too long. Plus, unless another
>> thread tries to write, the "misbehaving" thread goes mostly
>> unnoticed.
>>
>> So I thought I'd try to write a "long transaction detector" that
>> would start a timer when when a flush happens, and cancel it if a
>> commit or rollback happens. If the timer expires (after a few
>> seconds), a warning is logged with the stack trace of the flush.
>> The idea is that I can see information about the misbehaving
>> thread rather than the innocent victim, and I'll see problems
>> even without actual contention.
>>
>> While I'm still experimenting to see if this strategy will work,
>> it looks like I'm getting some false positives. It seems like
>> sometimes I get an after_flush event even when there are no
>> modifications to the database.
> A flush can proceed when objects are marked as dirty, but in some
> cases it turns out the changes on those objects are not net
> changes; e.g. an object attribute was set to a new value that
> matches the old one. that's the case where you might see a flush
> that doesn't actually do anything.
>
> As far as database is locked, I'd just increase the timeout
> (Pysqlite setting, defaults to 5 seconds), so that the locking
> acts just like any other mutex that you'd place into your
> application.
>
> As far as "SQLite's locking model", note that Pysqlite has
> extremely specific behavior here which makes the locking model
> much more forgiving. It only locks the database when the SQL on
> the current transaction turns into DML (e.g. INSERT / UPDATE /
> DELETE). So it should not be hard to write the app such that
> things that actually emit DML aren't taking up that much time.
> Note that when "rollback" or "commit" happens, the transaction is
> over, nothing is locked.
>
> If the app truly needs DML on multiple threads lots of the time
> and you're getting a lot of contention (even with Pysqlite's
> forgiving model), then SQLite isn't appropriate for that level of
> concurrency.
>
>
>
>> This is based on having the engine logging turned on so I can see
>> the SQL being emitted. I'll see something like:
>>
>> BEGIN (implicit)
>> SELECT
>> SELECT
>> ...
>> after_flush event
>> SELECT
>> SELECT
>> ...
>> My timer expires
>>
>> Code is below -- the class is passed as the class_ parameter to
>> sessionmaker.
>>
>> Am I doing something dumb? Is this a reasonable strategy for my
>> goals? Any ideas on the false positive?
>>
>> Thanks,
>> Eric
>>
>>
>>
>> class LongTransactionDetector(sqlalchemy.orm.Session):
>>
>> TIMEOUT = 3
>>
>> def __init__(self, *args, **kwargs):
>> super(LongTransactionDetector, self).__init__(*args,
>> **kwargs)
>> self._timer = None
>> self._stack = None
>> self._flush_time = None
>> event.listen(self, 'after_flush', self._after_flush)
>> event.listen(self, 'after_commit', self._after_commit)
>> event.listen(self, 'after_rollback', self._after_rollback)
>> event.listen(self, 'after_soft_rollback',
>> self._after_soft_rollback)
>>
>> def close(self):
>> # Calling close on a session automatically emits a
>> ROLLBACK, but we
>> # don't seem to get an event for it, so we'll just hook
>> it here.
>> super(LongTransactionDetector, self).close()
>> self._stop_timing()
>>
>> def _after_flush(self, session, flush_context):
>> if any([session.new, session.dirty, session.deleted]):
>> self._begin_timing()
>>
>> def _after_commit(self, session):
>> self._stop_timing()
>>
>> def _after_rollback(self, session):
>> self._stop_timing()
>>
>> def _after_soft_rollback(self, session, previous_transaction):
>> self._stop_timing()
>>
>> def _begin_timing(self):
>> if self._timer is None:
>> logger.debug('set transaction timer')
>> self._flush_time = datetime.datetime.now()
>> self._stack = traceback.extract_stack()
>> self._thread = threading.current_thread()
>> self._timer = threading.Timer(self.TIMEOUT,
>> self._on_timer)
>> self._timer.start()
>>
>> def _stop_timing(self):
>> if self._timer:
>> logger.debug('clear transaction timer')
>> self._timer.cancel()
>> self._timer = None
>> self._stack = None
>>
>> def _on_timer(self):
>> trace = ''.join(traceback.format_list(self._stack))
>> logger.warning('long transaction detected on {!r} {}\n{}\n{}'
>> .format(self._thread, self._flush_time,
>> trace))
>> --
>> You received this message because you are subscribed to the
>> Google Groups "sqlalchemy" group.
>> To unsubscribe from this group and stop receiving emails from it,
>> send an email to [email protected] <javascript:>.
>> To post to this group, send email to [email protected]
>> <javascript:>.
>> Visit this group at http://groups.google.com/group/sqlalchemy
>> <http://groups.google.com/group/sqlalchemy>.
>> For more options, visit https://groups.google.com/d/optout
>> <https://groups.google.com/d/optout>.
>
> --
> You received this message because you are subscribed to the Google
> Groups "sqlalchemy" group.
> To unsubscribe from this group and stop receiving emails from it, send
> an email to [email protected]
> <mailto:[email protected]>.
> To post to this group, send email to [email protected]
> <mailto:[email protected]>.
> Visit this group at http://groups.google.com/group/sqlalchemy.
> For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.