2022-09-30 11:40:22 +03:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-04-08 03:10:44 +03:00
|
|
|
import logging
|
|
|
|
import time
|
2022-04-29 18:16:39 +03:00
|
|
|
from dataclasses import dataclass, field
|
2022-05-24 23:21:14 +03:00
|
|
|
from typing import Any, Awaitable, Callable, Collection, Dict, List, Optional
|
|
|
|
|
|
|
|
from typing_extensions import Protocol
|
2022-04-08 03:10:44 +03:00
|
|
|
|
2023-07-18 20:36:12 +03:00
|
|
|
from chia.consensus.pos_quality import UI_ACTUAL_SPACE_CONSTANT_FACTOR, _expected_plot_size
|
2022-04-08 03:10:44 +03:00
|
|
|
from chia.plot_sync.delta import Delta, PathListDelta, PlotListDelta
|
|
|
|
from chia.plot_sync.exceptions import (
|
|
|
|
InvalidIdentifierError,
|
|
|
|
InvalidLastSyncIdError,
|
|
|
|
PlotAlreadyAvailableError,
|
|
|
|
PlotNotAvailableError,
|
|
|
|
PlotSyncException,
|
|
|
|
SyncIdsMatchError,
|
|
|
|
)
|
2022-07-07 05:02:09 +03:00
|
|
|
from chia.plot_sync.util import ErrorCodes, State, T_PlotSyncMessage
|
2022-04-08 03:10:44 +03:00
|
|
|
from chia.protocols.harvester_protocol import (
|
|
|
|
Plot,
|
|
|
|
PlotSyncDone,
|
|
|
|
PlotSyncError,
|
|
|
|
PlotSyncIdentifier,
|
|
|
|
PlotSyncPathList,
|
|
|
|
PlotSyncPlotList,
|
|
|
|
PlotSyncResponse,
|
|
|
|
PlotSyncStart,
|
|
|
|
)
|
2022-11-12 23:18:11 +03:00
|
|
|
from chia.protocols.protocol_message_types import ProtocolMessageTypes
|
|
|
|
from chia.server.outbound_message import make_msg
|
|
|
|
from chia.server.ws_connection import WSChiaConnection
|
2022-04-08 03:10:44 +03:00
|
|
|
from chia.types.blockchain_format.sized_bytes import bytes32
|
2022-04-29 18:16:39 +03:00
|
|
|
from chia.util.ints import int16, uint32, uint64
|
2022-04-29 05:36:21 +03:00
|
|
|
from chia.util.misc import get_list_or_len
|
2022-04-08 03:10:44 +03:00
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2022-04-29 18:16:39 +03:00
|
|
|
@dataclass
|
|
|
|
class Sync:
|
|
|
|
state: State = State.idle
|
|
|
|
sync_id: uint64 = uint64(0)
|
|
|
|
next_message_id: uint64 = uint64(0)
|
|
|
|
plots_processed: uint32 = uint32(0)
|
|
|
|
plots_total: uint32 = uint32(0)
|
|
|
|
delta: Delta = field(default_factory=Delta)
|
2022-04-30 04:34:14 +03:00
|
|
|
time_done: Optional[float] = None
|
|
|
|
|
|
|
|
def in_progress(self) -> bool:
|
|
|
|
return self.sync_id != 0
|
2022-04-29 18:16:39 +03:00
|
|
|
|
|
|
|
def bump_next_message_id(self) -> None:
|
|
|
|
self.next_message_id = uint64(self.next_message_id + 1)
|
|
|
|
|
|
|
|
def bump_plots_processed(self) -> None:
|
|
|
|
self.plots_processed = uint32(self.plots_processed + 1)
|
|
|
|
|
2022-05-25 02:21:56 +03:00
|
|
|
def __str__(self) -> str:
|
|
|
|
return (
|
|
|
|
f"[state {self.state}, "
|
|
|
|
f"sync_id {self.sync_id}, "
|
|
|
|
f"next_message_id {self.next_message_id}, "
|
|
|
|
f"plots_processed {self.plots_processed}, "
|
|
|
|
f"plots_total {self.plots_total}, "
|
|
|
|
f"delta {self.delta}, "
|
|
|
|
f"time_done {self.time_done}]"
|
|
|
|
)
|
|
|
|
|
2022-04-29 18:16:39 +03:00
|
|
|
|
2022-05-24 23:21:14 +03:00
|
|
|
class ReceiverUpdateCallback(Protocol):
|
|
|
|
def __call__(self, peer_id: bytes32, delta: Optional[Delta]) -> Awaitable[None]:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2022-04-08 03:10:44 +03:00
|
|
|
class Receiver:
|
|
|
|
_connection: WSChiaConnection
|
2022-04-29 18:16:39 +03:00
|
|
|
_current_sync: Sync
|
|
|
|
_last_sync: Sync
|
2022-04-08 03:10:44 +03:00
|
|
|
_plots: Dict[str, Plot]
|
|
|
|
_invalid: List[str]
|
|
|
|
_keys_missing: List[str]
|
|
|
|
_duplicates: List[str]
|
2022-04-30 04:34:14 +03:00
|
|
|
_total_plot_size: int
|
2023-07-18 20:36:12 +03:00
|
|
|
_total_effective_plot_size: int
|
2022-05-24 23:21:14 +03:00
|
|
|
_update_callback: ReceiverUpdateCallback
|
2022-04-08 03:10:44 +03:00
|
|
|
|
|
|
|
def __init__(
|
2022-05-04 01:33:38 +03:00
|
|
|
self,
|
|
|
|
connection: WSChiaConnection,
|
2022-05-24 23:21:14 +03:00
|
|
|
update_callback: ReceiverUpdateCallback,
|
2022-04-08 03:10:44 +03:00
|
|
|
) -> None:
|
|
|
|
self._connection = connection
|
2022-04-29 18:16:39 +03:00
|
|
|
self._current_sync = Sync()
|
|
|
|
self._last_sync = Sync()
|
2022-04-08 03:10:44 +03:00
|
|
|
self._plots = {}
|
|
|
|
self._invalid = []
|
|
|
|
self._keys_missing = []
|
|
|
|
self._duplicates = []
|
2022-04-30 04:34:14 +03:00
|
|
|
self._total_plot_size = 0
|
2023-07-18 20:36:12 +03:00
|
|
|
self._total_effective_plot_size = 0
|
2022-05-24 23:21:14 +03:00
|
|
|
self._update_callback = update_callback
|
2022-04-08 03:10:44 +03:00
|
|
|
|
2022-05-04 01:33:38 +03:00
|
|
|
async def trigger_callback(self, update: Optional[Delta] = None) -> None:
|
|
|
|
try:
|
2022-05-24 23:21:14 +03:00
|
|
|
await self._update_callback(self._connection.peer_node_id, update)
|
2022-05-04 01:33:38 +03:00
|
|
|
except Exception as e:
|
2022-05-25 02:21:56 +03:00
|
|
|
log.error(f"_update_callback: node_id {self.connection().peer_node_id}, raised {e}")
|
2022-05-04 01:33:38 +03:00
|
|
|
|
2022-04-08 03:10:44 +03:00
|
|
|
def reset(self) -> None:
|
2022-09-17 00:21:51 +03:00
|
|
|
log.info(f"reset: node_id {self.connection().peer_node_id}, current_sync: {self._current_sync}")
|
2022-04-29 18:16:39 +03:00
|
|
|
self._current_sync = Sync()
|
|
|
|
self._last_sync = Sync()
|
2022-04-08 03:10:44 +03:00
|
|
|
self._plots.clear()
|
|
|
|
self._invalid.clear()
|
|
|
|
self._keys_missing.clear()
|
|
|
|
self._duplicates.clear()
|
2022-04-30 04:34:14 +03:00
|
|
|
self._total_plot_size = 0
|
2023-07-18 20:36:12 +03:00
|
|
|
self._total_effective_plot_size = 0
|
2022-04-08 03:10:44 +03:00
|
|
|
|
|
|
|
def connection(self) -> WSChiaConnection:
|
|
|
|
return self._connection
|
|
|
|
|
2022-04-29 18:16:39 +03:00
|
|
|
def current_sync(self) -> Sync:
|
|
|
|
return self._current_sync
|
2022-04-08 03:10:44 +03:00
|
|
|
|
2022-04-29 18:16:39 +03:00
|
|
|
def last_sync(self) -> Sync:
|
|
|
|
return self._last_sync
|
2022-04-08 03:10:44 +03:00
|
|
|
|
2022-04-30 04:34:14 +03:00
|
|
|
def initial_sync(self) -> bool:
|
|
|
|
return self._last_sync.sync_id == 0
|
|
|
|
|
2022-04-08 03:10:44 +03:00
|
|
|
def plots(self) -> Dict[str, Plot]:
|
|
|
|
return self._plots
|
|
|
|
|
|
|
|
def invalid(self) -> List[str]:
|
|
|
|
return self._invalid
|
|
|
|
|
|
|
|
def keys_missing(self) -> List[str]:
|
|
|
|
return self._keys_missing
|
|
|
|
|
|
|
|
def duplicates(self) -> List[str]:
|
|
|
|
return self._duplicates
|
|
|
|
|
2022-04-30 04:34:14 +03:00
|
|
|
def total_plot_size(self) -> int:
|
|
|
|
return self._total_plot_size
|
|
|
|
|
2023-07-18 20:36:12 +03:00
|
|
|
def total_effective_plot_size(self) -> int:
|
|
|
|
return self._total_effective_plot_size
|
|
|
|
|
2022-04-08 03:10:44 +03:00
|
|
|
async def _process(
|
2022-07-07 05:02:09 +03:00
|
|
|
self, method: Callable[[T_PlotSyncMessage], Any], message_type: ProtocolMessageTypes, message: T_PlotSyncMessage
|
2022-04-08 03:10:44 +03:00
|
|
|
) -> None:
|
2022-05-25 02:21:56 +03:00
|
|
|
log.debug(
|
|
|
|
f"_process: node_id {self.connection().peer_node_id}, message_type: {message_type}, message: {message}"
|
|
|
|
)
|
|
|
|
|
2022-04-08 03:10:44 +03:00
|
|
|
async def send_response(plot_sync_error: Optional[PlotSyncError] = None) -> None:
|
|
|
|
if self._connection is not None:
|
|
|
|
await self._connection.send_message(
|
|
|
|
make_msg(
|
|
|
|
ProtocolMessageTypes.plot_sync_response,
|
|
|
|
PlotSyncResponse(message.identifier, int16(message_type.value), plot_sync_error),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
try:
|
|
|
|
await method(message)
|
|
|
|
await send_response()
|
|
|
|
except InvalidIdentifierError as e:
|
2022-05-25 02:21:56 +03:00
|
|
|
log.warning(f"_process: node_id {self.connection().peer_node_id}, InvalidIdentifierError {e}")
|
2022-04-08 03:10:44 +03:00
|
|
|
await send_response(PlotSyncError(int16(e.error_code), f"{e}", e.expected_identifier))
|
|
|
|
except PlotSyncException as e:
|
2022-05-25 02:21:56 +03:00
|
|
|
log.warning(f"_process: node_id {self.connection().peer_node_id}, Error {e}")
|
2022-04-08 03:10:44 +03:00
|
|
|
await send_response(PlotSyncError(int16(e.error_code), f"{e}", None))
|
|
|
|
except Exception as e:
|
2022-05-25 02:21:56 +03:00
|
|
|
log.warning(f"_process: node_id {self.connection().peer_node_id}, Exception {e}")
|
2022-04-08 03:10:44 +03:00
|
|
|
await send_response(PlotSyncError(int16(ErrorCodes.unknown), f"{e}", None))
|
|
|
|
|
|
|
|
def _validate_identifier(self, identifier: PlotSyncIdentifier, start: bool = False) -> None:
|
2022-04-29 18:16:39 +03:00
|
|
|
sync_id_match = identifier.sync_id == self._current_sync.sync_id
|
|
|
|
message_id_match = identifier.message_id == self._current_sync.next_message_id
|
2022-04-08 03:10:44 +03:00
|
|
|
identifier_match = sync_id_match and message_id_match
|
|
|
|
if (start and not message_id_match) or (not start and not identifier_match):
|
|
|
|
expected: PlotSyncIdentifier = PlotSyncIdentifier(
|
2022-04-29 18:16:39 +03:00
|
|
|
identifier.timestamp, self._current_sync.sync_id, self._current_sync.next_message_id
|
2022-04-08 03:10:44 +03:00
|
|
|
)
|
|
|
|
raise InvalidIdentifierError(
|
|
|
|
identifier,
|
|
|
|
expected,
|
|
|
|
)
|
|
|
|
|
|
|
|
async def _sync_started(self, data: PlotSyncStart) -> None:
|
|
|
|
if data.initial:
|
|
|
|
self.reset()
|
|
|
|
self._validate_identifier(data.identifier, True)
|
2022-04-29 18:16:39 +03:00
|
|
|
if data.last_sync_id != self._last_sync.sync_id:
|
|
|
|
raise InvalidLastSyncIdError(data.last_sync_id, self._last_sync.sync_id)
|
2022-04-08 03:10:44 +03:00
|
|
|
if data.last_sync_id == data.identifier.sync_id:
|
|
|
|
raise SyncIdsMatchError(State.idle, data.last_sync_id)
|
2022-04-29 18:16:39 +03:00
|
|
|
self._current_sync.sync_id = data.identifier.sync_id
|
|
|
|
self._current_sync.delta.clear()
|
|
|
|
self._current_sync.state = State.loaded
|
|
|
|
self._current_sync.plots_total = data.plot_file_count
|
|
|
|
self._current_sync.bump_next_message_id()
|
2022-04-08 03:10:44 +03:00
|
|
|
|
|
|
|
async def sync_started(self, data: PlotSyncStart) -> None:
|
|
|
|
await self._process(self._sync_started, ProtocolMessageTypes.plot_sync_start, data)
|
|
|
|
|
|
|
|
async def _process_loaded(self, plot_infos: PlotSyncPlotList) -> None:
|
|
|
|
self._validate_identifier(plot_infos.identifier)
|
|
|
|
|
|
|
|
for plot_info in plot_infos.data:
|
2022-04-29 18:16:39 +03:00
|
|
|
if plot_info.filename in self._plots or plot_info.filename in self._current_sync.delta.valid.additions:
|
2022-04-08 03:10:44 +03:00
|
|
|
raise PlotAlreadyAvailableError(State.loaded, plot_info.filename)
|
2022-04-29 18:16:39 +03:00
|
|
|
self._current_sync.delta.valid.additions[plot_info.filename] = plot_info
|
|
|
|
self._current_sync.bump_plots_processed()
|
2022-04-08 03:10:44 +03:00
|
|
|
|
2022-05-04 01:33:38 +03:00
|
|
|
# Let the callback receiver know about the sync progress updates
|
|
|
|
await self.trigger_callback()
|
|
|
|
|
2022-04-08 03:10:44 +03:00
|
|
|
if plot_infos.final:
|
2022-04-29 18:16:39 +03:00
|
|
|
self._current_sync.state = State.removed
|
2022-04-08 03:10:44 +03:00
|
|
|
|
2022-04-29 18:16:39 +03:00
|
|
|
self._current_sync.bump_next_message_id()
|
2022-04-08 03:10:44 +03:00
|
|
|
|
|
|
|
async def process_loaded(self, plot_infos: PlotSyncPlotList) -> None:
|
|
|
|
await self._process(self._process_loaded, ProtocolMessageTypes.plot_sync_loaded, plot_infos)
|
|
|
|
|
|
|
|
async def process_path_list(
|
|
|
|
self,
|
|
|
|
*,
|
|
|
|
state: State,
|
|
|
|
next_state: State,
|
|
|
|
target: Collection[str],
|
|
|
|
delta: List[str],
|
|
|
|
paths: PlotSyncPathList,
|
|
|
|
is_removal: bool = False,
|
|
|
|
) -> None:
|
|
|
|
self._validate_identifier(paths.identifier)
|
|
|
|
|
|
|
|
for path in paths.data:
|
|
|
|
if is_removal and (path not in target or path in delta):
|
|
|
|
raise PlotNotAvailableError(state, path)
|
|
|
|
if not is_removal and path in delta:
|
|
|
|
raise PlotAlreadyAvailableError(state, path)
|
|
|
|
delta.append(path)
|
2022-04-29 18:16:39 +03:00
|
|
|
if not is_removal:
|
|
|
|
self._current_sync.bump_plots_processed()
|
2022-04-08 03:10:44 +03:00
|
|
|
|
2022-05-04 01:33:38 +03:00
|
|
|
# Let the callback receiver know about the sync progress updates
|
|
|
|
await self.trigger_callback()
|
|
|
|
|
2022-04-08 03:10:44 +03:00
|
|
|
if paths.final:
|
2022-04-29 18:16:39 +03:00
|
|
|
self._current_sync.state = next_state
|
2022-04-08 03:10:44 +03:00
|
|
|
|
2022-04-29 18:16:39 +03:00
|
|
|
self._current_sync.bump_next_message_id()
|
2022-04-08 03:10:44 +03:00
|
|
|
|
|
|
|
async def _process_removed(self, paths: PlotSyncPathList) -> None:
|
|
|
|
await self.process_path_list(
|
|
|
|
state=State.removed,
|
|
|
|
next_state=State.invalid,
|
|
|
|
target=self._plots,
|
2022-04-29 18:16:39 +03:00
|
|
|
delta=self._current_sync.delta.valid.removals,
|
2022-04-08 03:10:44 +03:00
|
|
|
paths=paths,
|
|
|
|
is_removal=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
async def process_removed(self, paths: PlotSyncPathList) -> None:
|
|
|
|
await self._process(self._process_removed, ProtocolMessageTypes.plot_sync_removed, paths)
|
|
|
|
|
|
|
|
async def _process_invalid(self, paths: PlotSyncPathList) -> None:
|
|
|
|
await self.process_path_list(
|
|
|
|
state=State.invalid,
|
|
|
|
next_state=State.keys_missing,
|
|
|
|
target=self._invalid,
|
2022-04-29 18:16:39 +03:00
|
|
|
delta=self._current_sync.delta.invalid.additions,
|
2022-04-08 03:10:44 +03:00
|
|
|
paths=paths,
|
|
|
|
)
|
|
|
|
|
|
|
|
async def process_invalid(self, paths: PlotSyncPathList) -> None:
|
|
|
|
await self._process(self._process_invalid, ProtocolMessageTypes.plot_sync_invalid, paths)
|
|
|
|
|
|
|
|
async def _process_keys_missing(self, paths: PlotSyncPathList) -> None:
|
|
|
|
await self.process_path_list(
|
|
|
|
state=State.keys_missing,
|
|
|
|
next_state=State.duplicates,
|
|
|
|
target=self._keys_missing,
|
2022-04-29 18:16:39 +03:00
|
|
|
delta=self._current_sync.delta.keys_missing.additions,
|
2022-04-08 03:10:44 +03:00
|
|
|
paths=paths,
|
|
|
|
)
|
|
|
|
|
|
|
|
async def process_keys_missing(self, paths: PlotSyncPathList) -> None:
|
|
|
|
await self._process(self._process_keys_missing, ProtocolMessageTypes.plot_sync_keys_missing, paths)
|
|
|
|
|
|
|
|
async def _process_duplicates(self, paths: PlotSyncPathList) -> None:
|
|
|
|
await self.process_path_list(
|
|
|
|
state=State.duplicates,
|
|
|
|
next_state=State.done,
|
|
|
|
target=self._duplicates,
|
2022-04-29 18:16:39 +03:00
|
|
|
delta=self._current_sync.delta.duplicates.additions,
|
2022-04-08 03:10:44 +03:00
|
|
|
paths=paths,
|
|
|
|
)
|
|
|
|
|
|
|
|
async def process_duplicates(self, paths: PlotSyncPathList) -> None:
|
|
|
|
await self._process(self._process_duplicates, ProtocolMessageTypes.plot_sync_duplicates, paths)
|
|
|
|
|
|
|
|
async def _sync_done(self, data: PlotSyncDone) -> None:
|
|
|
|
self._validate_identifier(data.identifier)
|
2022-04-29 18:16:39 +03:00
|
|
|
self._current_sync.time_done = time.time()
|
2022-04-08 03:10:44 +03:00
|
|
|
# First create the update delta (i.e. transform invalid/keys_missing into additions/removals) which we will
|
|
|
|
# send to the callback receiver below
|
2022-04-29 18:16:39 +03:00
|
|
|
delta_invalid: PathListDelta = PathListDelta.from_lists(
|
|
|
|
self._invalid, self._current_sync.delta.invalid.additions
|
|
|
|
)
|
2022-04-08 03:10:44 +03:00
|
|
|
delta_keys_missing: PathListDelta = PathListDelta.from_lists(
|
2022-04-29 18:16:39 +03:00
|
|
|
self._keys_missing, self._current_sync.delta.keys_missing.additions
|
|
|
|
)
|
|
|
|
delta_duplicates: PathListDelta = PathListDelta.from_lists(
|
|
|
|
self._duplicates, self._current_sync.delta.duplicates.additions
|
2022-04-08 03:10:44 +03:00
|
|
|
)
|
|
|
|
update = Delta(
|
2022-04-29 18:16:39 +03:00
|
|
|
PlotListDelta(
|
|
|
|
self._current_sync.delta.valid.additions.copy(), self._current_sync.delta.valid.removals.copy()
|
|
|
|
),
|
2022-04-08 03:10:44 +03:00
|
|
|
delta_invalid,
|
|
|
|
delta_keys_missing,
|
|
|
|
delta_duplicates,
|
|
|
|
)
|
|
|
|
# Apply delta
|
2022-04-29 18:16:39 +03:00
|
|
|
self._plots.update(self._current_sync.delta.valid.additions)
|
|
|
|
for removal in self._current_sync.delta.valid.removals:
|
2022-04-08 03:10:44 +03:00
|
|
|
del self._plots[removal]
|
2022-04-29 18:16:39 +03:00
|
|
|
self._invalid = self._current_sync.delta.invalid.additions.copy()
|
|
|
|
self._keys_missing = self._current_sync.delta.keys_missing.additions.copy()
|
|
|
|
self._duplicates = self._current_sync.delta.duplicates.additions.copy()
|
2022-04-30 04:34:14 +03:00
|
|
|
self._total_plot_size = sum(plot.file_size for plot in self._plots.values())
|
2023-07-18 20:36:12 +03:00
|
|
|
self._total_effective_plot_size = int(
|
|
|
|
sum(UI_ACTUAL_SPACE_CONSTANT_FACTOR * int(_expected_plot_size(plot.size)) for plot in self._plots.values())
|
|
|
|
)
|
2022-04-29 18:16:39 +03:00
|
|
|
# Save current sync as last sync and create a new current sync
|
|
|
|
self._last_sync = self._current_sync
|
|
|
|
self._current_sync = Sync()
|
2022-04-08 03:10:44 +03:00
|
|
|
# Let the callback receiver know if this sync cycle caused any update
|
2022-05-04 01:33:38 +03:00
|
|
|
await self.trigger_callback(update)
|
2022-04-08 03:10:44 +03:00
|
|
|
|
|
|
|
async def sync_done(self, data: PlotSyncDone) -> None:
|
|
|
|
await self._process(self._sync_done, ProtocolMessageTypes.plot_sync_done, data)
|
|
|
|
|
2022-04-29 05:36:21 +03:00
|
|
|
def to_dict(self, counts_only: bool = False) -> Dict[str, Any]:
|
2022-04-30 04:34:14 +03:00
|
|
|
syncing = None
|
|
|
|
if self._current_sync.in_progress():
|
|
|
|
syncing = {
|
|
|
|
"initial": self.initial_sync(),
|
|
|
|
"plot_files_processed": self._current_sync.plots_processed,
|
|
|
|
"plot_files_total": self._current_sync.plots_total,
|
|
|
|
}
|
|
|
|
return {
|
2022-04-08 03:10:44 +03:00
|
|
|
"connection": {
|
|
|
|
"node_id": self._connection.peer_node_id,
|
2023-04-12 20:14:38 +03:00
|
|
|
"host": self._connection.peer_info.host,
|
|
|
|
"port": self._connection.peer_info.port,
|
2022-04-08 03:10:44 +03:00
|
|
|
},
|
2022-04-29 05:36:21 +03:00
|
|
|
"plots": get_list_or_len(list(self._plots.values()), counts_only),
|
|
|
|
"failed_to_open_filenames": get_list_or_len(self._invalid, counts_only),
|
|
|
|
"no_key_filenames": get_list_or_len(self._keys_missing, counts_only),
|
|
|
|
"duplicates": get_list_or_len(self._duplicates, counts_only),
|
2022-04-30 04:34:14 +03:00
|
|
|
"total_plot_size": self._total_plot_size,
|
2023-07-18 20:36:12 +03:00
|
|
|
"total_effective_plot_size": self._total_effective_plot_size,
|
2022-04-30 04:34:14 +03:00
|
|
|
"syncing": syncing,
|
|
|
|
"last_sync_time": self._last_sync.time_done,
|
2022-04-08 03:10:44 +03:00
|
|
|
}
|