What I read about the commit/rollback "wrapping" that web2py does, it doesn't seem to solve my problem.Let me explain.I have 3 tables (sn_counter, wo_counter and wo).The procedure I must do is the following:1. Get the record from wo_counter table a extract the counter value (read operation), let's suppose I get the number 10.
row = db(query).select(db.wo_counter.counter, for_update=True).first()
row = db.wo_counter(year_=request.now.year)
row = db(db.wo_counter.year_ == request.now.year).select(
db.wo_counter.year_, db.wo_counter.last_assigned, for_update=True).first()
File "C:\web2py\applications\ajc\controllers/open_wo.py", line 319, in new_par
t2
db.wo_counter.year_, db.wo_counter.last_assigned, for_update=True).first()
File "C:\web2py\gluon\packages\dal\pydal\objects.py", line 2378, in select
return adapter.select(self.query, fields, attributes)
File "C:\web2py\gluon\packages\dal\pydal\adapters\sqlite.py", line 82, in sele
ct
return super(SQLite, self).select(query, fields, attributes)
File "C:\web2py\gluon\packages\dal\pydal\adapters\base.py", line 763, in selec
t
return self._select_aux(sql, fields, attributes, colnames)
File "C:\web2py\gluon\packages\dal\pydal\adapters\base.py", line 719, in _sele
ct_aux
rows = self._select_aux_execute(sql)
File "C:\web2py\gluon\packages\dal\pydal\adapters\base.py", line 713, in _sele
ct_aux_execute
self.execute(sql)
File "C:\web2py\gluon\packages\dal\pydal\adapters\__init__.py", line 66, in wr
ap
return f(*args, **kwargs)
File "C:\web2py\gluon\packages\dal\pydal\adapters\base.py", line 413, in execu
te
rv = self.cursor.execute(command, *args[1:], **kwargs)
sqlite3.OperationalError: near "FOR": syntax error
sn_counter_id = db.equipment(session.new_form_part1.equipment_id).sn_counter_id
sn_counter_id = db(db.equipment.id == session.new_form_part1.equipment_id).select(db.equipment.sn_counter_id, for_update=True).first().sn_counter_id
ERROR:web2py:Traceback (most recent call last):
File "C:\web2py\gluon\restricted.py", line 219, in restricted
exec(ccode, environment)
File "C:\web2py\applications\ajc\controllers/open_wo.py", line 708, in <module
>
File "C:\web2py\gluon\globals.py", line 421, in <lambda>
self._caller = lambda f: f()
File "C:\web2py\gluon\tools.py", line 3868, in f
return action(*a, **b)
File "C:\web2py\applications\ajc\controllers/open_wo.py", line 331, in new_par
t2
).select(db.equipment.sn_counter_id, for_update=True).first().sn_counter_id
File "C:\web2py\gluon\packages\dal\pydal\objects.py", line 2378, in select
return adapter.select(self.query, fields, attributes)
File "C:\web2py\gluon\packages\dal\pydal\adapters\sqlite.py", line 81, in sele
ct
self.execute('BEGIN IMMEDIATE TRANSACTION;')
File "C:\web2py\gluon\packages\dal\pydal\adapters\__init__.py", line 66, in wr
ap
return f(*args, **kwargs)
File "C:\web2py\gluon\packages\dal\pydal\adapters\base.py", line 413, in execu
te
rv = self.cursor.execute(command, *args[1:], **kwargs)
sqlite3.OperationalError: cannot start a transaction within a transaction
db.executesql('BEGIN IMMEDIATE TRANSACTION')
Looks like a bug was introduced. SQLITE does not support the SQL "FOR UPDATE" command, so the DAL simulates it by executing "BEGIN IMMEDIATE TRANSACTION", which simply starts a transaction and locks the database. Originally, the SQLite adapter did not add the "FOR UPDATE" to queries when for_update=True, but it looks like in the refactor, that was overlooked, and SQLite now simply uses the same code as other SQL adapters. That should be fixed.When you added for_update=True to the first query, no transaction had been started at that point, so the "BEGIN IMMEDIATE TRANSACTION" command did not result in an error, and instead the "FOR UPDATE" command (which is run after) created the error. When you added for_update=True to the second query, a transaction had already started, so the "BEGIN IMMEDIATE TRANSACTION" command triggered an error (before the "FOR UPDATE" command even got executed).If for_update were working properly, you could use it on your first query and simply leave it out of the second query, as it would be unnecessary. However, since for_update is currently broken for SQLite, you can instead do the following -- right before the first select:
db.execute_sql('BEGIN IMMEDIATE TRANSACTION')
João,No, SQLite transactions are not only ACID, the Isolation is Serializable, which means even if the execute concurrently,
the result should be same as if they executed in series (on after the other). What @Anthony describes should not be
necessary, since you already in a transaction.
@João, you are programming yourself into a corner. No matter what you do, you’ll keep running into more problems. The reason for this is because you’re trying to do atomic operations at the application level. You simply can’t do that.
If you check whether or not something is locked at the application level, and then take some action, you risk making a mistake. What happens if the lock state changes between when you checked, and the next operation in your program? You will take the wrong action. By definition, *anything* can happen between *any* two operations in your program. Another process could change the state:
1: if databaseislocked() then:
2: doseomthing()
3: else:
4: dosomethingelse()
What if the database state changes between 1 and 2 or 4? Perhaps you think if it ends up being locked, and you try to lock it again, then your subsequent attempt will fail. But then what? Do you go into to a loop trying to lock it? Now you can get blocked waiting for a lock, or worse, get into a deadlock situation, with two processes blocked, each waiting for the other one to free up something.
So you say you created a flag? Now you’ve just pushed the problem back onto another application-level ‘lock’ that can’t possibly be relied upon (unless you’re using a low-level locking mechanism, and even then this can be tricky).
My understanding of what I read between you and Anthony is this:
a) SQLite defers starting the actual transaction until the first write, so your initial code wasn’t actually wrapped in a transaction at all.
b) As a result, your tests failed. But this doesn’t mean transactions don’t work in SQLite, or in general.
c) Anthony tried to force the transaction open with the FOR UPDATE clause, but that doesn’t work in SQLite.
d) So the solution for SQLite is to explicitly start the transaction before reading and/or writing anything.
So all you have to do is
db.executesql('BEGIN IMMEDIATE TRANSACTION')
before you start reading and updating your counters, and the work order, and then
db.commit()
as soon as you’re done. You *do not* need to check if the database is already locked. SQLite will handle this during the commit process. It will guarantee that any two operations running concurrently are isolated from each other—each process will see a consistent view of the database, and the final result will be the same as if they had executed one after the other (serially). If they collide, SQLite will abort one of them, and restart it. For this to work, it’s important that:
This process will be more scalable, and not require locking the whole database, on a multi-user MVCC database like Postgres. This would also avoid the silliness with the SQLite adapter bugs. We use Postgres with Nginx and Web2py in production systems with lots of contention and it works beautifully. But this really only matters if you have lots of users. SQLite is very solid for smaller, single-server deployments.
John
--
Resources:
- http://web2py.com
- http://web2py.com/book (Documentation)
- http://github.com/web2py/web2py (Source code)
- https://code.google.com/p/web2py/issues/list (Report Issues)
---
You received this message because you are subscribed to the Google Groups "web2py-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to
web2py+un...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
c) Anthony tried to force the transaction open with the FOR UPDATE clause, but that doesn’t work in SQLite.
while True:
try:
# SQLite only does database lock.
db.executesql('BEGIN IMMEDIATE TRANSACTION')
break
except sqlite3.OperationalError:
sleep(0.5)
# Do update_record or insert in one or more tables.
db.commit()
db.executesql('BEGIN IMMEDIATE TRANSACTION')
before you start reading and updating your counters, and the work order, and then
db.commit()
is what I'm using after Anthony's suggestion.