-
Notifications
You must be signed in to change notification settings - Fork 5
Support StrictRedis.eval for Lua scripts #9
Conversation
This reverts commit 06e02b5.
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. |
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 |
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. |
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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
|
Code review fallout is complete. |
Okay, let me know when you think it's all covered and I'll take another look. |
@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. |
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? |
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>`_ |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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. | ||
""" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:], |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed.
There was a problem hiding this comment.
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 | ||
) | ||
) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
test_fakenewsredis.py
Outdated
[ | ||
val[:2], | ||
val[2:] | ||
] |
There was a problem hiding this comment.
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.
test_fakenewsredis.py
Outdated
|
||
def test_eval_convert_nil_to_false(self): | ||
val = self.redis.eval('return ARGV[1] == false', 0, None) | ||
self.assertFalse(val) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)} | ||
) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
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. |
Ready for review again. |
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:
|
Fixes jamesls#176