MV10 / mv10.github.io

McGuireV10's personal blog
MIT License
4 stars 2 forks source link

Event Sourcing with Orleans Journaled Grains - Forty Years of Code #54

Open utterances-bot opened 4 years ago

utterances-bot commented 4 years ago

Event Sourcing with Orleans Journaled Grains - Forty Years of Code

Event sourcing with logging and snapshots for Microsoft Orleans

https://mcguirev10.com/2019/12/05/event-sourcing-with-orleans-journaled-grains.html

allieberezhnaya commented 4 years ago

Hello, thank you for the article. Could you please explain what is the profit of using stateless workers if they still call 1 state-full grain which is single threaded.

MV10 commented 4 years ago

Hi @allieberezhnaya -- for some reason github stopped notifying me of replies for awhile, sorry for the late response.

Orleans will spin up multiple stateless workers, and requests to stateful grains are queued (actors are message-driven), so it isn't a bottleneck like it might appear at first glance. But mostly it's just a clean "separation of concerns" decision (like CQRS itself). In a real system you'd have some sort of gateway / aggregator business-operations API that clients would call, then (as demonstrated here) those would invoke the CQRS services, and the event sourcing would effectively be a microservice behind CQRS. Also, of course, Orleans is free to host that stateful grain anywhere in the cluster whereas a stateless grain is always hosted locally to the silo servicing the calling client.

wdkr commented 3 years ago

Hi @MV10, thanks so much for the extensive write-up! Blogs like yours are a gold mine for developers trying to get familiar with technologies like Orleans.

While extending your OrleansEventStreamLog example I ran into a concurrency problem I hope you'll be able to help me out with. In the CustomerCommands class (marked as StatelessWorker and Reentrant) methods can be executed concurrently. In case of the method PostAccountTransaction, this means the following flow is possible:

  1. Account 1 is added with balance 100
  2. Client A calls PostAccountTransaction for this account with amount -60
  3. The method called by Client A checks the balance (which is 100)
  4. Before the above call writes its changes, Client B calls PostAccountTransaction for this account with amount -60
  5. The method called by Client B checks the balance (which is still 100)
  6. Both calls succeed and two transactions with an amount of -60 are posted

I've written some code to simulate this problem. The code can be inserted as-is on line 70 of DemoClient/Program.cs. If it doesn't simulate the problem on your PC perhaps an await Task.Delay(100) in ServiceCustomerAPI/CustomerCommands.cs:173 would do the trick.

var accountNumber = "1";
var account = new Account
{
    AccountNumber = accountNumber,
    AccountType = "Checking",
    Balance = 100,
    IsPrimaryAccount = true
};

var addAccount = await cmd.AddAccount(id, account);
if (addAccount.Success)
{
    var postFirstTransactionTask = cmd.PostAccountTransaction(id, accountNumber, -60);
    var postSecondTransactionTask = cmd.PostAccountTransaction(id, accountNumber, -60);
    await Task.WhenAll(postFirstTransactionTask, postSecondTransactionTask);

    var postFirstTransaction = postSecondTransactionTask.Result;
    var postSecondTransaction = postSecondTransactionTask.Result;

    if (postFirstTransaction.Success)
    {
        var balance = postFirstTransaction.Output.Accounts.Single().Balance;
        Console.WriteLine($"First transaction posted, new balance: {balance}");
    }
    else
    {
        Console.WriteLine($"Unable to post first transaction:\n{postFirstTransaction.Message}");
    }

    if (postSecondTransaction.Success)
    {
        var balance = postSecondTransaction.Output.Accounts.Single().Balance;
        Console.WriteLine($"Second transaction posted, new balance: {balance}");
    }
    else
    {
        Console.WriteLine($"Unable to post second transaction:\n{postSecondTransaction.Message}");
    }
}
else
{
    Console.WriteLine($"Unable to add account:\n{addAccount.Message}");
}

To me it seems like this business logic, such as checking for sufficient funds, should live somewhere inside a stateful grain which represents the Account (or the Customer and their accounts), but perhaps I'm missing something.

MV10 commented 3 years ago

@wdkr Thanks for the note. I'm not currently working with Orleans at all these days (unfortunately), so I'm not likely to revisit this code or the article -- but you're right, the scenario you describe is possible. This is why the world of finance (which is what I do in the real world) still has concepts like pending transactions and batch (overnight) posting and reconciliation.

That's effectively the same thing as have a $100 account balance and writing two checks for $60, it just goes sideways more quickly (progress!).

ccerrato147 commented 3 years ago

This is a really good article Jon. Thanks for sharing. I'm struggling to get an Event Sourcing project with Orleans running. Not so much for the logic but on the cluster configuration. Do you do consulting? We'd really appreciate you could help us to get the ball rolling.

MV10 commented 3 years ago

@ccerrato147 Hi Carlos, thanks for the kind words. I'm no longer using Orleans (it changes pretty quickly, which is both good and bad!), and I don't do any consulting, sorry. Good luck with your project.

ccerrato147 commented 3 years ago

@ccerrato147 Hi Carlos, thanks for the kind words. I'm no longer using Orleans (it changes pretty quickly, which is both good and bad!), and I don't do any consulting, sorry. Good luck with your project.

Yes, definitely changes with frequency. As you say both good and bad. I have to piece together from multiple tutorials and sources. If by any chance you know someone that does do consulting and is an active Orleans user then I'd appreciate the referral. Thank you for your good wishes Jon.

ccerrato147 commented 3 years ago

@ccerrato147 Let's have a chat about consultations.

I sent you an email.