I need to use transactions in laravel 8 and I'm using the Jenssegers-laravel-mongodb package. The beginTransaction method on the DB facade doesn't work for mongoDB, so I tried the following method:
$session = DB::connection('mongodb')->getMongoClient()->startSession();
$session->startTransaction();
try {
$task = Task::create($request->only('reminder', 'priority'));
$activity = $task->activity()->create($request->only('topic', 'description', 'creator_id'));
$session->commitTransaction();
} catch (\Exception $exception) {
$session->abortTransaction();
}
When a query fails in the try block, it throws the exception but does not roll back the transactions. I also found a similar question here: Laravel mongodb transactions does not rollback. I tried the given solution in the single answer, but it returns the following error:
{message: "Unsupported driver [].", exception: "InvalidArgumentException"}
Related
doctine-odm does not support native strategy for multi document transaction support as per this document
However, does anyone have figured out a workaround?
According to mongodb, we need to create a session and pass it to every query in the transaction to take control over full commit rollback. this implementation does the same thing but it does not work. I get error while passing session to the ->flush() method.
Error
The \"writeConcern\" option cannot be specified within a transaction. Instead, specify it when starting the transaction.
Try:
$callback = function (Session $session) { /** your code here */ }
$session = $client->startSession();
with_transaction($session, $callback, [
'readConcern' => new ReadConcern(ReadConcern::MAJORITY),
'writeConcern' => new WriteConcern(WriteConcern::MAJORITY, 1000),
'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY),
]);
I don't understand how transaction retry works in sequelize.
I am using managed transaction, though I also tried with unmanaged with same outcome
await sequelize.transaction({ isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.REPEATABLE_READ}, async (t) => {
user = await User.findOne({
where: { id: authenticatedUser.id },
transaction: t,
lock: t.LOCK.UPDATE,
});
user.activationCodeCreatedAt = new Date();
user.activationCode = activationCode;
await user.save({transaction: t});
});
Now if I run this when the row is already locked, I am getting
DatabaseError [SequelizeDatabaseError]: could not serialize access due to concurrent update
which is normal. This is my retry configuration:
retry: {
match: [
/concurrent update/,
],
max: 5
}
I want at this point sequelize to retry this transaction. But instead I see that right after SELECT... FOR UPDATE it's calling again SELECT... FOR UPDATE. This is causing another error
DatabaseError [SequelizeDatabaseError]: current transaction is aborted, commands ignored until end of transaction block
How to use sequelizes internal retry mechanism to retry the whole transaction?
Manual retry workaround function
Since Sequelize devs simply aren't interested in patching this for some reason after many years, here's my workaround:
async function transactionWithRetry(sequelize, transactionArgs, cb) {
let done = false
while (!done) {
try {
await sequelize.transaction(transactionArgs, cb)
done = true
} catch (e) {
if (
sequelize.options.dialect === 'postgres' &&
e instanceof Sequelize.DatabaseError &&
e.original.code === '40001'
) {
await sequelize.query(`ROLLBACK`)
} else {
// Error that we don't know how to handle.
throw e;
}
}
}
}
Sample usage:
const { Transaction } = require('sequelize');
await transactionWithRetry(sequelize,
{ isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE },
async t => {
const rows = await sequelize.models.MyInt.findAll({ transaction: t })
await sequelize.models.MyInt.update({ i: newI }, { where: {}, transaction: t })
}
)
The error code 40001 is documented at: https://www.postgresql.org/docs/13/errcodes-appendix.html and it's the only one I've managed to observe so far on Serialization failures: What are the conditions for encountering a serialization failure? Let me know if you find any others that should be auto looped and I'll patch them in.
Here's a full runnable test for it which seems to indicate that it is working fine: https://github.com/cirosantilli/cirosantilli.github.io/blob/dbb2ec61bdee17d42fe7e915823df37c4af2da25/sequelize/parallel_select_and_update.js
Tested on:
"pg": "8.5.1",
"pg-hstore": "2.3.3",
"sequelize": "6.5.1",
PostgreSQL 13.5, Ubuntu 21.10.
Infinite list of related requests
https://github.com/sequelize/sequelize/issues/1478 from 2014. Original issue was MySQL but thread diverged everywhere.
https://github.com/sequelize/sequelize/issues/8294 from 2017. Also asked on Stack Overflow, but got Tumbleweed badge and the question appears to have been auto deleted, can't find it on search. Mentions MySQL. Is a bit of a mess, as it also includes connection errors, which are not clear retries such as PostgreSQL serialization failures.
https://github.com/sequelize/sequelize/issues/12608 mentions Postgres
https://github.com/sequelize/sequelize/issues/13380 by the OP of this question
Meaning of current transaction is aborted, commands ignored until end of transaction block
The error is pretty explicit, but just to clarify to other PostgreSQL newbies: in PostgreSQL, when you get a failure in the middle of a transaction, Postgres just auto-errors any following queries until a ROLLBACK or COMMIT happens and ends the transaction.
The DB client code is then supposed to notice that just re-run the transaction.
These errors are therefore benign, and ideally Sequelize should not raise on them. Those errors are actually expected when using ISOLATION LEVEL SERIALIZABLE and ISOLATION LEVEL REPEATABLE READ, and prevent concurrent errors from happening.
But unfortunately sequelize does raise them just like any other errors, so it is inevitable for our workaround to have a while/try/catch loop.
i have a grails job which is scheduled to run at every night, to update stats of all user which are firstOrderDate, lastOrderDate and totalOrders.
Have a look at the code.
void updateOrderStatsForAllUsers(DateTime date) {
List<Order> usersByOrders = Delivery.findAllByDeliveryDateAndStatus(date, "DELIVERED")*.order
List<User> customers = usersByOrders*.customer.unique()
for (User u in customers) {
List<Order> orders = new ArrayList<Order>();
orders = u.orders?.findAll { it.status.equals("DELIVERED") }?.sort { it?.dateCreated }
if (orders?.size() > 0) {
u.firstOrderDate = orders?.first()?.dateCreated
u.lastOrderDate = orders?.last()?.dateCreated
u.totalOrders = orders.size()
u.save(flush: true)
}
}
}
and the job that runs this code is
def execute(){
long jobStartTime = System.currentTimeMillis()
emailService.sendJobStatusEmail(JOB_NAME, "STARTED", 0, null)
try {
// Daily job for updating user orders
DateTime yesterday = new DateTime().withZone(DateTimeZone.getDefault()).withTimeAtStartOfDay().minusDays(1)
userService.updateOrderStatsForAllUsers(yesterday)
emailService.sendJobStatusEmail(JOB_NAME, "FINISHED", jobStartTime, null)
}
catch (Exception e) {
emailService.sendJobStatusEmail(JOB_NAME, "FAILED", jobStartTime, e)
}
}
So i am sending a mail , for any exception that occurs , now the issue is i always get a failure mail of "Error: OptimisticLockingException" at u.save(). For a date i have around 400 users.
I know why optimistic locking happens , but as you can see i am not updating the same user record in loop instead , i have a list of different users and i am iterating them to update all of them. Then how come i get an optimistic locking exception at user save. help !
Optimistic locking is a hibernate error and Mango DB has nothing to do with this.
What entity is throwing optimistic locking exception is it customer or order or delivery?
How do you ensure none of these entities are getting updated elsewhere in the app when this job is running?
How do you ensure this job is getting triggered only once at a time?
Try to add some logging to see it's a repeatable issue by triggering the job again once the previous execution has completed.
More debugging may help resolve the issue.
the quartz jobs usually do not provide the TX-context for it's operations, so you should wrap your method into a transaction by hand:
def execute(){
...
User.withTransaction{ tx ->
userService.updateOrderStatsForAllUsers(yesterday)
}
....
}
I have defined a NamedNativeQuery next to a lot of NamedQueries. With NamedQueries the EntityManager.createNamedQuery(queryName) works well but when using the NamedNATIVEQuery I get an exception:
java.lang.IllegalArgumentException: NamedQuery of name: myNamedNativeQueryName not found.
The query itself works well executed in MySql Workbench but defining it as a NamedNativeQuery:
#NamedNativeQueries(value = {
#NamedNativeQuery(name = findVehicleTracksByUuidAndTimerange, query="...", resultClass=myType.class)
})
the exception occurs. I try to run the query this way:
try {
List<E> resultList = new ArrayList<E>();
javax.persistence.Query query = getEntityManager().createNamedQuery(queryName, type);
} catch(...)
What can be the reason?
As I already stated: The code from above runs well with dozens of NamedQueries...but suddenly not with the NamedNativeQuery.
Suppose "doc" is some document I want to insert into a MongoDB collection and "collection" is the collection I am inserting the document into.
I have something like the following:
try
{
WriteConcern wc = new WriteConcern();
wc.W = 1;
wc.Journal = true;
WriteConcernResult wcResult = collection.Insert(doc, wc);
if (!string.IsNullOrWhiteSpace(wcResult.ErrorMessage) || !wcResult.Ok)
{
return ErrorHandler(...);
}
else
{
return SuccessFunction(...);
}
}
catch (Exception e)
{
return e.Message;
}
Basically, if the insertion fails for any reason (other than hardware no longer working properly) I want to handle it (through the ErrorHandler function or the catch clause), while if it succeeds I want to call SuccessFunction.
My question: Is the above code sufficient for error checking purposes? In other words, will all failed insertions be caught, so that SuccessFunction is never called in those situations?
You don't even need to do any checking. collection.Insert will throw an exception if the write was not successful when you are using any write concern other than unacknowledged.
If you want to know if an error occured, you need to catch a WriteConcernException.