[Core] Defer save state function to separate thread

With large amounts of torrents, saving the state file becomes
a performance bottleneck, mainly due to the required processing
in pickle.dump. When run in the main thread, the server will
hang and be unresponsive for a significant time.

Solve this issue by running the save state job in a separate thread.
This commit is contained in:
bendikro 2015-11-23 14:01:34 +01:00 committed by Calum Lind
commit d13fca251e
2 changed files with 28 additions and 11 deletions

View file

@ -2,7 +2,7 @@
* libtorrent (rasterbar) >= 0.16.7 * libtorrent (rasterbar) >= 0.16.7
* python >= 2.6 * python >= 2.6
* setuptools * setuptools
* twisted >= 8.1 * twisted >= 11.1
* pyopenssl * pyopenssl
* pyxdg * pyxdg
* chardet * chardet

View file

@ -16,7 +16,7 @@ import os
import shutil import shutil
import time import time
from twisted.internet import reactor from twisted.internet import defer, reactor, threads
from twisted.internet.defer import Deferred, DeferredList from twisted.internet.defer import Deferred, DeferredList
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
@ -104,6 +104,7 @@ class TorrentManager(component.Component):
# Create the torrents dict { torrent_id: Torrent } # Create the torrents dict { torrent_id: Torrent }
self.torrents = {} self.torrents = {}
self.queued_torrents = set() self.queued_torrents = set()
self.is_saving_state = False
# This is a map of torrent_ids to Deferreds used to track needed resume data. # This is a map of torrent_ids to Deferreds used to track needed resume data.
# The Deferreds will be completed when resume data has been saved. # The Deferreds will be completed when resume data has been saved.
@ -194,6 +195,7 @@ class TorrentManager(component.Component):
self.save_resume_data_timer.start(190, False) self.save_resume_data_timer.start(190, False)
self.prev_status_cleanup_loop.start(10) self.prev_status_cleanup_loop.start(10)
@defer.inlineCallbacks
def stop(self): def stop(self):
# Stop timers # Stop timers
if self.save_state_timer.running: if self.save_state_timer.running:
@ -206,19 +208,15 @@ class TorrentManager(component.Component):
self.prev_status_cleanup_loop.stop() self.prev_status_cleanup_loop.stop()
# Save state on shutdown # Save state on shutdown
self.save_state() yield self.save_state()
self.session.pause() self.session.pause()
def remove_temp_file(result): result = yield self.save_resume_data(flush_disk_cache=True)
"""Remove the temp_file to signify successfully saved state""" # Remove the temp_file to signify successfully saved state
if result and os.path.isfile(self.temp_file): if result and os.path.isfile(self.temp_file):
os.remove(self.temp_file) os.remove(self.temp_file)
d = self.save_resume_data(flush_disk_cache=True)
d.addCallback(remove_temp_file)
return d
def update(self): def update(self):
for torrent_id, torrent in self.torrents.items(): for torrent_id, torrent in self.torrents.items():
# XXX: Should the state check be those that _can_ be stopped at ratio # XXX: Should the state check be those that _can_ be stopped at ratio
@ -647,6 +645,25 @@ class TorrentManager(component.Component):
return state return state
def save_state(self): def save_state(self):
"""
Run the save state task in a separate thread to avoid blocking main thread.
If a save task is already running, this call is ignored.
"""
if self.is_saving_state:
return defer.succeed(None)
self.is_saving_state = True
d = threads.deferToThread(self._save_state)
def on_state_saved(arg):
self.is_saving_state = False
if self.save_state_timer.running:
self.save_state_timer.reset()
d.addBoth(on_state_saved)
return d
def _save_state(self):
"""Save the state of the TorrentManager to the torrents.state file.""" """Save the state of the TorrentManager to the torrents.state file."""
state = self.create_state() state = self.create_state()
if not state.torrents: if not state.torrents: