201 lines
9.7 KiB
Python
201 lines
9.7 KiB
Python
import os
|
|
import re
|
|
import resource
|
|
import logging
|
|
from collections import namedtuple
|
|
from hub.scribe.network import LBCMainNet, LBCTestNet, LBCRegTest
|
|
|
|
|
|
NetIdentity = namedtuple('NetIdentity', 'host tcp_port ssl_port nick_suffix')
|
|
|
|
|
|
SEGMENT_REGEX = re.compile("(?!-)[A-Z_\\d-]{1,63}(?<!-)$", re.IGNORECASE)
|
|
|
|
|
|
def is_valid_hostname(hostname):
|
|
if len(hostname) > 255:
|
|
return False
|
|
# strip exactly one dot from the right, if present
|
|
if hostname and hostname[-1] == ".":
|
|
hostname = hostname[:-1]
|
|
return all(SEGMENT_REGEX.match(x) for x in hostname.split("."))
|
|
|
|
|
|
class Env:
|
|
|
|
# Peer discovery
|
|
PD_OFF, PD_SELF, PD_ON = range(3)
|
|
|
|
class Error(Exception):
|
|
pass
|
|
|
|
def __init__(self, db_dir=None, max_query_workers=None, chain=None, reorg_limit=None,
|
|
prometheus_port=None, cache_all_tx_hashes=None, cache_all_claim_txos=None,
|
|
blocking_channel_ids=None, filtering_channel_ids=None, index_address_status=None):
|
|
|
|
self.logger = logging.getLogger(__name__)
|
|
self.db_dir = db_dir if db_dir is not None else self.required('DB_DIRECTORY')
|
|
self.obsolete(['UTXO_MB', 'HIST_MB', 'NETWORK'])
|
|
self.max_query_workers = max_query_workers if max_query_workers is not None else self.integer('MAX_QUERY_WORKERS', 4)
|
|
if chain == 'mainnet':
|
|
self.coin = LBCMainNet
|
|
elif chain == 'testnet':
|
|
self.coin = LBCTestNet
|
|
else:
|
|
self.coin = LBCRegTest
|
|
self.reorg_limit = reorg_limit if reorg_limit is not None else self.integer('REORG_LIMIT', self.coin.REORG_LIMIT)
|
|
self.prometheus_port = prometheus_port if prometheus_port is not None else self.integer('PROMETHEUS_PORT', 0)
|
|
self.cache_all_tx_hashes = cache_all_tx_hashes if cache_all_tx_hashes is not None else self.boolean('CACHE_ALL_TX_HASHES', False)
|
|
self.cache_all_claim_txos = cache_all_claim_txos if cache_all_claim_txos is not None else self.boolean('CACHE_ALL_CLAIM_TXOS', False)
|
|
# Filtering / Blocking
|
|
self.blocking_channel_ids = blocking_channel_ids if blocking_channel_ids is not None else self.default(
|
|
'BLOCKING_CHANNEL_IDS', '').split(' ')
|
|
self.filtering_channel_ids = filtering_channel_ids if filtering_channel_ids is not None else self.default(
|
|
'FILTERING_CHANNEL_IDS', '').split(' ')
|
|
self.index_address_status = index_address_status if index_address_status is not None else \
|
|
self.boolean('INDEX_ADDRESS_STATUS', False)
|
|
|
|
@classmethod
|
|
def default(cls, envvar, default):
|
|
return os.environ.get(envvar, default)
|
|
|
|
@classmethod
|
|
def boolean(cls, envvar, default):
|
|
default = 'Yes' if default else ''
|
|
return bool(cls.default(envvar, default).strip())
|
|
|
|
@classmethod
|
|
def required(cls, envvar):
|
|
value = os.environ.get(envvar)
|
|
if value is None:
|
|
raise cls.Error(f'required envvar {envvar} not set')
|
|
return value
|
|
|
|
@classmethod
|
|
def string_amount(cls, envvar, default):
|
|
value = os.environ.get(envvar, default)
|
|
amount_pattern = re.compile("[0-9]{0,10}(\.[0-9]{1,8})?")
|
|
if len(value) > 0 and not amount_pattern.fullmatch(value):
|
|
raise cls.Error(f'{value} is not a valid amount for {envvar}')
|
|
return value
|
|
|
|
@classmethod
|
|
def integer(cls, envvar, default):
|
|
value = os.environ.get(envvar)
|
|
if value is None:
|
|
return default
|
|
try:
|
|
return int(value)
|
|
except Exception:
|
|
raise cls.Error(f'cannot convert envvar {envvar} value {value} to an integer')
|
|
|
|
@classmethod
|
|
def custom(cls, envvar, default, parse):
|
|
value = os.environ.get(envvar)
|
|
if value is None:
|
|
return default
|
|
try:
|
|
return parse(value)
|
|
except Exception as e:
|
|
raise cls.Error(f'cannot parse envvar {envvar} value {value}') from e
|
|
|
|
@classmethod
|
|
def obsolete(cls, envvars):
|
|
bad = [envvar for envvar in envvars if os.environ.get(envvar)]
|
|
if bad:
|
|
raise cls.Error(f'remove obsolete os.environment variables {bad}')
|
|
|
|
def cs_host(self):
|
|
"""Returns the 'host' argument to pass to asyncio's create_server
|
|
call. The result can be a single host name string, a list of
|
|
host name strings, or an empty string to bind to all interfaces.
|
|
|
|
If rpc is True the host to use for the RPC server is returned.
|
|
Otherwise the host to use for SSL/TCP servers is returned.
|
|
"""
|
|
host = self.host
|
|
result = [part.strip() for part in host.split(',')]
|
|
if len(result) == 1:
|
|
result = result[0]
|
|
if result == 'localhost':
|
|
# 'localhost' resolves to ::1 (ipv6) on many systems, which fails on default setup of
|
|
# docker, using 127.0.0.1 instead forces ipv4
|
|
result = '127.0.0.1'
|
|
return result
|
|
|
|
def sane_max_sessions(self):
|
|
"""Return the maximum number of sessions to permit. Normally this
|
|
is MAX_SESSIONS. However, to prevent open file exhaustion, ajdust
|
|
downwards if running with a small open file rlimit."""
|
|
env_value = self.integer('MAX_SESSIONS', 1000)
|
|
nofile_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
|
|
# We give the DB 250 files; allow ElectrumX 100 for itself
|
|
value = max(0, min(env_value, nofile_limit - 350))
|
|
if value < env_value:
|
|
self.logger.warning(f'lowered maximum sessions from {env_value:,d} to {value:,d} '
|
|
f'because your open file limit is {nofile_limit:,d}')
|
|
return value
|
|
|
|
def peer_discovery_enum(self):
|
|
pd = self.default('PEER_DISCOVERY', 'on').strip().lower()
|
|
if pd in ('off', ''):
|
|
return self.PD_OFF
|
|
elif pd == 'self':
|
|
return self.PD_SELF
|
|
else:
|
|
return self.PD_ON
|
|
|
|
def extract_peer_hubs(self):
|
|
peer_hubs = self.default('PEER_HUBS', '')
|
|
if not peer_hubs:
|
|
return []
|
|
return [hub.strip() for hub in peer_hubs.split(',')]
|
|
|
|
@classmethod
|
|
def contribute_to_arg_parser(cls, parser):
|
|
"""
|
|
Settings used by all services
|
|
"""
|
|
|
|
env_db_dir = cls.default('DB_DIRECTORY', None)
|
|
parser.add_argument('--db_dir', type=str, required=env_db_dir is None,
|
|
help="Path of the directory containing lbry-rocksdb. ", default=env_db_dir)
|
|
parser.add_argument('--reorg_limit', default=cls.integer('REORG_LIMIT', 200), type=int, help='Max reorg depth')
|
|
parser.add_argument('--chain', type=str, default=cls.default('NET', 'mainnet'),
|
|
help="Which chain to use, default is mainnet, others are used for testing",
|
|
choices=['mainnet', 'regtest', 'testnet'])
|
|
parser.add_argument('--max_query_workers', type=int, default=cls.integer('MAX_QUERY_WORKERS', 4),
|
|
help="Size of the thread pool. Can be set in env with 'MAX_QUERY_WORKERS'")
|
|
parser.add_argument('--cache_all_tx_hashes', action='store_true',
|
|
help="Load all tx hashes into memory. This will make address subscriptions and sync, "
|
|
"resolve, transaction fetching, and block sync all faster at the expense of higher "
|
|
"memory usage (at least 10GB more). Can be set in env with 'CACHE_ALL_TX_HASHES'.",
|
|
default=cls.boolean('CACHE_ALL_TX_HASHES', False))
|
|
parser.add_argument('--cache_all_claim_txos', action='store_true',
|
|
help="Load all claim txos into memory. This will make address subscriptions and sync, "
|
|
"resolve, transaction fetching, and block sync all faster at the expense of higher "
|
|
"memory usage. Can be set in env with 'CACHE_ALL_CLAIM_TXOS'.",
|
|
default=cls.boolean('CACHE_ALL_CLAIM_TXOS', False))
|
|
parser.add_argument('--prometheus_port', type=int, default=cls.integer('PROMETHEUS_PORT', 0),
|
|
help="Port for prometheus metrics to listen on, disabled by default. "
|
|
"Can be set in env with 'PROMETHEUS_PORT'.")
|
|
parser.add_argument('--blocking_channel_ids', nargs='*',
|
|
help="Space separated list of channel claim ids used for blocking. "
|
|
"Claims that are reposted by these channels can't be resolved "
|
|
"or returned in search results. Can be set in env with 'BLOCKING_CHANNEL_IDS'",
|
|
default=cls.default('BLOCKING_CHANNEL_IDS', '').split(' '))
|
|
parser.add_argument('--filtering_channel_ids', nargs='*',
|
|
help="Space separated list of channel claim ids used for blocking. "
|
|
"Claims that are reposted by these channels aren't returned in search results. "
|
|
"Can be set in env with 'FILTERING_CHANNEL_IDS'",
|
|
default=cls.default('FILTERING_CHANNEL_IDS', '').split(' '))
|
|
parser.add_argument('--index_address_statuses', action='store_true',
|
|
help="Use precomputed address statuses, must be enabled in the reader and the writer to "
|
|
"use it. If disabled (the default), the status of an address must be calculated at "
|
|
"runtime when clients request it (address subscriptions, address history sync). "
|
|
"If enabled, scribe will maintain an index of precomputed statuses",
|
|
default=cls.boolean('INDEX_ADDRESS_STATUS', False))
|
|
|
|
@classmethod
|
|
def from_arg_parser(cls, args):
|
|
raise NotImplementedError()
|