diff --git a/docs/environment.rst b/docs/environment.rst index 9c9fce4..f5cf512 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -248,12 +248,15 @@ raise them. .. envvar:: COST_SOFT_LIMIT .. envvar:: COST_HARD_LIMIT +.. envvar:: REQUEST_SLEEP +.. envvar:: INITIAL_CONCURRENT - Session cost soft and hard limits as integers. The default values are :const:`500` - and :const:`10,000` respectively. + All values are integers. :envar:`COST_SOFT_LIMIT` defaults to :const:`1,000`, + :envvar:`COST_HARD_LIMIT` to :const:`10,000`, :envvar:`REQUEST_SLEEP` to :const:`2,500` + milliseconds, and :envvar:`INITIAL_CONCURRENT` to :const:`10` concurrent requests. The server prices each request made to it based upon an estimate of the resources needed - to process it . Factors include whether the request uses bitcoind, how much bandwidth + to process it. Factors include whether the request uses bitcoind, how much bandwidth it uses, and how hard it hits the databases. To set a base for the units, a :func:`blockchain.scripthash.subscribe` subscription to @@ -261,14 +264,28 @@ raise them. considering the bandwidth consumed. :func:`server.ping` is costed at :const:`0.1`. As the total cost of a session goes over the soft limit, its requests start to be - throttled in two ways. First, each request sleeps a little before being handled. - Second, the number of requests that the server will handle concurrently reduces. Both - effects increase as the hard limit is approached, at which point the session is - disconnected. - - So non-abusive sessions can continue to be served, a session's cost gradually decays - over time. Subscriptions have an ongoing servicing cost, so the decay is slower as the - number of subscriptions increases. + throttled in two ways. First, the number of requests for that session that the server + will process concurrently is reduced. Second, each request starts to sleep a little + before being handled. + + Before throttling starts, the server will process up to :envvar:`INITIAL_CONCURRENT` + requests concurrently without sleeping. As the session cost ranges from + :envar:`COST_SOFT_LIMIT` to :envvar:`COST_HARD_LIMIT`, concurrency drops linearly to + zero and each request's sleep time increases linearly up to :envvar:`REQUEST_SLEEP` + milliseconds. Once the hard limit is reached, the session is disconnected. + + In order that non-abusive sessions can continue to be served, a session's cost gradually + decays over time. Subscriptions have an ongoing servicing cost, so the decay is slower + as the number of subscriptions increases. + + If a session disconnects, ElectrumX continues to associate its cost with its IP address, + so if it immediately reconnects it will re-acquire its previous cost allocation. + + A server operator should experiment with different values according to server loads. It + is not necessarily true that e.g. having a low soft limit, decreasing concurrency and + increasing sleep will help handling heavy loads, as it will also increase the backlog of + requests the server has to manage in memory. It will also give a much worse experience + for genuine connections. .. envvar:: BANDWIDTH_UNIT_COST diff --git a/electrumx/server/env.py b/electrumx/server/env.py index 1e1afe2..7e93a00 100644 --- a/electrumx/server/env.py +++ b/electrumx/server/env.py @@ -68,9 +68,11 @@ class Env(EnvBase): # Server limits to help prevent DoS self.max_send = self.integer('MAX_SEND', self.coin.DEFAULT_MAX_SEND) self.max_sessions = self.sane_max_sessions() - self.cost_soft_limit = self.integer('COST_SOFT_LIMIT', 500) + self.cost_soft_limit = self.integer('COST_SOFT_LIMIT', 1000) self.cost_hard_limit = self.integer('COST_HARD_LIMIT', 10000) self.bw_unit_cost = self.integer('BANDWIDTH_UNIT_COST', 5000) + self.initial_concurrent = self.integer('INITIAL_CONCURRENT', 10) + self.request_sleep = self.integer('REQUEST_SLEEP', 2500) self.session_timeout = self.integer('SESSION_TIMEOUT', 600) self.drop_client = self.custom("DROP_CLIENT", None, re.compile) self.blacklist_url = self.default('BLACKLIST_URL', self.coin.BLACKLIST_URL) diff --git a/electrumx/server/session.py b/electrumx/server/session.py index adefbf4..901d023 100644 --- a/electrumx/server/session.py +++ b/electrumx/server/session.py @@ -484,12 +484,17 @@ class SessionManager(object): session_class.cost_hard_limit = self.env.cost_hard_limit session_class.cost_decay_per_sec = session_class.cost_hard_limit / 10000 session_class.bw_cost_per_byte = 1.0 / self.env.bw_unit_cost + session_class.cost_sleep = self.env.request_sleep / 1000 + session_class.initial_concurrent = self.env.initial_concurrent self.logger.info(f'max session count: {self.env.max_sessions:,d}') self.logger.info(f'session timeout: {self.env.session_timeout:,d} seconds') self.logger.info(f'session cost hard limit {self.env.cost_hard_limit:,d}') self.logger.info(f'session cost soft limit {self.env.cost_soft_limit:,d}') self.logger.info(f'bandwidth unit cost {self.env.bw_unit_cost:,d}') + self.logger.info(f'request sleep {self.env.request_sleep:,d}ms') + self.logger.info(f'initial concurrent {self.env.initial_concurrent:,d}') + self.logger.info(f'max response size {self.env.max_send:,d} bytes') if self.env.drop_client is not None: self.logger.info('drop clients matching: {}' diff --git a/tests/server/test_env.py b/tests/server/test_env.py index 4c3398f..8289b5b 100644 --- a/tests/server/test_env.py +++ b/tests/server/test_env.py @@ -201,7 +201,15 @@ def test_COST_HARD_LIMIT(): def test_COST_SOFT_LIMIT(): - assert_integer('COST_SOFT_LIMIT', 'cost_soft_limit', 500) + assert_integer('COST_SOFT_LIMIT', 'cost_soft_limit', 1000) + + +def test_INITIAL_CONCURRENT(): + assert_integer('INITIAL_CONCURRENT', 'initial_concurrent', 10) + + +def test_REQUEST_SLEEP(): + assert_integer('REQUEST_SLEEP', 'request_sleep', 2500) def test_BANDWIDTH_UNIT_COST():