#  Copyright 2015-2016 Palo Alto Networks, Inc
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

"""
This module implements minemeld.ft.basepoller.BasePollerFT, a base class for
miners retrieving indicators by periodically polling an external source.
"""

import logging
import copy
import random
import collections
import sys
import shutil

import gevent
import gevent.event
import gevent.queue

from . import base
from . import ft_states
from .table import Table
from .utils import utc_millisec
from .utils import RWLock
from .utils import parse_age_out

LOG = logging.getLogger(__name__)

_MAX_AGE_OUT = ((1 << 32)-1)*1000  # 2106-02-07 6:28:15


class _BaseBPTable(object):
    def __init__(self, table):
        self.table = table

    def get(self, indicator, itype=None):
        return self.table.get(indicator)

    def delete(self, indicator, itype=None):
        self.table.delete(indicator)

    def put(self, indicator, value):
        self.table.put(indicator, value)

    def query(self, *args, **kwargs):
        return self.table.query(*args, **kwargs)

    def length(self):
        return self.table.num_indicators

    def close(self):
        self.table.close()

    def __del__(self):
        self.close()


class _BPTable_v0(_BaseBPTable):
    def __init__(self, table):
        super(_BPTable_v0, self).__init__(table)

        self.table.create_index('_age_out')
        self.table.create_index('_withdrawn')
        self.table.create_index('_last_run')


class _BPTable_v1(_BaseBPTable):
    def __init__(self, table, type_in_key):
        super(_BPTable_v1, self).__init__(table)

        self.table.create_index('_age_out')
        self.table.create_index('_withdrawn')
        self.table.create_index('_last_run')

        self.type_in_key = type_in_key

        cmetadata = self.table.get_custom_metadata()
        if cmetadata is None:
            _custom_metadata = dict(version=1, type_in_key=type_in_key)
            self.table.set_custom_metadata(_custom_metadata)
        else:
            if cmetadata.get('type_in_key', None) != self.type_in_key:
                raise RuntimeError('Can\'t change type in key of an existing table')

    def get(self, indicator, itype=None):
        if self.type_in_key:
            indicator = self._type_key(indicator, itype)

        return self.table.get(indicator)

    def delete(self, indicator, itype=None):
        if self.type_in_key:
            indicator = self._type_key(indicator, itype)

        return self.table.delete(indicator)

    def put(self, indicator, value):
        if self.type_in_key:
            itype = value.get('type', None)
            indicator = self._type_key(indicator, itype)

        return self.table.put(indicator, value)

    def query(self, *args, **kwargs):
        if not self.type_in_key:
            return self.table.query(*args, **kwargs)

        if kwargs.get('include_value', False):
            return self._type_key_query_with_value(*args, **kwargs)

        return self._type_key_query(*args, **kwargs)

    def _type_key_query(self, *args, **kwargs):
        for key in self.table.query(*args, **kwargs):
            yield self._type_key_indicator(key)

    def _type_key_query_with_value(self, *args, **kwargs):
        for key, value in self.table.query(*args, **kwargs):
            yield self._type_key_indicator(key), value

    def _type_key(self, indicator, itype):
        if itype is None:
            raise RuntimeError('Type None in table with type in key')

        return u'{}::{}'.format(itype, indicator)

    def _type_key_indicator(self, key):
        return key.split('::', 1)[1]


def _bptable_factory(name, truncate=False, type_in_key=False):
    table = Table(name, truncate=truncate)

    metadata = table.get_custom_metadata()
    if metadata is not None:
        version = metadata.get('version', None)
        if version is None:
            raise RuntimeError('{} - table with metadata but no version'.format(name))

        if version == 1:
            return _BPTable_v1(table, type_in_key=type_in_key)

        raise RuntimeError('{} - table with unknown version: {}'.format(name, version))

    # no metadata, could be a new table or an old one
    if table.num_indicators > 0:
        if type_in_key:
            raise RuntimeError('Old BPtable0 can\'t be used with multiple indicator types')

        return _BPTable_v0(table)

    # new table
    return _BPTable_v1(table, type_in_key=type_in_key)


class IndicatorStatus(object):
    D_MASK = 1
    F_MASK = 2
    A_MASK = 4
    W_MASK = 8

    NX = 0
    NFNANW = D_MASK
    XFNANW = D_MASK | F_MASK
    NFXANW = D_MASK | A_MASK
    XFXANW = D_MASK | F_MASK | A_MASK
    NFNAXW = D_MASK | W_MASK
    XFNAXW = D_MASK | F_MASK | W_MASK
    NFXAXW = D_MASK | A_MASK | W_MASK
    XFXAXW = D_MASK | F_MASK | A_MASK | W_MASK

    def __init__(self, indicator, attributes, itable, now, in_feed_threshold):
        self.state = 0

        self.cv = itable.get(indicator, itype=attributes.get('type', None))
        if self.cv is None:
            return
        self.state = self.state | IndicatorStatus.D_MASK

        if self.cv['_age_out'] < now:
            self.state = self.state | IndicatorStatus.A_MASK

        if self.cv['_last_run'] >= in_feed_threshold:
            self.state = self.state | IndicatorStatus.F_MASK

        if self.cv.get('_withdrawn', None) is not None:
            self.state = self.state | IndicatorStatus.W_MASK


class BasePollerFT(base.BaseFT):
    """Implements base class for polling miners.

    **Config parameters**
        :source_name: name of the source. This is placed in the
            *sources* attribute of the generated indicators. Default: name
            of the node.
        :attributes: dictionary of attributes for the generated indicators.
            This dictionary is used as template for the value of the generated
            indicators. Default: empty
        :interval: polling interval in seconds. Default: 3600.
        :num_retries: in case of failure, how many times the miner should
            try to reach the source. If this number is exceeded, the miner
            waits until the next polling time to try again. Default: 2
        :age_out: age out policies to apply to the indicators.
            Default: age out check interval 3600 seconds, sudden death enabled,
            default age out interval 30 days.

    **Age out policy**
        Age out policy is described by a dictionary with at least 3 keys:

        :interval: number of seconds between successive age out checks.
        :sudden_death: boolean, if *true* indicators are immediately aged out
            when they disappear from the feed.
        :default: age out interval. After this interval an indicator is aged
            out even if it is still present in the feed. If *null*, no age out
            interval is applied.

        Additional keys can be used to specify age out interval per indicator
        *type*.

    **Age out interval**
        Age out intervals have the following format::

            <base attribute>+<interval>

        *base attribute* can be *last_seen*, if the age out interval should be
        calculated based on the last time the indicator was found in the feed,
        or *first_seen*, if instead the age out interval should be based on the
        time the indicator was first seen in the feed. If not specified
        *first_seen* is used.

        *interval* is the length of the interval expressed in seconds. Suffixes
        *d*, *h* and *m* can be used to specify days, hours or minutes.

    Example:
        Example config in YAML for a feed where indicators should be aged out
        only when they are removed from the feed::

            source_name: example.persistent_feed
            interval: 600
            age_out:
                default: null
                sudden_death: true
                interval: 300
            attributes:
                type: IPv4
                confidence: 100
                share_level: green
                direction: inbound

        Example config in YAML for a feed where indicators are aged out when
        they disappear from the feed and 30 days after they have seen for the
        first time in the feed::

            source_name: example.long_running_feed
            interval: 3600
            age_out:
                default: first_seen+30d
                sudden_death: true
                interval: 1800
            attributes:
                type: URL
                confidence: 50
                share_level: green

        Example config in YAML for a feed where indicators are aged 30 days
        after they have seen for the last time in the feed::

            source_name: example.delta_feed
            interval: 3600
            age_out:
                default: last_seen+30d
                sudden_death: false
                interval: 1800
            attributes:
                type: URL
                confidence: 50
                share_level: green

    Args:
        name (str): node name, should be unique inside the graph
        chassis (object): parent chassis instance
        config (dict): node config.
    """

    _AGE_OUT_BASES = None
    _DEFAULT_AGE_OUT_BASE = None

    def __init__(self, name, chassis, config):
        self.table = None
        self.agg_table = None

        self._actor_queue = gevent.queue.Queue(maxsize=128)
        self._actor_glet = None
        self._actor_commands_ts = collections.defaultdict(int)
        self._poll_glet = None
        self._age_out_glet = None
        self._emit_counter = 0

        self.last_run = None
        self.last_successful_run = None
        self.last_ageout_run = None
        self._sub_state = None
        self._sub_state_message = None

        self.poll_event = gevent.event.Event()

        self.state_lock = RWLock()

        super(BasePollerFT, self).__init__(name, chassis, config)

    def configure(self):
        super(BasePollerFT, self).configure()

        self.source_name = self.config.get('source_name', self.name)
        self.attributes = self.config.get('attributes', {})
        self.multiple_indicator_types = self.config.get('multiple_indicator_types', False)
        self.interval = self.config.get('interval', 3600)
        self.num_retries = self.config.get('num_retries', 2)

        self.aggregate_indicators = self.config.get('aggregate_indicators', False)
        self.aggregate_use_partial = self.config.get('aggregate_use_partial', False)

        _age_out = self.config.get('age_out', {})

        self.age_out = {
            'interval': _age_out.get('interval', 3600),
            'sudden_death': _age_out.get('sudden_death', True),
            'default': parse_age_out(
                _age_out.get('default', '30d'),
                age_out_bases=self._AGE_OUT_BASES,
                default_base=self._DEFAULT_AGE_OUT_BASE
            )
        }
        for k, v in _age_out.iteritems():
            if k in self.age_out:
                continue
            self.age_out[k] = parse_age_out(v)

    def _saved_state_restore(self, saved_state):
        self.last_run = saved_state.get('last_run', None)
        self.last_successful_run = saved_state.get(
            'last_successful_run',
            None
        )

    def _saved_state_create(self):
        return {
            'last_run': self.last_run,
            'last_successful_run': self.last_successful_run
        }

    def _saved_state_reset(self):
        self.last_successful_run = None
        self.last_run = None

    def _initialize_table(self, truncate=False):
        self.table = _bptable_factory(
            self.name,
            truncate=truncate,
            type_in_key=self.multiple_indicator_types
        )

    def initialize(self):
        self._initialize_table()

    def rebuild(self):
        self._actor_queue.put(
            (utc_millisec(), 'rebuild')
        )
        self._initialize_table(truncate=(self.last_checkpoint is None))

    def reset(self):
        self._saved_state_reset()
        self._initialize_table(truncate=True)

    @base.BaseFT.state.setter
    def state(self, value):
        LOG.debug("%s - acquiring state write lock", self.name)
        self.state_lock.lock()
        #  this is weird ! from stackoverflow 10810369
        super(BasePollerFT, self.__class__).state.fset(self, value)
        self.state_lock.unlock()
        LOG.debug("%s - releasing state write lock", self.name)

    def _controlled_emit_update(self, indicator, value):
        self._emit_counter += 1
        if self._emit_counter == 15937:
            gevent.sleep(0.001)
            self._emit_counter = 0
        self.emit_update(indicator, value)

    def _controlled_emit_withdraw(self, indicator, value):
        self._emit_counter += 1
        if self._emit_counter == 15937:
            gevent.sleep(0.001)
            self._emit_counter = 0
        self.emit_withdraw(indicator=indicator, value=value)

    def _age_out(self):
        with self.state_lock:
            if self.state != ft_states.STARTED:
                return

            try:
                now = utc_millisec()

                for i, v in self.table.query(index='_age_out',
                                             to_key=now-1,
                                             include_value=True):
                    LOG.debug('%s - %s %s aged out', self.name, i, v)

                    if v.get('_withdrawn', None) is not None:
                        continue

                    self._controlled_emit_withdraw(
                        indicator=i,
                        value=v
                    )
                    v['_withdrawn'] = now
                    self.table.put(i, v)

                    self.statistics['aged_out'] += 1

                self.last_ageout_run = now

            except gevent.GreenletExit:
                raise

            except:
                LOG.exception('Exception in _age_out')

    def _flush(self):
        with self.state_lock:
            if self.state != ft_states.STARTED:
                return

            try:
                now = utc_millisec()

                for i, v in self.table.query(include_value=True):
                    if v.get('_withdrawn', None) is not None:
                        continue

                    self._controlled_emit_withdraw(
                        indicator=i,
                        value=v
                    )
                    v['_withdrawn'] = now
                    v['_last_run'] = 0
                    self.table.put(i, v)

                    self.statistics['flushed'] += 1

            except gevent.GreenletExit:
                raise

            except:
                LOG.exception('Exception in _flush')

    def _sudden_death(self):
        if self.last_successful_run is None:
            return

        with self.state_lock:
            if self.state != ft_states.STARTED:
                return

            LOG.debug('checking sudden death for %d', self.last_successful_run)

            for i, v in self.table.query(index='_last_run',
                                         to_key=self.last_successful_run-1,
                                         include_value=True):
                LOG.debug('%s - %s %s sudden death', self.name, i, v)

                v['_age_out'] = self.last_successful_run-1
                self.table.put(i, v)
                self.statistics['removed'] += 1

    def _collect_garbage(self):
        now = utc_millisec()

        with self.state_lock:
            if self.state != ft_states.STARTED:
                return

            for i, v in self.table.query(index='_withdrawn',
                                         to_key=now,
                                         include_value=True):
                if v.get('_last_run', 0) >= (self.last_successful_run-1):
                    continue

                LOG.debug('%s - %s collected', self.name, i)
                self.table.delete(i, itype=v.get('type', None))
                self.statistics['garbage_collected'] += 1

    def _compare_attributes(self, oa, na):
        default_attrs = ['sources', 'last_seen', 'first_seen']
        default_attrs.extend(self.attributes.keys())

        for k in oa:
            if k in default_attrs or k[0] in ('_', '$'):
                continue
            if k not in na:
                return False
        for k in na:
            if oa.get(k, None) != na[k]:
                return False
        return True

    def _update_attributes(self, current, _new, current_run, new_run):
        x = {k:v for k,v in current.iteritems() 
             if k in _new or
                k in self.attributes or
                k in ['sources', 'last_seen', 'first_seen'] or
                k[0] in ('_', '$')}
        x.update(_new)

        return x

    def _aggregate_iterator(self, iterator):
        self.agg_table = _bptable_factory(
            '{}.aggregate-temp'.format(self.name),
            truncate=True,
            type_in_key=True
        )

        for nitem, item in enumerate(iterator):
            if nitem != 0 and nitem % 1024 == 0:
                gevent.sleep(0.001)

            with self.state_lock:
                if self.state != ft_states.STARTED:
                    LOG.info(
                        '%s - state not STARTED, aggregation not performed',
                        self.name
                    )
                    self.agg_table.close()
                    return False

                try:
                    ipairs = self._process_item(item)

                except gevent.GreenletExit:
                    raise

                except:
                    self.statistics['error.parsing'] += 1
                    LOG.exception('%s - Exception parsing %s', self.name, item)
                    continue

                for indicator, attributes in ipairs:
                    self.agg_table.put(indicator, attributes)

        return True

    def _aggregate_process_item(self, item):
        return [item]

    def _polling_loop(self):
        LOG.info("Polling %s", self.name)

        now = utc_millisec()

        with self.state_lock:
            if self.state != ft_states.STARTED:
                LOG.info(
                    '%s - state not STARTED, polling not performed',
                    self.name
                )
                return False

            iterator = self._build_iterator(now)

            if iterator is None:
                return False

        process_item = self._process_item
        aggregation_exc = None
        if self.aggregate_indicators:
            if self.agg_table is not None:
                self.agg_table.close()
                self.agg_table = None

            try:
                if not self._aggregate_iterator(iterator):
                    return False
            except:
                # if aggregate_use_partial is True, we store exception
                # and handle partial results
                if not self.aggregate_use_partial:
                    raise
                LOG.info('{} - Exception during aggregation, storing'.format(self.name))
                aggregation_exc = sys.exc_info()

            process_item = self._aggregate_process_item
            iterator = self.agg_table.query(include_value=True)

        for nitem, item in enumerate(iterator):
            if nitem != 0 and nitem % 1024 == 0:
                gevent.sleep(0.001)

            with self.state_lock:
                if self.state != ft_states.STARTED:
                    break

                try:
                    ipairs = process_item(item)

                except gevent.GreenletExit:
                    raise

                except:
                    self.statistics['error.parsing'] += 1
                    LOG.exception('%s - Exception parsing %s', self.name, item)
                    continue

                for indicator, attributes in ipairs:
                    if indicator is None:
                        LOG.debug('%s - indicator is None for item %s',
                                  self.name, item)
                        continue

                    in_feed_threshold = self.last_successful_run
                    if in_feed_threshold is None:
                        in_feed_threshold = now - self.interval*1000

                    istatus = IndicatorStatus(
                        indicator=indicator,
                        attributes=attributes,
                        itable=self.table,
                        now=now,
                        in_feed_threshold=in_feed_threshold
                    )

                    if istatus.state in [IndicatorStatus.NX,
                                         IndicatorStatus.NFNANW,
                                         IndicatorStatus.NFXANW,
                                         IndicatorStatus.NFXAXW,
                                         IndicatorStatus.NFNAXW]:
                        v = copy.copy(self.attributes)
                        v['sources'] = [self.source_name]
                        v['last_seen'] = now
                        v['first_seen'] = now
                        v['_last_run'] = now
                        v.update(attributes)
                        v['_age_out'] = self._calc_age_out(indicator, v)

                        self.statistics['added'] += 1
                        self.table.put(indicator, v)

                        if v['_age_out'] >= now:
                            self._controlled_emit_update(indicator, v)

                        LOG.debug('%s - added %s %s', self.name, indicator, v)

                    elif istatus.state == IndicatorStatus.XFNANW:
                        v = istatus.cv

                        eq = self._compare_attributes(v, attributes)

                        old_last_run = v['_last_run']
                        v['_last_run'] = now

                        v = self._update_attributes(
                            v, attributes,
                            old_last_run, now
                        )

                        v['_age_out'] = self._calc_age_out(indicator, v)

                        self.table.put(indicator, v)

                        # emit updates if different and not aged out
                        if not eq and v['_age_out'] >= now:
                            self._controlled_emit_update(indicator, v)

                    elif istatus.state == IndicatorStatus.XFXANW:
                        v = istatus.cv
                        v['_last_run'] = now
                        self.table.put(indicator, v)

                    elif istatus.state in [IndicatorStatus.XFXAXW,
                                           IndicatorStatus.XFNAXW]:
                        v = istatus.cv
                        v['_last_run'] = now
                        v['_withdrawn'] = now
                        self.table.put(indicator, v)

                    else:
                        LOG.error('%s - indicator state unhandled: %s',
                                  self.name, istatus.state)
                        continue

        if self.agg_table is not None:
            iterator.close()
            self.agg_table.close()
            self.agg_table = None
            shutil.rmtree('{}.aggregate-temp'.format(self.name))

        if aggregation_exc is not None:
            LOG.info('{} - Reraising exception happened during aggregation'.format(self.name))
            raise aggregation_exc[0], aggregation_exc[1], aggregation_exc[2]

        return True

    def _rebuild(self):
        with self.state_lock:
            if self.state != ft_states.STARTED:
                return

            self.sub_state = 'REBUILDING'

            for i, v in self.table.query(include_value=True):
                self._controlled_emit_update(i, v)

    def _poll(self):
        tryn = 0

        while tryn < self.num_retries:
            lastrun = utc_millisec()

            try:
                self.sub_state = 'POLLING'

                performed = self._polling_loop()
                if performed:
                    self.last_successful_run = lastrun

                _result = 'SUCCESS'
                break

            except gevent.GreenletExit:
                raise

            except Exception as e:
                try:
                    _error_msg = str(e)
                except UnicodeDecodeError:
                    _error_msg = repr(e)

                _result = ('ERROR', _error_msg)

                self.statistics['error.polling'] += 1

                LOG.exception("Exception in polling loop for %s: %s",
                              self.name, str(e))

            tryn += 1
            gevent.sleep(random.randint(1, 5))

        LOG.debug("%s - End of polling - #indicators: %d",
                  self.name, self.table.length())

        self.last_run = lastrun
        self.sub_state = _result

    def _actor_loop(self):
        while True:
            timestamp, command = self._actor_queue.get()
            LOG.info('%s - command: %d %s', self.name, timestamp, command)

            try:
                last_ts = self._actor_commands_ts[command]
                if timestamp < last_ts:
                    LOG.info(
                        '%s - command %s, old timestamp - ignored',
                        self.name,
                        command
                    )
                    continue

                if command == 'poll':
                    self._poll()

                elif command == 'age_out':
                    self._age_out()

                elif command == 'sudden_death':
                    self._sudden_death()

                elif command == 'gc':
                    self._collect_garbage()

                elif command == 'rebuild':
                    self._rebuild()

                elif command == 'flush':
                    self._flush()

                else:
                    LOG.error('%s - unknown command: %s', self.name, command)

            except gevent.GreenletExit:
                raise

            except:
                LOG.exception(
                    '%s - exception executing command %s', self.name, command
                )

            self._actor_commands_ts[command] = utc_millisec()

    def _poll_loop(self):
        # wait to poll until after the first ageout run
        while self.last_ageout_run is None:
            gevent.sleep(1)

        # if last_run is not None it means we have restored
        # a previous state, wait until poll time
        if self.last_run is not None:
            self.sub_state = 'WAITING'

            LOG.info(
                '%s - restored last run, waiting until the next poll time',
                self.name
            )

            try:
                self._huppable_wait(
                    (self.last_run+self.interval*1000)-utc_millisec()
                )
            except gevent.GreenletExit:
                return

        while True:
            with self.state_lock:
                if self.state != ft_states.STARTED:
                    break

            self._actor_queue.put(
                (utc_millisec(), 'poll')
            )

            if self.age_out['sudden_death']:
                self._actor_queue.put(
                    (utc_millisec(), 'sudden_death')
                )

            self._actor_queue.put(
                (utc_millisec(), 'age_out')
            )
            self._actor_queue.put(
                (utc_millisec(), 'gc')
            )

            try:
                self._huppable_wait(self.interval*1000)
            except gevent.GreenletExit:
                break

    def _age_out_loop(self):
        while True:
            with self.state_lock:
                if self.state != ft_states.STARTED:
                    break

            self._actor_queue.put(
                (utc_millisec(), 'age_out')
            )

            if self.age_out['interval'] is None:
                break

            try:
                gevent.sleep(self.age_out['interval'])
            except gevent.GreenletExit:
                break

    def _calc_age_out(self, indicator, attributes):
        t = attributes.get('type', None)
        if t is None or t not in self.age_out:
            sel = self.age_out['default']
        else:
            sel = self.age_out[t]

        if sel is None:
            return _MAX_AGE_OUT

        b = attributes[sel['base']]

        return b + sel['offset']

    def _huppable_wait(self, deltat):
        while deltat < 0:
            LOG.warning(
                'Time for processing exceeded interval for %s',
                self.name
            )
            deltat += self.interval*1000

        LOG.info('hup is clear: %r', self.poll_event.is_set())
        hup_called = self.poll_event.wait(timeout=deltat/1000.0)
        if hup_called:
            LOG.debug('%s - clearing poll event', self.name)
            self.poll_event.clear()

    def mgmtbus_status(self):
        result = super(BasePollerFT, self).mgmtbus_status()
        result['last_run'] = self.last_run
        result['last_successful_run'] = self.last_successful_run
        result['sub_state'] = self.sub_state[0]

        if self.sub_state[1] is not None:
            result['sub_state_message'] = self.sub_state[1]

        return result

    def mgmtbus_signal(self, source=None, signal=None, **kwargs):
        if signal != 'flush':
            super(BasePollerFT, self).mgmtbus_signal(
                source=source,
                signal=signal,
                **kwargs
            )

        self._actor_queue.put(
            (utc_millisec(), 'flush')
        )
        self._actor_queue.put(
            (utc_millisec(), 'gc')
        )

    @property
    def sub_state(self):
        return (self._sub_state, self._sub_state_message)

    @sub_state.setter
    def sub_state(self, value):
        if (type(value) == tuple):
            self._sub_state = value[0]
            self._sub_state_message = value[1]
        else:
            self._sub_state = value
            self._sub_state_message = None

        self.publish_status(force=True)

    def hup(self, source=None):
        LOG.info('%s - hup received, force polling', self.name)
        self.poll_event.set()

    def length(self, source=None):
        return self.table.length()

    def start(self):
        super(BasePollerFT, self).start()

        if self._actor_glet is not None:
            return

        self._actor_glet = gevent.spawn(
            self._actor_loop
        )
        self._poll_glet = gevent.spawn_later(
            random.randint(0, 2),
            self._poll_loop
        )

        self._age_out_glet = gevent.spawn(
            self._age_out_loop
        )

    def stop(self):
        super(BasePollerFT, self).stop()

        if self._actor_glet is None:
            return

        self._actor_glet.kill()
        self._poll_glet.kill()
        self._age_out_glet.kill()

        self.table.close()

        LOG.info("%s - # indicators: %d", self.name, self.table.length())

    @staticmethod
    def gc(name, config=None):
        base.BaseFT.gc(name, config=config)
        shutil.rmtree(name, ignore_errors=True)
        shutil.rmtree('{}.aggregate-temp'.format(name), ignore_errors=True)