Skip to content

Commit

Permalink
Add Simple Storage Component
Browse files Browse the repository at this point in the history
  • Loading branch information
drew2a committed Jan 18, 2022
1 parent fed9255 commit 1158869
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 0 deletions.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import asyncio
import logging

from pydantic import BaseModel

from tribler_core.utilities.path_util import Path

DEFAULT_SAVE_INTERVAL = 5 * 60 # force Storage to save data every 5 minutes


class StorageData(BaseModel):
last_processed_torrent_id: int = 0


class SimpleStorage:
""" SimpleStorage is object storage that stores data in JSON format and uses
`pydantic` `BaseModel` for defining models.
It stores data on a shutdown and every 5 minutes.
"""
def __init__(self, path: Path, save_interval: float = DEFAULT_SAVE_INTERVAL):
"""
Args:
path: path to the file with storage. Could be a path to a non existent file.
save_interval: interval in seconds in which the storage will store a data to a disk.
"""
self.logger = logging.getLogger(self.__class__.__name__)
self.data = StorageData()

self.path = path
self.save_interval = save_interval

self._loop = asyncio.get_event_loop()
self._task: asyncio.TimerHandle = self._loop.call_later(self.save_interval, self._save_and_schedule_next)

def load(self):
""" Load data from `self.path`. In case the file doesn't exists, the function
will create the data with defaults values.
"""
self.logger.info(f'Loading storage from {self.path}')
try:
self.data = StorageData.parse_file(self.path)
except FileNotFoundError:
self.logger.info('The storage file does not exists. Create a new storage.')
self.data = StorageData()
self.logger.info(f'Loaded storage: {self.data}')

def save(self):
""" Save data to the `self.path`.
"""
self.logger.info(f'Saving storage to: {self.path}.\nStorage {self.data}')
self.path.write_text(self.data.json())

def _save_and_schedule_next(self):
""" Save data and schedule the next call of save function after `self.save_interval`
"""
self.save()
self._task = self._loop.call_later(self.save_interval, self._save_and_schedule_next)

def shutdown(self):
self._task.cancel()
self.save()
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from tribler_core.components.base import Component
from tribler_core.components.simple_storage.simple_storage import SimpleStorage


class SimpleStorageComponent(Component):
"""Storage is aimed to store the limited amount of data. It is not speed efficient.
"""

storage: SimpleStorage = None

async def run(self):
await super().run()

path = self.session.config.state_dir / 'storage.json'
self.storage = SimpleStorage(path)
self.storage.load()

async def shutdown(self):
await super().shutdown()
if self.storage:
self.storage.shutdown()
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import asyncio
from unittest.mock import Mock

import pytest

from tribler_core.components.simple_storage.simple_storage import DEFAULT_SAVE_INTERVAL, SimpleStorage, StorageData


# pylint: disable=protected-access


@pytest.fixture
def simple_storage(tmp_path):
return SimpleStorage(path=tmp_path / 'storage.json')


def test_constructor(simple_storage: SimpleStorage):
assert simple_storage.logger
assert simple_storage.data == StorageData()
assert simple_storage.path
assert simple_storage.save_interval == DEFAULT_SAVE_INTERVAL
assert simple_storage._loop
assert simple_storage._task


def test_load_missed_file(simple_storage: SimpleStorage):
# test that in case of missed path file, default values will be created
assert not simple_storage.path.exists()
simple_storage.data.last_processed_torrent_id = 100

simple_storage.load()
assert simple_storage.data.last_processed_torrent_id == 0


def test_load(simple_storage: SimpleStorage):
# test that in case of existed file, values will be loaded from file
simple_storage.data.last_processed_torrent_id = 100
simple_storage.save()

simple_storage.data.last_processed_torrent_id = 1
simple_storage.load()
assert simple_storage.data.last_processed_torrent_id == 100


def test_shutdown(simple_storage: SimpleStorage):
# test that on shutdown values have been saved and task has been cancelled
simple_storage.data.last_processed_torrent_id = 100
simple_storage.shutdown()

assert simple_storage.path.exists()
assert simple_storage._task.cancelled()


@pytest.mark.asyncio
async def test_save_and_schedule_next(tmp_path):
# In this test we will set up save_interval as 0.1 sec, then wait for 1 sec
# and count how many times function `save` will be called.
storage = SimpleStorage(path=tmp_path / 'storage.json', save_interval=0.1)
storage.save = Mock()
await asyncio.sleep(1)
assert 8 <= storage.save.call_count <= 10
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from contextlib import asynccontextmanager

import pytest

from tribler_core.components.base import Session
from tribler_core.components.simple_storage.simple_storage_component import SimpleStorageComponent


# pylint: disable=protected-access

@asynccontextmanager
async def in_session(session):
try:
yield await session.start()
finally:
await session.shutdown()


@pytest.mark.asyncio
async def test_simple_storage_component(tribler_config):
# Test that component could be created without errors
async with Session(tribler_config, [SimpleStorageComponent()]).start():
comp = SimpleStorageComponent.instance()
assert comp.started_event.is_set() and not comp.failed

0 comments on commit 1158869

Please sign in to comment.