background servicing (temporally executed from failmanager): prevents memory leak on some platforms/python versions, using forced GC in periodic intervals (latency and threshold);

Side effect: GC is disabled now inside fail2ban-server (to avoid multiple garbage collect)
pull/1346/head
sebres 2015-11-23 12:48:07 +01:00
parent a10eb39bbe
commit 6406f6f560
3 changed files with 84 additions and 6 deletions

View File

@ -20,11 +20,16 @@
__author__ = "Cyril Jaquier, Arturo 'Buanzo' Busleiman, Yaroslav Halchenko" __author__ = "Cyril Jaquier, Arturo 'Buanzo' Busleiman, Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
import sys import gc
import os
import traceback
import re
import logging import logging
import os
import re
import sys
import traceback
from threading import Lock
from .server.mytime import MyTime
def formatExceptionInfo(): def formatExceptionInfo():
@ -137,3 +142,48 @@ def splitcommaspace(s):
if not s: if not s:
return [] return []
return filter(bool, re.split('[ ,]', s)) return filter(bool, re.split('[ ,]', s))
class BgService(object):
"""Background servicing
Prevents memory leak on some platforms/python versions,
using forced GC in periodical intervals.
"""
_mutex = Lock()
_instance = None
def __new__(cls):
if not cls._instance:
cls._instance = \
super(BgService, cls).__new__(cls)
return cls._instance
def __init__(self):
self.__serviceTime = -0x7fffffff
self.__periodTime = 30
self.__threshold = 100;
self.__count = self.__threshold;
if hasattr(gc, 'set_threshold'):
gc.set_threshold(0)
gc.disable()
def service(self, force=False, wait=False):
self.__count -= 1
# avoid locking if next service time don't reached
if not force and (self.__count > 0 or MyTime.time() < self.__serviceTime):
return False
# return immediately if mutex already locked (other thread in servicing):
if not BgService._mutex.acquire(wait):
return False
try:
# check again in lock:
if MyTime.time() < self.__serviceTime:
return False
gc.collect()
self.__serviceTime = MyTime.time() + self.__periodTime
self.__count = self.__threshold
return True
finally:
BgService._mutex.release()
return False

View File

@ -28,7 +28,7 @@ from threading import Lock
import logging import logging
from .ticket import FailTicket from .ticket import FailTicket
from ..helpers import getLogger from ..helpers import getLogger, BgService
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
@ -43,6 +43,7 @@ class FailManager:
self.__maxRetry = 3 self.__maxRetry = 3
self.__maxTime = 600 self.__maxTime = 600
self.__failTotal = 0 self.__failTotal = 0
self.__bgSvc = BgService()
def setFailTotal(self, value): def setFailTotal(self, value):
with self.__lock: with self.__lock:
@ -102,6 +103,8 @@ class FailManager:
for k,v in self.__failList.iteritems()]) for k,v in self.__failList.iteritems()])
logSys.log(logLevel, "Total # of detected failures: %d. Current failures from %d IPs (IP:count): %s" logSys.log(logLevel, "Total # of detected failures: %d. Current failures from %d IPs (IP:count): %s"
% (self.__failTotal, len(self.__failList), failures_summary)) % (self.__failTotal, len(self.__failList), failures_summary))
self.__bgSvc.service()
return attempts return attempts
def size(self): def size(self):
@ -126,6 +129,7 @@ class FailManager:
# create new dictionary without items to be deleted: # create new dictionary without items to be deleted:
self.__failList = dict((ip,item) for ip,item in self.__failList.iteritems() \ self.__failList = dict((ip,item) for ip,item in self.__failList.iteritems() \
if item.getLastTime() + self.__maxTime > time) if item.getLastTime() + self.__maxTime > time)
self.__bgSvc.service()
def delFailure(self, ip): def delFailure(self, ip):
with self.__lock: with self.__lock:
@ -141,7 +145,8 @@ class FailManager:
if data.getRetry() >= self.__maxRetry: if data.getRetry() >= self.__maxRetry:
del self.__failList[ip] del self.__failList[ip]
return data return data
raise FailManagerEmpty self.__bgSvc.service()
raise FailManagerEmpty
class FailManagerEmpty(Exception): class FailManagerEmpty(Exception):

View File

@ -122,6 +122,29 @@ class AddFailure(unittest.TestCase):
self.assertNotEqual(ticket.getIP(), "100.100.10.10") self.assertNotEqual(ticket.getIP(), "100.100.10.10")
self.assertRaises(FailManagerEmpty, self.__failManager.toBan) self.assertRaises(FailManagerEmpty, self.__failManager.toBan)
def testBgService(self):
bgSvc = self.__failManager._FailManager__bgSvc
failManager2nd = FailManager()
# test singleton (same object):
bgSvc2 = failManager2nd._FailManager__bgSvc
self.assertTrue(id(bgSvc) == id(bgSvc2))
bgSvc2 = None
# test service :
self.assertTrue(bgSvc.service(True, True))
self.assertFalse(bgSvc.service())
# bypass threshold and time:
for i in range(1, bgSvc._BgService__threshold):
self.assertFalse(bgSvc.service())
# bypass time check:
bgSvc._BgService__serviceTime = -0x7fffffff
self.assertTrue(bgSvc.service())
# bypass threshold and time:
bgSvc._BgService__serviceTime = -0x7fffffff
for i in range(1, bgSvc._BgService__threshold):
self.assertFalse(bgSvc.service())
self.assertTrue(bgSvc.service(False, True))
self.assertFalse(bgSvc.service(False, True))
class FailmanagerComplex(unittest.TestCase): class FailmanagerComplex(unittest.TestCase):