mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
Drop multithreading support
This is mainly in preparation to moving to an async architecture.
This commit is contained in:
parent
25435ce11d
commit
4930b5f389
6 changed files with 33 additions and 229 deletions
|
|
@ -50,41 +50,6 @@ def test_sync_inexistant_pair(tmpdir, runner):
|
||||||
assert "pair foo does not exist." in result.output.lower()
|
assert "pair foo does not exist." in result.output.lower()
|
||||||
|
|
||||||
|
|
||||||
def test_debug_connections(tmpdir, runner):
|
|
||||||
runner.write_with_general(
|
|
||||||
dedent(
|
|
||||||
"""
|
|
||||||
[pair my_pair]
|
|
||||||
a = "my_a"
|
|
||||||
b = "my_b"
|
|
||||||
collections = null
|
|
||||||
|
|
||||||
[storage my_a]
|
|
||||||
type = "filesystem"
|
|
||||||
path = "{0}/path_a/"
|
|
||||||
fileext = ".txt"
|
|
||||||
|
|
||||||
[storage my_b]
|
|
||||||
type = "filesystem"
|
|
||||||
path = "{0}/path_b/"
|
|
||||||
fileext = ".txt"
|
|
||||||
"""
|
|
||||||
).format(str(tmpdir))
|
|
||||||
)
|
|
||||||
|
|
||||||
tmpdir.mkdir("path_a")
|
|
||||||
tmpdir.mkdir("path_b")
|
|
||||||
|
|
||||||
result = runner.invoke(["discover"])
|
|
||||||
assert not result.exception
|
|
||||||
|
|
||||||
result = runner.invoke(["-vdebug", "sync", "--max-workers=3"])
|
|
||||||
assert "using 3 maximal workers" in result.output.lower()
|
|
||||||
|
|
||||||
result = runner.invoke(["-vdebug", "sync"])
|
|
||||||
assert "using 1 maximal workers" in result.output.lower()
|
|
||||||
|
|
||||||
|
|
||||||
def test_empty_storage(tmpdir, runner):
|
def test_empty_storage(tmpdir, runner):
|
||||||
runner.write_with_general(
|
runner.write_with_general(
|
||||||
dedent(
|
dedent(
|
||||||
|
|
|
||||||
|
|
@ -65,33 +65,6 @@ def app(ctx, config):
|
||||||
main = app
|
main = app
|
||||||
|
|
||||||
|
|
||||||
def max_workers_callback(ctx, param, value):
|
|
||||||
if value == 0 and logging.getLogger("vdirsyncer").level == logging.DEBUG:
|
|
||||||
value = 1
|
|
||||||
|
|
||||||
cli_logger.debug(f"Using {value} maximal workers.")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def max_workers_option(default=0):
|
|
||||||
help = "Use at most this many connections. "
|
|
||||||
if default == 0:
|
|
||||||
help += (
|
|
||||||
'The default is 0, which means "as many as necessary". '
|
|
||||||
"With -vdebug enabled, the default is 1."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
help += f"The default is {default}."
|
|
||||||
|
|
||||||
return click.option(
|
|
||||||
"--max-workers",
|
|
||||||
default=default,
|
|
||||||
type=click.IntRange(min=0, max=None),
|
|
||||||
callback=max_workers_callback,
|
|
||||||
help=help,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def collections_arg_callback(ctx, param, value):
|
def collections_arg_callback(ctx, param, value):
|
||||||
"""
|
"""
|
||||||
Expand the various CLI shortforms ("pair, pair/collection") to an iterable
|
Expand the various CLI shortforms ("pair, pair/collection") to an iterable
|
||||||
|
|
@ -126,10 +99,9 @@ collections_arg = click.argument(
|
||||||
"to be deleted from both sides."
|
"to be deleted from both sides."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@max_workers_option()
|
|
||||||
@pass_context
|
@pass_context
|
||||||
@catch_errors
|
@catch_errors
|
||||||
def sync(ctx, collections, force_delete, max_workers):
|
def sync(ctx, collections, force_delete):
|
||||||
"""
|
"""
|
||||||
Synchronize the given collections or pairs. If no arguments are given, all
|
Synchronize the given collections or pairs. If no arguments are given, all
|
||||||
will be synchronized.
|
will be synchronized.
|
||||||
|
|
@ -151,53 +123,36 @@ def sync(ctx, collections, force_delete, max_workers):
|
||||||
vdirsyncer sync bob/first_collection
|
vdirsyncer sync bob/first_collection
|
||||||
"""
|
"""
|
||||||
from .tasks import prepare_pair, sync_collection
|
from .tasks import prepare_pair, sync_collection
|
||||||
from .utils import WorkerQueue
|
|
||||||
|
|
||||||
wq = WorkerQueue(max_workers)
|
for pair_name, collections in collections:
|
||||||
|
prepare_pair(
|
||||||
with wq.join():
|
pair_name=pair_name,
|
||||||
for pair_name, collections in collections:
|
collections=collections,
|
||||||
wq.put(
|
config=ctx.config,
|
||||||
functools.partial(
|
force_delete=force_delete,
|
||||||
prepare_pair,
|
callback=sync_collection,
|
||||||
pair_name=pair_name,
|
)
|
||||||
collections=collections,
|
|
||||||
config=ctx.config,
|
|
||||||
force_delete=force_delete,
|
|
||||||
callback=sync_collection,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
wq.spawn_worker()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
@collections_arg
|
@collections_arg
|
||||||
@max_workers_option()
|
|
||||||
@pass_context
|
@pass_context
|
||||||
@catch_errors
|
@catch_errors
|
||||||
def metasync(ctx, collections, max_workers):
|
def metasync(ctx, collections):
|
||||||
"""
|
"""
|
||||||
Synchronize metadata of the given collections or pairs.
|
Synchronize metadata of the given collections or pairs.
|
||||||
|
|
||||||
See the `sync` command for usage.
|
See the `sync` command for usage.
|
||||||
"""
|
"""
|
||||||
from .tasks import prepare_pair, metasync_collection
|
from .tasks import prepare_pair, metasync_collection
|
||||||
from .utils import WorkerQueue
|
|
||||||
|
|
||||||
wq = WorkerQueue(max_workers)
|
for pair_name, collections in collections:
|
||||||
|
prepare_pair(
|
||||||
with wq.join():
|
pair_name=pair_name,
|
||||||
for pair_name, collections in collections:
|
collections=collections,
|
||||||
wq.put(
|
config=ctx.config,
|
||||||
functools.partial(
|
callback=metasync_collection,
|
||||||
prepare_pair,
|
)
|
||||||
pair_name=pair_name,
|
|
||||||
collections=collections,
|
|
||||||
config=ctx.config,
|
|
||||||
callback=metasync_collection,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
wq.spawn_worker()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
|
|
@ -210,33 +165,25 @@ def metasync(ctx, collections, max_workers):
|
||||||
"for debugging. This is slow and may crash for broken servers."
|
"for debugging. This is slow and may crash for broken servers."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@max_workers_option(default=1)
|
|
||||||
@pass_context
|
@pass_context
|
||||||
@catch_errors
|
@catch_errors
|
||||||
def discover(ctx, pairs, max_workers, list):
|
def discover(ctx, pairs, list):
|
||||||
"""
|
"""
|
||||||
Refresh collection cache for the given pairs.
|
Refresh collection cache for the given pairs.
|
||||||
"""
|
"""
|
||||||
from .tasks import discover_collections
|
from .tasks import discover_collections
|
||||||
from .utils import WorkerQueue
|
|
||||||
|
|
||||||
config = ctx.config
|
config = ctx.config
|
||||||
wq = WorkerQueue(max_workers)
|
|
||||||
|
|
||||||
with wq.join():
|
for pair_name in pairs or config.pairs:
|
||||||
for pair_name in pairs or config.pairs:
|
pair = config.get_pair(pair_name)
|
||||||
pair = config.get_pair(pair_name)
|
|
||||||
|
|
||||||
wq.put(
|
discover_collections(
|
||||||
functools.partial(
|
status_path=config.general["status_path"],
|
||||||
discover_collections,
|
pair=pair,
|
||||||
status_path=config.general["status_path"],
|
from_cache=False,
|
||||||
pair=pair,
|
list_collections=list,
|
||||||
from_cache=False,
|
)
|
||||||
list_collections=list,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
wq.spawn_worker()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ import string
|
||||||
from configparser import RawConfigParser
|
from configparser import RawConfigParser
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
from click_threading import get_ui_worker
|
|
||||||
|
|
||||||
from .. import exceptions
|
from .. import exceptions
|
||||||
from .. import PROJECT_HOME
|
from .. import PROJECT_HOME
|
||||||
from ..utils import cached_property
|
from ..utils import cached_property
|
||||||
|
|
@ -257,11 +255,7 @@ class PairConfig:
|
||||||
b_name = self.config_b["instance_name"]
|
b_name = self.config_b["instance_name"]
|
||||||
command = conflict_resolution[1:]
|
command = conflict_resolution[1:]
|
||||||
|
|
||||||
def inner():
|
return _resolve_conflict_via_command(a, b, command, a_name, b_name)
|
||||||
return _resolve_conflict_via_command(a, b, command, a_name, b_name)
|
|
||||||
|
|
||||||
ui_worker = get_ui_worker()
|
|
||||||
return ui_worker.put(inner)
|
|
||||||
|
|
||||||
return resolve
|
return resolve
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import functools
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from .. import exceptions
|
from .. import exceptions
|
||||||
|
|
@ -16,15 +15,13 @@ from .utils import manage_sync_status
|
||||||
from .utils import save_status
|
from .utils import save_status
|
||||||
|
|
||||||
|
|
||||||
def prepare_pair(wq, pair_name, collections, config, callback, **kwargs):
|
def prepare_pair(pair_name, collections, config, callback, **kwargs):
|
||||||
pair = config.get_pair(pair_name)
|
pair = config.get_pair(pair_name)
|
||||||
|
|
||||||
all_collections = dict(
|
all_collections = dict(
|
||||||
collections_for_pair(status_path=config.general["status_path"], pair=pair)
|
collections_for_pair(status_path=config.general["status_path"], pair=pair)
|
||||||
)
|
)
|
||||||
|
|
||||||
# spawn one worker less because we can reuse the current one
|
|
||||||
new_workers = -1
|
|
||||||
for collection_name in collections or all_collections:
|
for collection_name in collections or all_collections:
|
||||||
try:
|
try:
|
||||||
config_a, config_b = all_collections[collection_name]
|
config_a, config_b = all_collections[collection_name]
|
||||||
|
|
@ -35,20 +32,12 @@ def prepare_pair(wq, pair_name, collections, config, callback, **kwargs):
|
||||||
pair_name, json.dumps(collection_name), list(all_collections)
|
pair_name, json.dumps(collection_name), list(all_collections)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
new_workers += 1
|
|
||||||
|
|
||||||
collection = CollectionConfig(pair, collection_name, config_a, config_b)
|
collection = CollectionConfig(pair, collection_name, config_a, config_b)
|
||||||
wq.put(
|
callback(collection=collection, general=config.general, **kwargs)
|
||||||
functools.partial(
|
|
||||||
callback, collection=collection, general=config.general, **kwargs
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for _ in range(new_workers):
|
|
||||||
wq.spawn_worker()
|
|
||||||
|
|
||||||
|
|
||||||
def sync_collection(wq, collection, general, force_delete):
|
def sync_collection(collection, general, force_delete):
|
||||||
pair = collection.pair
|
pair = collection.pair
|
||||||
status_name = get_status_name(pair.name, collection.name)
|
status_name = get_status_name(pair.name, collection.name)
|
||||||
|
|
||||||
|
|
@ -87,7 +76,7 @@ def sync_collection(wq, collection, general, force_delete):
|
||||||
raise JobFailed()
|
raise JobFailed()
|
||||||
|
|
||||||
|
|
||||||
def discover_collections(wq, pair, **kwargs):
|
def discover_collections(pair, **kwargs):
|
||||||
rv = collections_for_pair(pair=pair, **kwargs)
|
rv = collections_for_pair(pair=pair, **kwargs)
|
||||||
collections = list(c for c, (a, b) in rv)
|
collections = list(c for c, (a, b) in rv)
|
||||||
if collections == [None]:
|
if collections == [None]:
|
||||||
|
|
@ -128,7 +117,7 @@ def repair_collection(config, collection, repair_unsafe_uid):
|
||||||
repair_storage(storage, repair_unsafe_uid=repair_unsafe_uid)
|
repair_storage(storage, repair_unsafe_uid=repair_unsafe_uid)
|
||||||
|
|
||||||
|
|
||||||
def metasync_collection(wq, collection, general):
|
def metasync_collection(collection, general):
|
||||||
from ..metasync import metasync
|
from ..metasync import metasync
|
||||||
|
|
||||||
pair = collection.pair
|
pair = collection.pair
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
import contextlib
|
import contextlib
|
||||||
import errno
|
import errno
|
||||||
import importlib
|
import importlib
|
||||||
import itertools
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import queue
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import click_threading
|
|
||||||
from atomicwrites import atomic_write
|
from atomicwrites import atomic_write
|
||||||
|
|
||||||
from . import cli_logger
|
from . import cli_logger
|
||||||
|
|
@ -311,92 +308,6 @@ def handle_storage_init_error(cls, config):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WorkerQueue:
|
|
||||||
"""
|
|
||||||
A simple worker-queue setup.
|
|
||||||
|
|
||||||
Note that workers quit if queue is empty. That means you have to first put
|
|
||||||
things into the queue before spawning the worker!
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, max_workers):
|
|
||||||
self._queue = queue.Queue()
|
|
||||||
self._workers = []
|
|
||||||
self._max_workers = max_workers
|
|
||||||
self._shutdown_handlers = []
|
|
||||||
|
|
||||||
# According to http://stackoverflow.com/a/27062830, those are
|
|
||||||
# threadsafe compared to increasing a simple integer variable.
|
|
||||||
self.num_done_tasks = itertools.count()
|
|
||||||
self.num_failed_tasks = itertools.count()
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
while self._shutdown_handlers:
|
|
||||||
try:
|
|
||||||
self._shutdown_handlers.pop()()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _worker(self):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
func = self._queue.get(False)
|
|
||||||
except queue.Empty:
|
|
||||||
break
|
|
||||||
|
|
||||||
try:
|
|
||||||
func(wq=self)
|
|
||||||
except Exception:
|
|
||||||
handle_cli_error()
|
|
||||||
next(self.num_failed_tasks)
|
|
||||||
finally:
|
|
||||||
self._queue.task_done()
|
|
||||||
next(self.num_done_tasks)
|
|
||||||
if not self._queue.unfinished_tasks:
|
|
||||||
self.shutdown()
|
|
||||||
|
|
||||||
def spawn_worker(self):
|
|
||||||
if self._max_workers and len(self._workers) >= self._max_workers:
|
|
||||||
return
|
|
||||||
|
|
||||||
t = click_threading.Thread(target=self._worker)
|
|
||||||
t.start()
|
|
||||||
self._workers.append(t)
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def join(self):
|
|
||||||
assert self._workers or not self._queue.unfinished_tasks
|
|
||||||
ui_worker = click_threading.UiWorker()
|
|
||||||
self._shutdown_handlers.append(ui_worker.shutdown)
|
|
||||||
_echo = click.echo
|
|
||||||
|
|
||||||
with ui_worker.patch_click():
|
|
||||||
yield
|
|
||||||
|
|
||||||
if not self._workers:
|
|
||||||
# Ugly hack, needed because ui_worker is not running.
|
|
||||||
click.echo = _echo
|
|
||||||
cli_logger.critical("Nothing to do.")
|
|
||||||
sys.exit(5)
|
|
||||||
|
|
||||||
ui_worker.run()
|
|
||||||
self._queue.join()
|
|
||||||
for worker in self._workers:
|
|
||||||
worker.join()
|
|
||||||
|
|
||||||
tasks_failed = next(self.num_failed_tasks)
|
|
||||||
tasks_done = next(self.num_done_tasks)
|
|
||||||
|
|
||||||
if tasks_failed > 0:
|
|
||||||
cli_logger.error(
|
|
||||||
"{} out of {} tasks failed.".format(tasks_failed, tasks_done)
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def put(self, f):
|
|
||||||
return self._queue.put(f)
|
|
||||||
|
|
||||||
|
|
||||||
def assert_permissions(path, wanted):
|
def assert_permissions(path, wanted):
|
||||||
permissions = os.stat(path).st_mode & 0o777
|
permissions = os.stat(path).st_mode & 0o777
|
||||||
if permissions > wanted:
|
if permissions > wanted:
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import urllib.parse as urlparse
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from atomicwrites import atomic_write
|
from atomicwrites import atomic_write
|
||||||
from click_threading import get_ui_worker
|
|
||||||
|
|
||||||
from . import base
|
from . import base
|
||||||
from . import dav
|
from . import dav
|
||||||
|
|
@ -41,8 +40,7 @@ class GoogleSession(dav.DAVSession):
|
||||||
raise exceptions.UserError("requests-oauthlib not installed")
|
raise exceptions.UserError("requests-oauthlib not installed")
|
||||||
|
|
||||||
token_file = expand_path(token_file)
|
token_file = expand_path(token_file)
|
||||||
ui_worker = get_ui_worker()
|
return self._init_token(token_file, client_id, client_secret)
|
||||||
ui_worker.put(lambda: self._init_token(token_file, client_id, client_secret))
|
|
||||||
|
|
||||||
def _init_token(self, token_file, client_id, client_secret):
|
def _init_token(self, token_file, client_id, client_secret):
|
||||||
token = None
|
token = None
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue