Skip to content

Async Patterns

Xyranaut edited this page Jun 1, 2026 · 3 revisions

Async patterns & pitfalls

This is the single biggest mental hurdle for newcomers. Database queries in omp-MySQL are asynchronous — they don't give you the answer immediately. Once this clicks, everything else is easy.

The core idea: you don't get the result on the same line

When you call mysql_execute, it starts the query and returns right away — the rows aren't ready yet. They arrive later, in a callback.

mysql_execute(g_DB, "SELECT money FROM accounts WHERE id = 5", "OnMoney");
// <-- the money is NOT available here yet!

forward OnMoney();
public OnMoney()
{
    new money; mysql_rs_get_int_by(0, "money", money);  // <-- it's available HERE
    return 1;
}

Think of it like ordering food: you place the order (mysql_execute) and keep doing other things; when the kitchen is done it calls your number (OnMoney).

❌ You cannot return a database value

This is the #1 mistake. This does not work:

GetMoney(playerid)               // WRONG - can't return an async result
{
    new money;
    mysql_execute(g_DB, "SELECT money ...", ...);
    return money;                // money is still empty here!
}

There's no way to "wait here and return the row." Instead, do the work in the callback:

LoadMoney(playerid)
{
    mysql_execute(g_DB, "SELECT money FROM accounts WHERE id = 5", "OnMoney", "d", playerid);
}
forward OnMoney(playerid);
public OnMoney(playerid)
{
    new money; mysql_rs_get_int_by(0, "money", money);
    GivePlayerMoney(playerid, money);   // act on it here
    return 1;
}

"But I really want to wait!" — there's mysql_execute_sync which does block and return a result. Use it only for one-time startup (creating tables). In a player event it freezes your whole server. See Performance.

Passing context into the callback

The callback runs later, so it needs to know who/what it's for. Pass extra args:

// "d" = pass playerid through to the callback
mysql_execute(g_DB, "SELECT ...", "OnLoaded", "d", playerid);

// "ds" = pass an int and a string
mysql_execute(g_DB, "SELECT ...", "OnLoaded2", "ds", playerid, someText);

Format letters: d/i int, f float, s string. The callback's parameters must match: public OnLoaded(playerid), public OnLoaded2(playerid, const text[]).

Pitfall: the player left before the result came back

Because results arrive later, the player might have disconnected in the meantime. Always re-check:

public OnLoaded(playerid)
{
    if (!IsPlayerConnected(playerid)) return 1;   // they left; bail out
    // ... safe to use playerid ...
    return 1;
}

There's a built-in helper for this common case: mysql_execute_for behaves like mysql_execute but treats the first integer callback arg as a playerid and skips the callback automatically if that player has left:

mysql_execute_for(g_DB, "SELECT ...", "OnLoaded", "d", playerid);  // auto-skips if gone

Pitfall: the slot got reused

A playerid is a slot number (0–49). If player 5 leaves and a new player 5 joins before your callback fires, acting on playerid could touch the wrong person. Guard against it:

  • Use mysql_execute_for (skips if the original player left), and/or
  • Re-verify identity (e.g. compare the loaded account name to the current player name) for anything sensitive.

This is also why the mysql-admin demo wipes session state on every connect and never trusts a slot's old login — see Security.

Pitfall: ordering and races

  • Two queries on the same handle run in order (FIFO) — predictable.
  • Two queries on different handles can finish in any order.
  • Don't assume query B sees query A's result unless B runs after A's callback, or they're in the same transaction.

Example race: saving on disconnect and an autosave timer firing at the same moment — make saves idempotent (a plain UPDATE ... WHERE id = ? is safe to run twice).

Pitfall: reading results outside the callback

The result set is only "active" inside the callback for that query. Reading mysql_rs_* anywhere else gives empty/garbage. Read what you need in the callback (copy it into variables if you must keep it).

Pitfall: forgetting there might be zero rows

public OnLoaded(playerid)
{
    new rows; mysql_rs_row_count(rows);
    if (rows == 0) { /* no such account */ return 1; }
    new money; mysql_rs_get_int_by(0, "money", money);
    return 1;
}

Reading row 0 when there are no rows yields 0/empty and hides bugs.

The mental model, summarized

  1. Keep player state in Pawn variables during play.
  2. Load from the DB into those variables on login (in a callback).
  3. Save them back periodically + on disconnect (fire-and-forget UPDATE).
  4. Never try to "return" a DB value — branch into a callback instead.
  5. Always assume the player might be gone when the callback runs.

Get these five right and you'll never fight the async model again.

Next: Performance · Transactions · Cookbook

Clone this wiki locally