レタスのかわをぜんぶむく

ぜんぶむきます

RedashのPythonデータソースで自作の再帰関数が動作しない

そもそもRedashのPythonデータソースで再帰関数を書くな。

Redashではデータソースとして各種DBやDWH以外にPythonで記述したスクリプトも利用することができる。
検証のために実行時間が数十秒かかるようなスクリプトを書いた際にハマったので、調べて分かったことをまとめる。

要約

  • Redash内で記述出来るPythonスクリプトはRestrictedPythonを利用して実行されている
  • RestrictedPythonを利用してる都合上様々な制限がある
  • 利用したい関数をglobalとすれば利用できる

環境

  • Redash v8.0.0 (dev)

経緯

Redashで時間がかかりすぎているクエリを特定した時にどう対応するかを確認するために
あえて、数十秒程度実行に時間がかかるような処理をクエリとして実行したかった。

一定時間かかる処理かつある程度負荷をパラメータで調整できるような処理をSQLでは
すぐには思いつかなかったのでPythonスクリプトでフィボナッチ数を計算するスクリプトを書いた。

以下のソースコードがまず動かない

print "start"
def fibonacci(n):
    if n <= 2:
        return 1
    else:
        return fibonacci(n - 2) + fibonacci(n - 1)
print fibonacci(5)
print "finish"

実行すると
Error running query: <type 'exceptions.NameError'> global name 'fibonacci' is not defined
とエラーが出力されてスクリプトが動作しない。

自分のPython力が不安になりpaiza.ioで試すと問題なく動作する。
次に関数定義が制限されてるのかと考えて下記の処理を書いた。

def hello():
  print "hello"

hello()

問題なく動作する。
次に自作関数から別の自作関数を定義するような処理を書いた。

def a():
  print "hello"

def b():
  a()

b()

Error running query: <type 'exceptions.NameError'> global name 'a' is not defined
で最初のコード例と同様に動作しない。

自作関数から自作関数をコールすると死ぬ?なぜ?
と思って調べたら以下のようなIssueが公式にあった

github.com

まさに同じような問題に直面している。
globalキーワードをつければ動くよとのことなので最初のコードでは
下記のようにfibonacciにglobalをつけて動作を確認することができた。

print "start"
def fibonacci(n):
    if n <= 2:
        return 1
    else:
        return fibonacci(n - 2) + fibonacci(n - 1)
global fibonacci
print fibonacci(5)
print "finish"

このコードだと動作する。
めでたしめでたし。

以下蛇足

なぜメソッド名が見つからない(制限されているのか)?

先程のIssueコメントのリンク内では

https://github.com/getredash/redash/blob/master/redash/query_runner/python.py#L263

でExceptionを吐くと言ってるが当時のmasterを指しているので正確ではなく
コメント時期から推測するにRedash v2.0.1の

https://github.com/getredash/redash/blob/22aa64571a2e827672d0a68f7daebd3516e3f6f8/redash/query_runner/python.py#L263

...
            exec(code) in restricted_globals, self._script_locals
...

この部分でExceptionが発生している。

前段のcompile_restricted で生成されているバイトコードを実行しようとしているので RestrictedPython.compile_restrictedを調べると

pypi.org

RestrictedPythonというものがあることがわかる。

RestrictedPythonとは

ユーザーから受け取ったPythonスクリプトを安全に実行するためのライブラリ
Pythonのビルトイン関数のcompileを置き換え、利用可能なモジュール、メソッドを制限することで
バイトコードの段階で怪しい処理をチェックする、とのこと。

定義済みの利用可能なモジュール、メソッドのセットを使用したり、自前で追加削除が可能な模様。
RedashではPythonクエリランナーのこのあたりで処理していそう。

具体的にどこで制限しているの

わかりませんでした。

このあたりで入ってきたバイトコードを見つつ
not_allowedなどで使用できないキーワードを決めたりしているところまでは確認できるものの
明確に辞書から外す、入れるみたいな処理を確認できなかった。

メソッド定義を発見した時の処理で、色々とやっているように見えるけれど
そもそも例外が送出されるタイミングがバイトコードを実行するexec側なので
戻りのバイトコードが壊れてる(または意図的に壊している)のか判断つかなかった。

似たような言及を探してみると
以下の記事ではPythonスクリプトの先頭でimportしたライブラリが使えないので
メソッド内でimportしろという記述もある

www.ehfeng.com

RestrictedPythonのドキュメントを見てもどの部分がこの機能に相当するのかわからない…
Pythonの言語仕様自体に対して理解が浅いだけかもしれないしこれ以上はハマりそうなので撤退

ウオーなんでやと思って調査しはじめたら3時間くらい溶けた…
オチとしてはよくわかりませんでしたで締まらないけど
初見のソースコードをガッツリ掘っていくのもたまには楽しい。