Skip to content
This repository has been archived by the owner on Sep 11, 2019. It is now read-only.

Support StrictRedis.eval for Lua scripts #9

Merged
merged 57 commits into from
Feb 13, 2018
Merged

Support StrictRedis.eval for Lua scripts #9

merged 57 commits into from
Feb 13, 2018

Conversation

blfoster
Copy link

@blfoster blfoster commented Feb 6, 2018

Fixes jamesls#176

@coveralls
Copy link

coveralls commented Feb 6, 2018

Coverage Status

Coverage decreased (-0.04%) to 97.949% when pulling 507a125 on amplifylitco:master into 9f881a8 on ska-sa:master.

@coveralls
Copy link

coveralls commented Feb 6, 2018

Coverage Status

Coverage increased (+0.04%) to 98.024% when pulling 9f4d910 on amplifylitco:master into 9f881a8 on ska-sa:master.

@blfoster
Copy link
Author

blfoster commented Feb 6, 2018

Coveralls report shows that new code has 100% coverage. I'm not sure why it reports a net decrease. The uncovered line seems unrelated to this PR.

@bmerry
Copy link

bmerry commented Feb 7, 2018

Thanks, I'll take a look soon. What happens if the user doesn't have Lua installed? Ideally it should gracefully fall back to not supporting the eval command, rather than being a hard requirement.

@blfoster
Copy link
Author

blfoster commented Feb 7, 2018

Thanks. I'm testing it now on a clean install of Linux Mint without Lua, and it works fine. I don't have a great explanation for how that is possible at the moment. I would need to dig into the lupa source a bit more. I can look at that in the morning.

@bmerry
Copy link

bmerry commented Feb 7, 2018

Is it possible that lupa is bundling a local copy of Lua in a binary wheel? That might be something that will work only for people using platforms for which lupa ships a wheel.

fakenewsredis.py Outdated
'incrby': self.incr
}
op = op.lower()
func = special_cases[op] if op in special_cases else getattr(self, op)
Copy link

Choose a reason for hiding this comment

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

There are presumably attributes of self that shouldn't be visible to Lua - for a start, anything with an underscore prefix, plus things like delete and from_url. Also, when self is a FakeRedis rather than a FakeStrictRedis, it presumably should use the overload from the FakeStrictRedis base class.

Copy link
Author

Choose a reason for hiding this comment

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

Probably yes. This is a bit of a hack, but I figure the tradeoff is that it doesn't have a huge mapping from Redis commands to Python functions that needs to be maintained as more Redis functions get added. I'll make it a bit more selective.

@blfoster
Copy link
Author

blfoster commented Feb 7, 2018

It looks like Lupa includes the Lua source, which it builds as a fallback in the event that it can't find Lua installed locally.

@bmerry
Copy link

bmerry commented Feb 7, 2018

It looks like Lupa includes the Lua source, which it builds as a fallback in the event that it can't find Lua installed locally.

I'd prefer not to make a compiler (or an already-installed Lua) a hard requirement. I would suggest

  • importing lupa locally in the eval function; and
  • declaring it as an extras_require in setup.py.

@blfoster
Copy link
Author

blfoster commented Feb 7, 2018

Code review fallout is complete.

@bmerry
Copy link

bmerry commented Feb 8, 2018

I still need to go through the docs to see if I missed anything.

Okay, let me know when you think it's all covered and I'll take another look.

@blfoster
Copy link
Author

blfoster commented Feb 9, 2018

@bmerry I've added a bunch more tests and fixed some more issues. It seems that you're more familiar with Redis than I, so you may find that I've missed something, but it looks pretty good to me.

@bmerry
Copy link

bmerry commented Feb 9, 2018

I've added a bunch more tests and fixed some more issues. It seems that you're more familiar with Redis than I, so you may find that I've missed something, but it looks pretty good to me.

Great. I'll look over it again next week. Coveralls seems to indicate that there is an exception handler that isn't being covered by the tests - can you take a look?

@blfoster
Copy link
Author

blfoster commented Feb 9, 2018

Oops, I don't think that exception handler is necessary. I took it out.

README.rst Outdated
@@ -266,6 +265,11 @@ they have all been tagged as 'slow' so you can skip them by running::
Revision history
================

0.9.5
Copy link

Choose a reason for hiding this comment

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

Can you change this to "Development version". There is some possibility of re-merging with fakeredis, so I don't want to make assumptions about version numbers yet.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

README.rst Outdated
0.9.5
-----
Add support for StrictRedis.eval for Lua scripts
- `#9 <https://github.com/ska-sa/fakenewsredis/pull/9>`_
Copy link

Choose a reason for hiding this comment

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

Can you put the description into the bullet point (to make the format of the other changelog entries).

Copy link
Author

Choose a reason for hiding this comment

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

Done.

fakenewsredis.py Outdated
Returns the result of the script.
In practice, use the object returned by ``register_script``. This
function exists purely for Redis API completion.
"""
Copy link

Choose a reason for hiding this comment

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

You can delete the docstring. It's assumed that people will refer to redis-py for docs.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

fakenewsredis.py Outdated
expected_globals = set()
set_globals(
(None,) + keys_and_args[:numkeys],
(None,) + keys_and_args[numkeys:],
Copy link

Choose a reason for hiding this comment

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

I imagine this will go wrong if numkeys is out of range; it should probably raise a ResponseError.

Copy link
Author

Choose a reason for hiding this comment

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

Indeed.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed, along with a bunch of other edge cases.

"Script attempted to set a global variables: %s" % ", ".join(
actual_globals - expected_globals
)
)
Copy link

Choose a reason for hiding this comment

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

I don't think this is a good approach to preventing global variables being created: it doesn't make the script itself error out, and it doesn't actually prevent the globals from polluting the namespace.

Here is the code in redis itself that implements the protection. It may need some tweaks to adapt it - unfortunately I've never programmed in Lua so I'm not sure.

See also the function above the code in that link - it appears to disable readfile and dofile, presumably for security reasons.

Copy link
Author

Choose a reason for hiding this comment

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

I think this is actually safe, because if we try to set a global variable and error out, the LuaRuntime instance will never be used again; if we try to run another Lua script, we'll get a new LuaRuntime, which will not have that global variable set. Unless there's something I'm missing. I do think this way is a bit more readable, but perhaps that's because I don't know Lua. I could add a few more lines to the unit test asserting that trying to set a global variable can't have a side effect...

Copy link

Choose a reason for hiding this comment

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

I hadn't spotted that the runtime gets discarded each time, so it's safer than I thought. I think it will still be different to real redis if a script sets a global variable, then modifies the database: in real redis it would error out as soon as it tries to set the global, whereas in this implementation it will get to modify the database before the error. Another case that will probably behave differently is a script that creates a global and then deletes it again, before your check.

We're starting to get to the point of diminishing returns. If you've got the time and energy to test it and fix things up, then go for it, but I realise that I've made you do a lot more work than you probably expected when you started. If you're running out of steam then this is something that can be left for another day.

Copy link
Author

Choose a reason for hiding this comment

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

👍 I don't think it should be possible to modify the database either after setting a global, because _lua_redis_call calls _check_for_lua_globals before executing any command that could change the database.

Copy link

Choose a reason for hiding this comment

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

Aha, I'd missed that subtlety too. So then probably the only case that'll behave differently is if the script creates a global and deletes it again.

Copy link
Author

Choose a reason for hiding this comment

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

Yep. I'll leave that alone for now if you're ok with it.

[
val[:2],
val[2:]
]
Copy link

Choose a reason for hiding this comment

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

This whole statement can go on one line.


def test_eval_convert_nil_to_false(self):
val = self.redis.eval('return ARGV[1] == false', 0, None)
self.assertFalse(val)
Copy link

Choose a reason for hiding this comment

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

I don't think this test is testing what is implied by the name. redis-py converts all arguments through its to_bytes, so None as an argument becomes the string "None" in the script, rather than false. The following test currently fails (but passes for real redis):

        val = self.redis.eval('return ARGV[1] == "None"', 0, None)
        self.assertTrue(val)

Copy link
Author

Choose a reason for hiding this comment

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

Huh, something went wrong here. I'll sort it out...

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

fakenewsredis.py Outdated
try:
result = lua_runtime.execute(script)
except LuaSyntaxError as ex:
raise ResponseError(ex)
Copy link

Choose a reason for hiding this comment

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

I think you need to catch more than just LuaSyntaxError. The following test fails with a LuaError.

    def test_eval_runtime_error(self):
        with self.assertRaises(ResponseError):
            self.redis.eval('error("CRASH")', 0)

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

fakenewsredis.py Outdated
'ZREVRANGEBYLEX', 'ZREVRANGEBYSCORE', 'ZREVRANK', 'ZSCAN', 'ZSCORE', 'ZUNIONSTORE'
]
if op.upper() not in commands:
raise ResponseError("Unknown Redis command called from Lua script")
Copy link

Choose a reason for hiding this comment

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

It seems a bit odd to convert to uppercase for this check and then to lowercase to actually make the call. How about just expressing the whitelist in lowercase?

Copy link
Author

Choose a reason for hiding this comment

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

Heh, the commands are uppercase because that's the way they were written in the Redis documentation I copied this from. I'll change it to lowercase.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

fakenewsredis.py Outdated
except Exception as ex:
return lua_runtime.table_from(
{"err": str(ex)}
)
Copy link

Choose a reason for hiding this comment

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

This can go on one line.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@blfoster
Copy link
Author

There are some more subtleties to the numkeys argument (in addition to not being negative), which I've added. I'll push those in a few minutes when I have tests.

@blfoster
Copy link
Author

Ready for review again.

@bmerry
Copy link

bmerry commented Feb 13, 2018

I'm going to merge this because I think it's at a point where it is a solid useful contribution - thanks! There are still some other things that could be done if you're feeling keen to work on them in a new PR. If not I can file them as bugs to be worked on later:

  • The other script related commands (EVALSHA, SCRIPT LOAD etc)
  • The Script convenience class provided by redis-py on top of the raw commands
  • Loading modules like bitop, cjson etc
  • Some environmental differences e.g. corner cases in global variable handling we've discussed, blocking database modifications after calling non-deterministic functions, removal of dofile and readfile. This is probably a rabbit-hole though.

@bmerry bmerry merged commit 4bfcda2 into ska-sa:master Feb 13, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants