2012-01-25 58 views
2

我們的應用程序使用一些公平的網絡調用(它建立在第三方REST API之上),所以我們使用了很多異步操作來保持系統響應。 (使用Swirl保持理智,因爲應用程序是在tornado.gen之前寫的)。所以,當需要做一些地理編碼時,我們認爲這將是微不足道的 - 拋出一些異步調用到另一個外部API,我們會很好。異步操作後神祕地掛起龍捲風 - 我該如何調試?

不知何故,我們的異步代碼神祕地懸掛着Tornado--該進程仍在運行,但它不會響應請求或向日志輸出任何內容。更糟的是,當我們將第三方服務器完全排除在外時,它仍然掛起 - 它似乎鎖定了異步請求返回後的任意時間段。

下面是一些存根代碼複製問題:

def async_geocode(lat, lon, callback, fields=('city', 'country')): 
    '''Translates lat and lon into human-readable geographic info''' 
    iol = IOLoop.instance() 
    iol.add_timeout(time.time() + 1, lambda: callback("(unknown)")) 

而這裏的測試,通常(但並不總是 - 這是它是如何得到生產擺在首位),捕獲它:

class UtilTest(tornado.testing.AsyncTestCase): 

    def get_new_ioloop(self): 
     '''Ensure that any test code uses the right IOLoop, since the code 
     it tests will use the singleton.''' 
     return tornado.ioloop.IOLoop.instance() 

    def test_async_geocode(self): 
     # Yahoo gives (-122.419644, 37.777125) for SF, so we expect it to 
     # reverse geocode to SF too... 
     async_geocode(lat=37.777, lon=-122.419, callback=self.stop, 
         fields=('city', 'country')) 
     result = self.wait(timeout=4) 
     self.assertEquals(result, u"San Francisco, United States") 
     # Now test if it's hanging (or has hung) the IOLoop on finding London 
     async_geocode(lat=51.506, lon=-0.127, callback=self.stop, 
         fields=('city',)) 
     result = self.wait(timeout=5) 
     self.assertEquals(result, u"London") 
     # Test it fails gracefully 
     async_geocode(lat=0.00, lon=0.00, callback=self.stop, 
         fields=('city',)) 
     result = self.wait(timeout=6) 
     self.assertEquals(result, u"(unknown)") 

    def test_async_geocode2(self): 
     async_geocode(lat=37.777, lon=-122.419, callback=self.stop, 
         fields=('city', 'state', 'country')) 
     result = self.wait(timeout=7) 
     self.assertEquals(result, u"San Francisco, California, United States") 
     async_geocode(lat=51.506325, lon=-0.127144, callback=self.stop, 
         fields=('city', 'state', 'country')) 
     result = self.wait(timeout=8) 
     self.io_loop.add_timeout(time.time() + 8, lambda: self.stop(True)) 
     still_running = self.wait(timeout=9) 
     self.assert_(still_running) 

請注意,第一次測試幾乎總是通過,而這是第二次測試(以及致電async_geocode)通常會失敗。

編輯添加:還請注意,我們有很多類似的異步調用到我們的其他第三方API工作非常好。

(爲了完整起見,這裏的全面實施async_geocode及其輔助類(雖然上面的存根複製的問題)):

def async_geocode(lat, lon, callback, fields=('city', 'country')): 
    '''Use AsyncGeocoder to do the work.''' 
    geo = AsyncGeocoder(lat, lon, callback, fields) 
    geo.geocode() 


class AsyncGeocoder(object): 
    ''' 
    Reverse-geocode to as specific a level as possible 

    Calls Yahoo! PlaceFinder for reverse geocoding. Takes a lat, lon, and 
    callback function (to call with the result string when the request 
    completes), and optionally a sequence of fields to return, in decreasing 
    order of specificity (e.g. street, neighborhood, city, country) 

    NB: Does not do anything intelligent with the geocoded data -- just returns 
    the first result found. 
    ''' 

    url = "http://where.yahooapis.com/geocode" 

    def __init__(self, lat, lon, callback, fields, ioloop=None): 
     self.lat, self.lon = lat, lon 
     self.callback = callback 
     self.fields = fields 
     self.io_loop = ioloop or IOLoop.instance() 
     self._client = AsyncHTTPClient(io_loop=self.io_loop) 

    def geocode(self): 
     params = urllib.urlencode({ 
      'q': '{0}, {1}'.format(self.lat, self.lon), 
      'flags': 'J', 'gflags': 'R' 
     }) 

     tgt_url = self.url + "?" + params 
     self._client.fetch(tgt_url, self.geocode_cb) 

    def geocode_cb(self, response): 
     geodata = json_decode(response.body) 
     try: 
      geodata = geodata['ResultSet']['Results'][0] 
     except IndexError: 
      # Response didn't contain anything 
      result_string = "" 
     else: 
      results = [] 
      for f in self.fields: 
       val = geodata.get(f, None) 
       if val: 
        results.append(val) 
      result_string = ", ".join(results) 

     if result_string == '': 
      # This can happen if the response was empty _or_ if 
      # the requested fields weren't in it. Regardless, 
      # the user needs to see *something* 
      result_string = "(unknown)" 

     self.io_loop.add_callback(lambda: self.callback(result_string)) 

編輯:所以不少繁瑣的調試和記錄後在幾天內系統失效的情況下,事實證明,正如接受的答案所指出的那樣,我的測試由於不相關的原因而失敗。它也證明了它懸掛的原因與IOLoop沒有關係,而是一個有問題的協程立即掛着等待數據庫鎖定。

對不起,有針對性的問題,並感謝大家的耐心等待。

回答

3

你的第二個測試似乎是因爲這一部分的故障:

self.io_loop.add_timeout(time.time() + 8, lambda: self.stop(True)) 
still_running = self.wait(timeout=9) 
self.assert_(still_running) 

當您通過self.wait添加超時到IOLoop,當self.stop被稱爲是超時不被清除,據我可以告訴。 I.E.你的第一次超時是持續的,當你睡眠IOLoop 8秒時,它會觸發。

我懷疑任何與您的原始問題有關。

+0

你是完全正確的,這是導致測試失敗。同樣正確的是,我原來的問題與它無關。 – necaris

+0

可悲的是我不知道如何開始調試你的實際問題。 –