Skip to content

Conversation

@vyavdoshenko
Copy link
Contributor

@vyavdoshenko vyavdoshenko commented Dec 2, 2025

Introduces a legacy-float script flag that makes cjson.decode compatible with Lua 5.1 behavior.

The core incompatibility:

redis-cli EVAL "local data = cjson.decode(ARGV[1]); return 'foo_'..data.id" 0 '{"id": 101}'
  • Legacy (Lua 5.1): returns "foo_101"
  • Dragonfly (Lua 5.4): returns "foo_101.0"

Libraries like django-cacheops depend on this behavior.

Solution:

Patched lua_cjson.c to check a Lua global variable __dfly_legacy_float__. When enabled, cjson.decode pushes integers instead of floats for whole numbers.

Usage via script header:

--!df flags=legacy-float
local obj = cjson.decode('{"id": 42}')
return tostring(obj.id)  -- returns "42" instead of "42.0"

Via command line (for all scripts):

dragonfly --default_lua_flags=legacy-float

Fixes: #6147

@vyavdoshenko vyavdoshenko self-assigned this Dec 2, 2025
Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Comment augment review to trigger a new review at any time.

Copy link
Collaborator

@romange romange left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good. @dranikpg PTAL as well

string flags = absl::GetFlag(FLAGS_default_lua_flags);

static_assert(ScriptParams{}.atomic && !ScriptParams{}.undeclared_keys);
static_assert(ScriptParams{}.atomic && !ScriptParams{}.undeclared_keys &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a weird assert, what does it check?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the default values of parameters, but yeah its a little weird 😅

}

const char* kFloatAsIntShas[] = {
"8c4dafdf9b6b7bcf511a0d1ec0518bed9260e16d", // django-cacheops
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have multiple scripts? I saw you changed only one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reused the same list of django-cacheops mentioned above. Only one of them has the same hash as the master branch. I guess the hashes can be used from previous versions, and my assumption is that we have to make these scripts work.

@romange
Copy link
Collaborator

romange commented Dec 2, 2025

I think it's an ok solution. The core problem is around the following incompatibility against valkey:

redis-cli EVAL "local data = cjson.decode(ARGV[1]); return 'foo_'..data.id" 0 '{"id": 101}'

and I think we can solve it directly in cjson.decode

@romange
Copy link
Collaborator

romange commented Dec 2, 2025

specifically, I think you can do a fix in json_process_value in lua_cjson.c and do the similar check of floor(token->value.number) to decide either to push an integer or the number. This way cjson.decode will be fully compatible on integers.

string flags = absl::GetFlag(FLAGS_default_lua_flags);

static_assert(ScriptParams{}.atomic && !ScriptParams{}.undeclared_keys);
static_assert(ScriptParams{}.atomic && !ScriptParams{}.undeclared_keys &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the default values of parameters, but yeah its a little weird 😅

Comment on lines 862 to 863
const char* code = enable ? kEnableLegacyFloat : kDisableLegacyFloat;
RunSafe(lua_, code, enable ? "@enable_legacy_float" : "@disable_legacy_float");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets store the state and run it only if it changes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because this will decrease performance even for the normal path

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the implementation. Please take a look.

// Valid flags are:
// - allow-undeclared-keys -> undeclared_keys=true
// - disable-atomicity -> atomic=false
// - legacy-float -> float_as_int=true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it would've been easier if it was a global flag, as I assume that if someones uses it, he'll use it for all scripts... but this is cleaner of course this way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to use it per script

Copy link
Contributor

@dranikpg dranikpg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should have a much smaller performance impact now, and is cleaner

@vyavdoshenko vyavdoshenko requested a review from dranikpg December 3, 2025 10:05
double num = token->value.number;
double intpart;
/* Check if legacy float mode is enabled via global variable */
lua_getglobal(l, "__dfly_legacy_float__");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that now it's much cleaner. But I must ask @dranikpg @vyavdoshenko why not use lua_pushinteger unconditionally if the number is integer ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in other words, why do we need __dfly_legacy_float__ mode at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flag preserves user choice. Scripts migrated from legacy Lua 4.1 can opt into legacy behavior, while new scripts keep Lua 5.4 semantics. Making it unconditional removes that flexibility.

If you insist on making it unconditionally, I will fix it, but in my opinion, it is better to save float behavior for future scripts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lua: add reply-float-as-int flag to scripts

4 participants