Reasons
The ten answers Ledger gives when something doesn't go through.
Anything that can fail tells you why. Writes answer (boolean, Reason?) and reads answer
(value?, Reason?), and there are only ten reasons. They live on Ledger.Reason, so you can
compare against the constant instead of a string literal.
local Ok, Why = Session:Commit("SpendGold", { Amount = 25 }):Wait()
if not Ok and Why == Ledger.Reason.Refused then
Tell(Player, "you can't afford that")
endThis page says what each one means. Handling failures says what to do about them.
Refused
Your reducer returned nil. The op was legal to write, it just wasn't allowed to happen, so the
player couldn't afford it, or already owns it, or whatever your rule was. This is the only reason
that came from your own code, and it's the only one that's a normal part of gameplay.
Nothing changed. Don't retry it, the answer won't be different.
Busy
Someone else is in the middle of a transaction on that key and it hasn't finished yet. Nothing changed.
Ledger puts the key on the recovery sweep, which runs every 60 seconds. Your own retry usually gets there first, because every read and write settles what it finds pending on the key before doing anything else. Retry in a few seconds.
Session:Apply also answers this when the key has hit its 1.5 MB op cap and a parked transaction is
stopping it compacting. The datastore is fine. Retry once the transaction settles.
Tx does not retry a Busy for you. Every server retrying at once would pile more load onto a key
that is already contended, so the backoff is yours to write. See
One key at a time.
Spent
That name is used up and nothing was applied.
For a transfer it means the money went back to the sender. The hold sat there long enough to expire and Ledger refunded it. The id is kept afterwards so a late retry can't pay out against a refund that already happened.
For a transaction it means the name can't be used for what you asked. Either that name already ran for
a different set of keys or a different amount, or an earlier attempt was cancelled part way. Ledger
would rather refuse than half apply it. A transfer answers Spent for the same reason, so asking
again under a name that already went through, for a different amount or a different key, gets Spent
instead of true.
Spent says this attempt changed nothing. It does not promise the name can never work again. A name
whose transaction was cancelled before any leg applied is free to use, and that is what lets a retry
finish a cancelled attempt. A name that already moved money is done.
Spent is not success. Nothing moved, and for a transfer the money is back where it started. See
Spent.
Held
A transfer took the money and could not hand it over, because the receiving key was erased. This is not "nothing happened". The amount has left the sender and is set aside with them.
Ledger gives it back on its own, and recovery follows that key until it does. Tell the sender their money is coming back rather than that the transfer failed. Don't send it again under a new name, or they pay twice.
Release and Confirm answer it too, while a Grant is handing those
units to another key. They are not there to give back or to spend, and the grant may already have
delivered them. Recovery finishes it one way or the other.
Unresolved
Ledger doesn't know. Either the datastore call failed in a way that might still have written, or a transaction leg is parked on the key and the fold can change once it settles.
Session:Apply answers this when a transaction is parked and your op only holds if that transaction
does not go through. Ledger folds both futures and only answers when they agree. Ask again once the
transaction settles.
Unresolved does not mean no, and it is the one that costs money if you treat it as a refusal. See
Unresolved.
Closed
The session is gone. The player left and it was released, or the store was destroyed. Anything you write to it now goes nowhere.
Backlog
Ops are piling up because saves aren't going through. Either 4096 ops are queued or they've hit the 1.5 MB cap on unsaved bytes. This basically only happens when the datastore is down.
Nothing changed. This is a signal to stop writing and look at what's wrong, not to retry harder.
Full
The profile is at the 2 MB cap and the op would make it bigger. Ledger lets ops through that shrink it, so a cleanup still works.
Store:Edit answers this when the key already carries 1.5 MB of ops that have not been compacted
away. That happens when a parked transaction is stopping the key compacting. It clears once the
transaction settles.
Nothing changed. You need to remove something before you can add anything.
Invalid
The op can't be stored. Something in the fields isn't JSON, so an Instance, a function, a cyclic
table, a table mixing array and dictionary keys, NaN, inf. Or the fields use a name Ledger reserves,
or the kind starts with __. Or your reducer handed back something that wasn't a table.
Edit, Apply, Commit and Tx all answer this the same way, and for Tx it means one of the
legs, so nothing was prepared on any key.
Nothing changed. This is a bug in the calling code, and Ledger warns with the specific field.
Behind
A newer server wrote that record and this one can't read it. Either the stored format is newer than this server's build, or the record needs a migration this build doesn't have.
Nothing changed, and retrying is pointless. Behind describes which version of your game this server
is running, so nothing about the datastore will change it. It clears when the deploy finishes and
every server is on the same build.
You'll only see it during a rolling deploy, and only on records a newer server already touched. If you see it after a deploy has finished, someone is running an old build.
Never treat Behind as "this player has no data". The record is fine. This server is the problem,
and writing a fresh profile over the top would destroy it. See
Behind.