2016-10-23 17 views
1

Google App EngineでホストされているDjangoアプリからShopify APIを使用しようとしています。Rate Limit API Google App EngineでDjangoでAPIを呼び出す

# Setup local bucket to limit API Calls 
bucket = TokenBucket(40, .5) 

api_call_success = False 

while not api_call_success: 
    if bucket.tokens < 1: 
     sleep(.5) 
    else: 
     [... do an API call ...] 
     bucket.consume(1) 
     api_call_success = True 

これは私の地元のスクリプトで動作しますが、それは動作しません:私はShopifyのレート制限を超える行っていないことを確認するmodified version of thisを使用しています私の地元のシングルスレッドスクリプトの場合

私のGoogle App Engineは複数のテナントが存在し、複数のセッションが同時に発生するアプリケーションをホストしました。

私はこのレート制限を処理する最善の方法を研究しようとしていましたが、現在、常に各ユーザー/店舗のリクエストレスポンスヘッダーをmemcacheに書き込んで、「x-shopify-shop以前のコール制限(およびコール時間)が何であったかを確認するには、「-api-call-limit」を使用します。だから私はこのような何かを試してみました:

fill_rate = .5 
    capacity = 40 

    # get memcache key info 
    last_call_time = memcache.get(memKey+"_last_call_time") 
    last_call_value = memcache.get(memKey+"_last_call_value") 

    # Calculate how many tokens should be available 
    now = datetime.datetime.utcnow() 
    delta = fill_rate * ((now - last_call_time).seconds) 
    tokensAvailable = min(capacity, delta + last_call_value) 

    # Check if we can perform operation 
    if tokensAvailble > 1: 
     [... Some work involving Shopify API call ...] 
     # Do some work and then update memcache 
     memcache.set_multi({"_last_call_time": datetime.datetime.strptime(resp_dict['date'], '%a, %d %b %Y %H:%M:%S %Z'), "_last_call_value": resp_dict['x-shopify-shop-api-call-limit'].split('/',1)[0]}, key_prefix=memKey, time=120) 
    else: 
     [... wait ...] 

誰もがこのレート制限を管理するためのより良い方法をお勧めしますか?

答えて

2

あなたは(Redisのを使用して)として、私は基本的に同じロジックを持っている、しかし、代わりにどこでも、このインラインを行うので、私は猿がそうのようshopify.base.ShopifyConnectionパッチを適用しました:

from time import sleep 
from django.conf import settings 
from pyactiveresource.activeresource import formats 
from pyactiveresource.connection  import (
    Connection, 
    ConnectionError, 
    ServerError, 
) 
import shopify 


class ShopifyConnection(Connection, object): 
    response = None 

    def __init__(self, site, user=None, password=None, timeout=None, 
       format=formats.JSONFormat): 
     super(ShopifyConnection, self).__init__(site, user, password, timeout, format) 

    def consume_token(uid, capacity, rate, min_interval=0): 
     # Your rate limiting logic here 

    def _open(self, *args, **kwargs): 
     uid = self.site.split("https://")[-1].split(".myshopify.com")[0] 
     self.response = None 
     retries = 0 
     while True: 
      try: 
       self.consume_token(uid, 40, 1.95, 0.05) 
       self.response = super(ShopifyConnection, self)._open(*args, **kwargs) 
       return self.response 
      except (ConnectionError, ServerError) as err: 
       retries += 1 
       if retries > settings.SHOPIFY_MAX_RETRIES: 
        self.response = err.response 
        raise 
       sleep(settings.SHOPIFY_RETRY_WAIT) 


shopify.base.ShopifyConnection = ShopifyConnection 

あなたはこのコードが住んでいることになるでしょうあなたのアプリのディレクトリ内にファイルを作成し、そのファイルをあなたのアプリの__init__.pyにインポートします。このようにして、レート制限ロジックを気にせずに残りのコードを書くことができます。 shopifyモジュールを更新するときにShopifyConnectionクラスが変更されているかどうかを確認し、それに応じてサルパッチを更新するだけです。モジュールが頻繁に更新されないので、大きな問題ではありません。あなたが見ることができるように1/1000程度の要求が理由もなく失敗するよう

(、私は、再試行ロジックを挿入するための機会として、このモンキーパッチを撮影した。私はsettings.pySHOPIFY_MAX_RETRIESSHOPIFY_RETRY_WAITを定義し、値を引っ張っていますここをクリックしてください)

+0

、アプリケーションのフロントエンドもShopifyを呼び出す場合APIを使用すると、ユーザーが操作を実行しようとすると同時にコールバケット内のすべての部屋をバックエンドプロセスが使い切った場合、応答性の問題が発生することはありますか?私は人々が問題であると示唆したいくつかのスレッドを読んだ。 – cj1689262

+0

あなたは 'def consume_token'で使用しているレート制限ロジックをさらに詳しく説明できますか?以前の応答時間に基づいて計算を試みようとしていましたが、consume_tokenを呼び出す前に私はself.response = noneを呼び出していますので、前回の呼び出し時間に実際にアクセスできません。 – cj1689262

+0

あなたの最初の質問について:私は、フロントエンドからAPIを使用することがどういう意味か分かりません。私の知る限りでは、サーバーに要求を送信する必要がありますし、サーバーがAPI呼び出しを行います。これをクライアント側から行うとセキュリティ上の問題が発生します。つまり、APIリクエストを発行する前にアプリがトークンを待っていれば、それは何をすべきかを正確に行うことに成功しているという。これが大きな問題になる場合は、実装の再評価をお勧めし、API呼び出しを減らす可能性があるかどうかを確認してください。 – Julien

0

Shopifyは、レート制限コード付きのCLI Ruby gemを配布します。 RubyとPythonは構文的に近いので、コードを読むのに苦労する必要はありません。 Shopifyのコードは通常高水準で書かれているので、パターンを捏造してPythonに変換すると、GAEでうまく機能するはずです。

+0

これは、あなたが話しているCLI Rubyの逸品です:[リンク](https://github.com/shopify/shopify_cli) – cj1689262

+0

いや...それはこの1つ..ですCLIテーマの... HTTPS ://github.com/Shopify/shopify_theme と、このファイルを https://github.com/Shopify/shopify_theme/blob/master/lib/shopify_theme.rb –

0
import logging 
from google.appengine.api import memcache 
import datetime 
from datetime import date, timedelta 
from django.conf import settings 
from time import sleep 

# Store the response from the last request in the connection object 
class ShopifyConnection(pyactiveresource.connection.Connection): 
    response = None 

    def __init__(self, site, user=None, password=None, timeout=None, 
       format=formats.JSONFormat): 
     super(ShopifyConnection, self).__init__(site, user, password, timeout, format)   

    def consume_token(self, uid, capacity, rate, min_interval): 
     # Get this users last UID 
     last_call_time = memcache.get(uid+"_last_call_time") 
     last_call_value = memcache.get(uid+"_last_call_value") 

     if last_call_time and last_call_value: 
      # Calculate how many tokens are regenerated 
      now = datetime.datetime.utcnow() 
      delta = rate * ((now - last_call_time).seconds) 

      # If there is no change in time then check what last call value was 
      if delta == 0: 
       tokensAvailable = min(capacity, capacity - last_call_value) 
      # If there was a change in time, how much regen has occurred 
      else:    
       tokensAvailable = min(capacity, (capacity - last_call_value) + delta) 

      # No tokens available can't call 
      if tokensAvailable <= min_interval: 
       raise pyactiveresource.connection.ConnectionError(message="No tokens available for: " + str(uid)) 


    def _open(self, *args, **kwargs): 
     uid = self.site.split("https://")[-1].split(".myshopify.com")[0] 
     self.response = None 
     retries = 0 
     while True: 
      try: 
       self.consume_token(uid, 40, 2, settings.SHOPIFY_MIN_TOKENS) 
       self.response = super(ShopifyConnection, self)._open(*args, **kwargs) 

       # Set the memcache reference 
       memcache.set_multi({ 
        "_last_call_time": datetime.datetime.strptime(self.response.headers['date'], '%a, %d %b %Y %H:%M:%S %Z'), "_last_call_value": int(self.response.headers['x-shopify-shop-api-call-limit'].split('/',1)[0])}, 
           key_prefix=uid, time=25) 
       return self.response  
      except (pyactiveresource.connection.ConnectionError, pyactiveresource.connection.ServerError) as err: 
       retries += 1 
       if retries > settings.SHOPIFY_MAX_RETRIES: 
        self.response = err.response 
        logging.error("Logging error for _open ShopifyConnection: " + str(uid) + ":" + str(err.message)) 
        raise 
       sleep(settings.SHOPIFY_RETRY_WAIT) 

ユーザーJulienに感謝します。テストやフィードバックを通じて過失がないことを確認したいだけです。

+0

これは正確に私たちがこの猿のパッチで回避しようとしているように、トークンが使い尽くされたときにエラーを発生させることをお勧めしません。むしろ、次のリクエストを送信する前にトークンを回復するために必要な時間スリープ()してください。私はまた、あなたの 'min_interval'は私が提案したものとは異なるものだと思っています。実際、あなたの実装で'容量 'と何が違うのか分かりません。 – Julien

関連する問題