-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sequential transactions run out of order when using client extensions and middleware #18276
Comments
I think it's also worth adding that using an interactive transaction doesn't work here either, though for a different reason. With the following code, the const xprisma = prisma.$extends({
query: {
user: {
async findFirst({ args, query }) {
const result = await prisma.$transaction(async (tx) => {
await tx.$queryRawUnsafe("SET ROLE test_user");
const res = await query(args);
await tx.$queryRawUnsafe("SET ROLE none");
return res;
});
return result;
},
},
},
}); |
You can force the query to run in a transaction by "reconstructing" the prisma query like so: const result = await prisma.$transaction(async (tx) => {
await tx.$queryRawUnsafe("SET ROLE test_user");
const res = await (tx as any)[model][operation](args);
await tx.$queryRawUnsafe("SET ROLE none");
return res;
}) However, this will result in middleware running again for the reconstructed query, which is highly undesirable. |
This fixes a bug where RLS policies would not be used correctly when Yates is combined with async middleware. See prisma/prisma#18276 Signed-off-by: Lucian Buzzo <lucian.buzzo@gmail.com>
This fixes a bug where RLS policies would not be used correctly when Yates is combined with async middleware. See prisma/prisma#18276 Signed-off-by: Lucian Buzzo <lucian.buzzo@gmail.com>
We eventually solved this by using private methods of Prisma to bypass the middleware, it's not ideal, but it works for now: // The interactive transaction ensures that the queries run in the correct order
const queryResults = await prisma.$transaction(async (tx) => {
await tx.$queryRawUnsafe(`SET ROLE test_user`);
// We need to manually reconstruct the query, and attached the "secret" transaction ID.
// This ensures that the query will run inside the transaction AND that middlewares will not be re-applied
// https://github.com/prisma/prisma/blob/4.11.0/packages/client/src/runtime/getPrismaClient.ts#L1013
const txId = (tx as any)[Symbol.for("prisma.client.transaction.id")];
// See https://github.com/prisma/prisma/blob/4.11.0/packages/client/src/runtime/getPrismaClient.ts#L860
const __internalParams = (params as any).__internalParams;
const result = await prisma._executeRequest({
...__internalParams,
// The transaction data isn't included in __internalParams
transaction: {
kind: "itx",
id: txId,
},
});
// Switch role back to admin user
await tx.$queryRawUnsafe("SET ROLE none");
return result;
}); |
Also hit this; big thanks and +100 helpfulness points to @LucianBuzzo for the clear + detailed explanation. Successfully using your workaround in the last comment for now (tweaked to use the ...
// See https://github.com/prisma/prisma/blob/4.11.0/packages/client/src/runtime/getPrismaClient.ts#L860
const result = await prisma._executeRequest({
args,
clientMethod: `${model.toLowerCase()}.${operation}`,
jsModelName: model.toLowerCase(),
action: operation,
model,
transaction: {
kind: "itx",
id: txId,
},
});
... |
...neither forms of the workaround work with the prisma data proxy 😢 |
Can you open an issue for that please @andyjy? In a separate thread with all the information we might be able to debug and hopefully fix this. |
@janpio sorry, to clarify I was referring to how the workaround figured out by @LucianBuzzo using the private I did explore briefly whether something additional could be done to make it work but with the internals of how Prisma processes requests there were too many unknowns. Glad to file a separate issue or anything else that may be helpful - but given this is messing with the private / undocumented internals of Prisma I don't really consider it an issue requiring fixing per-se. It would be wonderful to have the original issue posted here by @LucianBuzzo resolved so that transactions within client extensions don't run out of order when combined with middleware that uses |
I realised it's possible to "convert" middleware to a client extension by wrapping it: function middlewareAsClientExtension(middleware: Prisma.Middleware) {
return Prisma.defineExtension(
(prisma) =>
prisma.$extends({
name: "middleware-as-client-extension",
query: {
$allModels: {
async $allOperations({ args, model, operation, query }) {
// wrap original query in middleware
return await middleware(
{
action: operation as Prisma.PrismaAction, // TODO: not exactly equivalent
args,
model,
runInTransaction: false, // TODO: fix when prisma exposes this to client extensions
dataPath: [], // TODO: don't know what this is
},
(params) => query(params.args)
);
},
},
},
}) as GenericPrismaClientExtended
);
}
// before:
// prisma.$use(myMiddleware);
// const prismaClient = prisma;
// after:
const prismaClient = prisma.$extends(middlewareAsClientExtension(myMiddleware)); In my testing this provides a temporary workaround for the original issue in a "cleaner" way - i.e. without accessing Prisma internals - and also works with the Prisma Data Proxy. |
Update: I don't think this is middleware-specific - I hit this same issue (transactions running out of order) when extending a client that had previously been extended with a no-op $allModels.$allOperations: return Prisma.defineExtension({
query: {
$allModels: {
$allOperations({ args, model, operation, query }) {
console.log(`prisma $allOperations: ${model}.${operation}`);
return query(args);
},
},
},
}); |
Partial-diagnosis of what I suspect causes the original issue: Batch transactions are implemented via
prisma/packages/client/src/runtime/utils/waitForBatch.ts Lines 51 to 56 in c91381c
My assumption here is that under typical usage, this causes each request to be started in the correct order, as expected. However - when execution of a particular request is drawn out (e.g. via middleware that calls |
@andyjy Yep that definitely looks like the culprit! |
@andyjy I've hit this issue and it doesn't seem to be specific to interactive transactions. I have a non-interactive transaction that is also running out of order when one of the queries is extended. |
@jadamduff yes! The issue affects batch transactions, not interactive transactions. (The references to interactive transactions above are attempts to find a workaround; most clearly illustrated in @LucianBuzzo's first comment under his original post. If I've written anything above that introduces confusion regarding interactive transactions please quote it and I'll try edit to clarify. Thanks!) |
Root cause of the problem: data loader batches requests in the order they come in, which in case of `$transaction` call happens to be the order in which they are specified in the array. However, depending on middlewares/extensions (and possibly other factors), it is possible that they will be deleviered to the data loader in a different order. Fortunately, we already have an index of each request in a batch as a part of request, so we can just sort the requests before sending them to the engine. Fix #18276
Root cause of the problem: data loader batches requests in the order they come in, which in case of `$transaction` call happens to be the order in which they are specified in the array. However, depending on middlewares/extensions (and possibly other factors), it is possible that they will be deleviered to the data loader in a different order. Fortunately, we already have an index of each request in a batch as a part of request, so we can just sort the requests before sending them to the engine. Fix #18276
Root cause of the problem: data loader batches requests in the order they come in, which in case of `$transaction` call happens to be the order in which they are specified in the array. However, depending on middlewares/extensions (and possibly other factors), it is possible that they will be deleviered to the data loader in a different order. Fortunately, we already have an index of each request in a batch as a part of request, so we can just sort the requests before sending them to the engine. Fix #18276
* fix(client): Fix batch order for middleware/query combo Root cause of the problem: data loader batches requests in the order they come in, which in case of `$transaction` call happens to be the order in which they are specified in the array. However, depending on middlewares/extensions (and possibly other factors), it is possible that they will be deleviered to the data loader in a different order. Fortunately, we already have an index of each request in a batch as a part of request, so we can just sort the requests before sending them to the engine. Fix #18276 * Fix process.nextTick polyifill for edge client Promise callback is actually called before next callback in the microtask queue. When locks are used, this causes batch to be flushed before all requests waiting on lock have finished.
Whoop! Awesome, thanks @SevInf |
Fix will be published with the next Prisma release. |
Did this issue pop up again? 🤔 I'm using an extended prisma client like below and my list of queries does not always run in order. Or is it that I'm using a transaction inside of my extended prisma client that is the problem? prisma.$extends({
query: {
$allModels: {
async $allOperations({ model, args, query }) {
const organizationId = getClaimedOrgId();
if (!organizationId) {
throw new Error('Organization ID not found in context');
}
const [, result] = await prisma.$transaction([
prisma.$executeRaw`SELECT set_config('app.orgId', ${`${organizationId}`}, TRUE)`,
query(args),
]);
return result;
},
},
},
}) |
Please open a new issue @adamnyberg, then we can properly react to this. Thanks. |
Bug description
When using a batch transaction inside a client query extension, if you are also using a middleware that uses
await
then the batch transaction will run out of order.How to reproduce
The bug can be easily demonstrated using the example given in the docs for batch transactions in queries, including
SET ROLE
statements.Schema:
Minimal reproduction:
The SQL is then executed out of order:
If you comment out the
await 'test'
line, then the transaction will run sequentially.Expected behavior
I expect batch transactions to run sequentially when combining query extension and middleware with
await
statement.Prisma information
// Add your schema.prisma
// Add your code using Prisma Client
Environment & setup
Prisma Version
The text was updated successfully, but these errors were encountered: