Code Monkey home page Code Monkey logo

Comments (2)

rspeele avatar rspeele commented on July 20, 2024

Hi Risord,

This is a big answer. Please read carefully. I should probably add something to the Rezoom docs about this, but might end up just linking to this issue.

Transactions

When you execute a plan, the whole thing is wrapped in a transaction. The isolation level defaults to whatever you get from DbConnection.BeginTransaction() which will depend on the database backend you're using. But you can implement your own ConnectionProvider by inheriting from DefaultConnectionProvider and overriding BeginTransaction. Then use it like so:

let myConnectionProvider = new MyConnectionProvider() // whatever class you wrote
let serviceConfig = new ServiceConfig()
// make sure to explicitly pass the type parameter here so it configures
// the base ConnectionProvider type to use this instance
serviceConfig.SetConfiguration<Rezoom.SQL.Mapping.ConnectionProvider>(myConnectionProvider)
let executionConfig =
    {   Execution.ExecutionConfig.Default with
            ServiceConfig = upcast serviceConfig
    }
let runPlan (plan : 'a Plan) : 'a Task = Execution.execute executionConfig plan

Batching and pre-condition checks

It sounds like your problem might not be due to the transactions at all, but happening within the execution of a single plan. Batching can make things that would otherwise be occasional concurrency conflicts happen every time. If you write code like this, it will always fail (if starting with no such user in the DB, and a unique constraint on email):

let addUserIfNotExisting (email : string) =
    plan {
          let! alreadyExists = checkIfUserAlreadyExists email
          if not alreadyExists then
              do! addUser email
    }

let exampleFailure() =
    plan {
        for email in batch ["[email protected]"; "[email protected]"] do
            do! addUserIfNotExisting email
    }

This is because all the plans in the batch step move in lockstep. So they start by both checking if the user exists, and both get the same answer "nope, no user by that name". Then they both conclude that they can move forward. so the 2nd one to execute blows up when it violates the unique constraint.

It's actually not so bad here since you at least see there's a problem. What you really want to watch out for is attempting to write code like:

let addUser (organization : OrganizationId) (email : string) =
    plan {
          let! organizationLimits = getOrganizationLimits organization
          let! numberOfUsers = getNumberOfUsers organization
          if numberOfUsers >= organizationLimits.MaximumNumberOfUsers then
              failwith "Hey, you have to pay more for a subscription to have that many users!"
          else
              do! addUserInternal organization email
    }
let addMultipleUsers (organization : OrganizationId) (emails : string list) =
    plan {
          for email in batch emails do
                do! addUser organization email
    }

Here, anyone could add as many users as they wanted to their organization, as long as they currently have room for at least one and they add them via a single call to addMultipleUsers. So the code that tries to enforce the limits is ineffective.

To be clear, this would have been bad code anyway, except that normally we only have concurrency between multiple requests to our web API, not within the handling of a single request, so slapping a transaction around the whole API call solves the concurrency issue. With plan batching you have to think about concurrency at a finer-grained level.

Avoiding this problem

Option 1: Atomic SQL commands

When possible, the best way to avoid the problem is to unify the check and the action into a single command so it executes atomically. For example, you could write an insert-or-update as a two-command sequence where part 1 inserts the record if it does not exist then spits out its ID, and part 2 unconditionally updates the record.

The SQL for part 1 could look like:

-- add the user if they don't already exist
insert into Users(Email)
select @email
where not exists (select null x from users where Email = @email);

-- return the row id to the program for use in the update part
select Id from Users where Email = @email;

Option 2: Convert pre-conditions to post-conditions

If you have a more complex scenario, violation of the business rule should be an error, and you don't want to have to think about what exactly the implications of batching will be, a bulletproof approach is to change your preconditions into postconditions.

This would probably be the approach I'd use in the case of the user limit for an organization. I'd write the code to add the user and then check if the limit was exceeded, instead of the other way around. Here I'd be leaning on the fact that every plan implicitly runs in the transaction, and if the plan execution fails due to an uncaught exception, the whole thing won't be committed. Of course you do still have to make sure you don't have a catch-all somewhere higher up within the plan that will eat and ignore this. If you really want to be certain, you can raise (PlanAbortException("message")) which cannot be caught within a plan block.

Option 3: Contribute a new feature to Rezoom

This feature does not exist, but it could, and it would be great for situations like this. If there was a way to convert an 'a Plan to an 'a Errand, you could use this to wrap your plan { precondition; conditional action } so it runs atomically (i.e. no other errands can run in between the precondition and conditional action).

This is semi-possible already since you can convert the plan to a task, and then wrap the task in an errand, but doing that loses the caching information and also would probably have issues due to the wrapped plan trying to use a different connection and transaction.

Implementing this feature would not be terribly hard (Rezoom is a small codebase, much smaller than Rezoom.SQL) but requires some thought as to what order errands would run in and whether that order would be guaranteed or arbitrary.

from rezoom.sql.

Risord avatar Risord commented on July 20, 2024

Hi

Thanks for fast and wide answer. This is useful information many ways for the future.

Actually in this case it was really multi request race condition. SQL Server just seems to use read commited isolation level by default so raising that to repeatable read this problem vanished. Although transaction repeat would be needed next.

At the end I made my SQL query to atomic. I was convinced that conditional insert is not possible but thanks for you sample it's now done and seems to work perfectly!

Edit: Btw I did use SQL profiler to watch under hood but since transactions aren't done with explicit begin transaction I was concluded wrongly that there are none.

from rezoom.sql.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.