From 4b03821caf2c3628b83b84685d1264eb5bb4fe6c Mon Sep 17 00:00:00 2001 From: Markus Frosch Date: Tue, 4 Sep 2018 14:31:17 +0200 Subject: [PATCH 1/6] BaseTestCase: Let db be accessed statically E.g. from setUp and tearDown for class --- library/Director/Test/BaseTestCase.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/library/Director/Test/BaseTestCase.php b/library/Director/Test/BaseTestCase.php index 31a6b50f..23ceee54 100644 --- a/library/Director/Test/BaseTestCase.php +++ b/library/Director/Test/BaseTestCase.php @@ -6,7 +6,6 @@ use Icinga\Application\Icinga; use Icinga\Application\Config; use Icinga\Data\ResourceFactory; use Icinga\Exception\ConfigurationError; -use Icinga\Module\Director\Data\Db\DbConnection; use Icinga\Module\Director\Db; use Icinga\Module\Director\Db\Migrations; use Icinga\Module\Director\Objects\IcingaObject; @@ -16,7 +15,8 @@ abstract class BaseTestCase extends PHPUnit_Framework_TestCase { private static $app; - private $db; + /** @var Db */ + private static $db; public function setUp() { @@ -39,7 +39,7 @@ abstract class BaseTestCase extends PHPUnit_Framework_TestCase return $this->getDbResourceName() !== null; } - protected function getDbResourceName() + protected static function getDbResourceName() { if (array_key_exists('DIRECTOR_TESTDB_RES', $_SERVER)) { return $_SERVER['DIRECTOR_TESTDB_RES']; @@ -52,10 +52,10 @@ abstract class BaseTestCase extends PHPUnit_Framework_TestCase * @return Db * @throws ConfigurationError */ - protected function getDb() + protected static function getDb() { - if ($this->db === null) { - $resourceName = $this->getDbResourceName(); + if (self::$db === null) { + $resourceName = self::getDbResourceName(); if (! $resourceName) { throw new ConfigurationError( 'Could not run DB-based tests, please configure a testing db resource' @@ -74,12 +74,12 @@ abstract class BaseTestCase extends PHPUnit_Framework_TestCase if (array_key_exists('DIRECTOR_TESTDB_PASSWORD', $_SERVER)) { $dbConfig->password = $_SERVER['DIRECTOR_TESTDB_PASSWORD']; } - $this->db = new Db($dbConfig); - $migrations = new Migrations($this->db); + self::$db = new Db($dbConfig); + $migrations = new Migrations(self::$db); $migrations->applyPendingMigrations(); } - return $this->db; + return self::$db; } protected function newObject($type, $name, $properties = array()) From 325047260cea334e18059206fbfcf4a273d42b8d Mon Sep 17 00:00:00 2001 From: Markus Frosch Date: Tue, 4 Sep 2018 14:33:20 +0200 Subject: [PATCH 2/6] test: Add HostGroupMembershipResolverTest refs #1574 --- .../HostGroupMembershipResolverTest.php | 357 ++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 test/php/library/Director/Objects/HostGroupMembershipResolverTest.php diff --git a/test/php/library/Director/Objects/HostGroupMembershipResolverTest.php b/test/php/library/Director/Objects/HostGroupMembershipResolverTest.php new file mode 100644 index 00000000..547d1c44 --- /dev/null +++ b/test/php/library/Director/Objects/HostGroupMembershipResolverTest.php @@ -0,0 +1,357 @@ +getDbAdapter(); + + $where = sprintf("object_name LIKE '%s%%'", self::PREFIX); + + $db->delete('icinga_' . self::TYPE . 'group', $where); + + $db->delete('icinga_' . self::TYPE, $where . " AND object_type = 'object'"); + $db->delete('icinga_' . self::TYPE, $where); + } + + public static function setUpBeforeClass() + { + static::cleanArtifacts(); + } + + public static function tearDownAfterClass() + { + static::cleanArtifacts(); + } + + /** + * @param string $type + * @param string $name + * @param array $props + * + * @return IcingaObject + * @throws DuplicateKeyException + * @throws \Icinga\Exception\ConfigurationError + */ + protected function object($type, $name, $props = []) + { + $db = $this->getDb(); + $fullName = self::PREFIX . $name; + $object = null; + + try { + $object = IcingaObject::loadByType($type, $fullName, $db); + + foreach ($props as $k => $v) { + $object->set($k, $v); + } + + $object->store(); + } catch (NotFoundError $e) { + $object = null; + } + + if ($object === null) { + $object = IcingaObject::createByType($type, array_merge([ + 'object_name' => $fullName, + 'object_type' => 'object', + ], $props), $this->getDb()); + + $object->store(); + } + + return $object; + } + + protected function objects($type) + { + /** @var IcingaObject $class */ + $class = IcingaObject::classByType($type); + + /** @var IcingaObject $dummy */ + $dummy = $class::create(); + + $table = $dummy->getTableName(); + $query = $this->getDb()->getDbAdapter()->select() + ->from($table) + ->where('object_name LIKE ?', self::PREFIX . '%'); + + $objects = []; + $l = strlen(self::PREFIX); + + foreach ($class::loadAll($this->getDb(), $query) as $object) { + $key = substr($object->getObjectName(), $l); + $objects[$key] = $object; + } + + return $objects; + } + + protected function resolved() + { + $db = $this->getDb()->getDbAdapter(); + + $select = $db->select() + ->from( + ['r' => 'icinga_' . self::TYPE . 'group_' . self::TYPE . '_resolved'], + [] + )->join( + ['o' => 'icinga_' . self::TYPE], + 'o.id = r.' . self::TYPE . '_id', + ['object' => 'object_name'] + )->join( + ['g' => 'icinga_' . self::TYPE . 'group'], + 'g.id = r.' . self::TYPE . 'group_id', + ['groupname' => 'object_name'] + ); + + $map = []; + $l = strlen(self::PREFIX); + + foreach ($db->fetchAll($select) as $row) { + $o = $row->object; + $g = $row->groupname; + + if (! substr($o, 0, $l) === self::PREFIX) { + continue; + } + $o = substr($o, $l); + + if (! substr($g, 0, $l) === self::PREFIX) { + continue; + } + $g = substr($g, $l); + + if (! array_key_exists($o, $map)) { + $map[$o] = []; + } + + $map[$o][] = $g; + } + + return $map; + } + + /** + * Creates: + * + * - 1 template + * - 10 hosts importing the template with a var match_var=magic + * + * @throws DuplicateKeyException + * @throws \Icinga\Exception\ConfigurationError + */ + public function testCreateHosts() + { + // template that sets a group later + $template = $this->object('host', 'template', [ + 'object_type' => 'template', + ]); + $this->assertTrue($template->hasBeenLoadedFromDb()); + + // hosts to assign groups + for ($i = 1; $i <= 10; $i++) { + $host = $this->object('host', $i, [ + 'imports' => self::PREFIX . 'template', + 'vars.match_var' => 'magic' + ]); + $this->assertTrue($host->hasBeenLoadedFromDb()); + } + } + + /** + * Creates: + * + * - 10 hostgroups applying on hosts with match_var=magic + * - 2 static hostgroups + * + * @throws DuplicateKeyException + * @throws \Icinga\Exception\ConfigurationError + */ + public function testCreateHostgroups() + { + $filter = 'host.vars.match_var=%22magic%22'; + for ($i = 1; $i <= 10; $i++) { + $hostgroup = $this->object('hostgroup', 'apply' . $i, [ + 'assign_filter' => $filter + ]); + $this->assertTrue($hostgroup->hasBeenLoadedFromDb()); + } + + // static groups + for ($i = 1; $i <= 2; $i++) { + $hostgroup = $this->object('hostgroup', 'static' . $i); + $this->assertTrue($hostgroup->hasBeenLoadedFromDb()); + } + } + + /** + * Assigns static groups to: + * + * - the template + * - the first host + * + * @throws DuplicateKeyException + * @throws \Icinga\Exception\ConfigurationError + * + * @depends testCreateHosts + * @depends testCreateHostgroups + */ + public function testAddStaticGroups() + { + // add group to template + $template = $this->object('host', 'template'); + $template->setGroups(self::PREFIX . 'static1'); + $template->store(); + $this->assertFalse($template->hasBeenModified()); + + // add group to first host + $host = $this->object('host', 1); + $host->setGroups(self::PREFIX . 'static2'); + $host->store(); + $this->assertFalse($host->hasBeenModified()); + } + + /** + * Asserts that static groups are resolved for hosts: + * + * - all but first should have static1 + * - first should have static2 + * + * @depends testAddStaticGroups + */ + public function testStaticResolvedMappings() + { + $resolved = $this->resolved(); + + $this->assertArrayHasKey( + 1, + $resolved, + 'Host 1 must have groups resolved' + ); + + $this->assertContains( + 'static2', + $resolved[1], + 'Host template must have static group 1' + ); + + $hosts = $this->objects('host'); + $this->assertNotEmpty($hosts, 'Must have hosts found in DB'); + + foreach ($hosts as $name => $host) { + if ($host->object_type === 'template') { + continue; + } + + $this->assertArrayHasKey( + $name, + $resolved, + 'All hosts must have groups resolved' + ); + + if ($name === 1) { + $this->assertNotContains( + 'static1', + $resolved[$name], + 'First host must not have static group 1' + ); + } else { + $this->assertContains( + 'static1', + $resolved[$name], + 'All hosts but the first must have static group 1' + ); + } + } + } + + /** + * @depends testCreateHostgroups + */ + public function testApplyResolvedMappings() + { + $resolved = $this->resolved(); + + $hosts = $this->objects('host'); + $this->assertNotEmpty($hosts, 'Must have hosts found in DB'); + + foreach ($hosts as $name => $host) { + if ($host->object_type === 'template') { + continue; + } + + $this->assertArrayHasKey($name, $resolved, 'Host must have groups resolved'); + + for ($i = 1; $i <= 10; $i++) { + $this->assertContains( + 'apply' . $i, + $resolved[$name], + 'All Host objects must have all applied groups' + ); + } + } + } + + /** + * @throws DuplicateKeyException + * @throws \Icinga\Exception\ConfigurationError + * + * @depends testAddStaticGroups + */ + public function testChangeAppliedGroupsAfterStatic() + { + $filter = 'host.vars.match_var=%22magic*%22'; + + $hostgroup = $this->object('hostgroup', 'apply1', [ + 'assign_filter' => $filter + ]); + $this->assertTrue($hostgroup->hasBeenLoadedFromDb()); + $this->assertFalse($hostgroup->hasBeenModified()); + + $resolved = $this->resolved(); + + for ($i = 1; $i <= 10; $i++) { + $this->assertContains( + 'apply1', + $resolved[$i], + 'All Host objects must have apply1 group' + ); + } + } + + /** + * @throws \Icinga\Exception\ConfigurationError + * @throws \Zend_Db_Adapter_Exception + * + * @depends testStaticResolvedMappings + * @depends testApplyResolvedMappings + * @depends testChangeAppliedGroupsAfterStatic + */ + public function testFullRecheck() + { + $resolver = new HostGroupMembershipResolver($this->getDb()); + + $resolver->refreshAllMappings(); + $this->assertTrue(true); // we reached this without exception + + // TODO: check results of the recheck - it should not change anything at this point + } +} From e46a610b5fcdbfadd2618c73e986391703019826 Mon Sep 17 00:00:00 2001 From: Markus Frosch Date: Thu, 17 May 2018 12:59:58 +0200 Subject: [PATCH 3/6] GroupMembershipResolver: Add interfaces to be able to check before updating --- .../Objects/GroupMembershipResolver.php | 68 ++++++++++++++++--- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/library/Director/Objects/GroupMembershipResolver.php b/library/Director/Objects/GroupMembershipResolver.php index 728bc5af..64262424 100644 --- a/library/Director/Objects/GroupMembershipResolver.php +++ b/library/Director/Objects/GroupMembershipResolver.php @@ -47,6 +47,9 @@ abstract class GroupMembershipResolver /** @var bool */ protected $deferred = false; + /** @var bool */ + protected $checked = false; + /** @var bool */ protected $useTransactions = false; @@ -67,6 +70,33 @@ abstract class GroupMembershipResolver return $this->clearGroups()->clearObjects()->refreshDb(true); } + public function checkDb() + { + if ($this->checked) { + return $this; + } + + if ($this->isDeferred()) { + // ensure we are not working with cached data + IcingaTemplateRepository::clear(); + } + + Benchmark::measure('Rechecking all objects'); + $this->recheckAllObjects($this->getAppliedGroups()); + if (empty($this->objects)) { + Benchmark::measure('Nothing to check, got no qualified object'); + + return $this; + } + + Benchmark::measure('Recheck done, loading existing mappings'); + $this->fetchStoredMappings(); + Benchmark::measure('Got stored group mappings'); + + $this->checked = true; + return $this; + } + /** * @param bool $force * @return $this @@ -75,23 +105,18 @@ abstract class GroupMembershipResolver public function refreshDb($force = false) { if ($force || ! $this->isDeferred()) { - if ($this->isDeferred()) { - // ensure we are not working with cached data - IcingaTemplateRepository::clear(); - } + $this->checkDb(); - Benchmark::measure('Rechecking all objects'); - $this->recheckAllObjects($this->getAppliedGroups()); if (empty($this->objects)) { Benchmark::measure('Nothing to check, got no qualified object'); return $this; } - Benchmark::measure('Recheck done, loading existing mappings'); - $this->fetchStoredMappings(); + Benchmark::measure('Ready, going to store new mappings'); $this->storeNewMappings(); $this->removeOutdatedMappings(); + Benchmark::measure('Updated group mappings in db'); } return $this; @@ -235,6 +260,8 @@ abstract class GroupMembershipResolver $this->assertBeenLoadedFromDb($group); $this->groups[$group->get('id')] = $group; + $this->checked = false; + return $this; } @@ -248,6 +275,8 @@ abstract class GroupMembershipResolver $this->addGroup($group); } + $this->checked = false; + return $this; } @@ -277,15 +306,25 @@ abstract class GroupMembershipResolver public function clearGroups() { $this->objects = array(); + $this->checked = false; return $this; } + public function getNewMappings() + { + if ($this->newMappings !== null && $this->existingMappings !== null) { + return $this->getDifference($this->newMappings, $this->existingMappings); + } else { + return []; + } + } + /** * @throws \Zend_Db_Adapter_Exception */ protected function storeNewMappings() { - $diff = $this->getDifference($this->newMappings, $this->existingMappings); + $diff = $this->getNewMappings(); $count = count($diff); if ($count === 0) { return; @@ -327,9 +366,18 @@ abstract class GroupMembershipResolver } } + public function getOutdatedMappings() + { + if ($this->newMappings !== null && $this->existingMappings !== null) { + return $this->getDifference($this->existingMappings, $this->newMappings); + } else { + return []; + } + } + protected function removeOutdatedMappings() { - $diff = $this->getDifference($this->existingMappings, $this->newMappings); + $diff = $this->getOutdatedMappings(); $count = count($diff); if ($count === 0) { return; From 7bfe1e03e6e35976d0875d2ca9fc337cf9086c25 Mon Sep 17 00:00:00 2001 From: Markus Frosch Date: Thu, 17 May 2018 13:00:31 +0200 Subject: [PATCH 4/6] Housekeeping: Add helper to refresh memberships in database This is usually only done when either object or group changes. --- .../clicommands/HousekeepingCommand.php | 36 ++++++ .../Db/HostMembershipHousekeeping.php | 8 ++ .../Director/Db/MembershipHousekeeping.php | 111 ++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 library/Director/Db/HostMembershipHousekeeping.php create mode 100644 library/Director/Db/MembershipHousekeeping.php diff --git a/application/clicommands/HousekeepingCommand.php b/application/clicommands/HousekeepingCommand.php index 58c673ae..20ea1635 100644 --- a/application/clicommands/HousekeepingCommand.php +++ b/application/clicommands/HousekeepingCommand.php @@ -5,6 +5,7 @@ namespace Icinga\Module\Director\Clicommands; use Icinga\Exception\MissingParameterException; use Icinga\Module\Director\Cli\Command; use Icinga\Module\Director\Db\Housekeeping; +use Icinga\Module\Director\Db\MembershipHousekeeping; class HousekeepingCommand extends Command { @@ -62,6 +63,41 @@ class HousekeepingCommand extends Command } } + /** + * Check and repair membership cache + * + * Options: + * --type host Set the object type (Only host supported currently) + * --apply Actually update the database + */ + public function membershipsAction() + { + $type = $this->params->get('type', 'host'); + $apply = $this->params->shift('apply'); + + /** @var MembershipHousekeeping $class */ + $class = 'Icinga\\Module\\Director\\Db\\' . ucfirst($type) . 'MembershipHousekeeping'; + /** @var MembershipHousekeeping $helper */ + $helper = new $class($this->db()); + + printf("Checking %s memberships\n", $type); + + list($new, $outdated) = $helper->check(); + $newCount = count($new); + $outdatedCount = count($outdated); + $objects = $helper->getObjects(); + $groups = $helper->getGroups(); + + printf("%d objects - %d groups\n", count($objects), count($groups)); + + printf("Found %d new and %d outdated mappings\n", $newCount, $outdatedCount); + + if ($apply && ($newCount > 0 || $outdatedCount > 0)) { + $helper->update(); + printf("Update complete.\n"); + } + } + protected function housekeeping() { if ($this->housekeeping === null) { diff --git a/library/Director/Db/HostMembershipHousekeeping.php b/library/Director/Db/HostMembershipHousekeeping.php new file mode 100644 index 00000000..3a2de05d --- /dev/null +++ b/library/Director/Db/HostMembershipHousekeeping.php @@ -0,0 +1,8 @@ +connection = $connection; + + if ($this->groupType === null) { + $this->groupType = $this->type . 'Group'; + } + } + + protected function prepare() + { + if ($this->prepared) { + return $this; + } + + $this->prepareCache(); + $this->resolver()->defer(); + + $this->objects = IcingaObject::loadAllByType($this->type, $this->connection); + $this->resolver()->addObjects($this->objects); + + $this->groups = IcingaObject::loadAllByType($this->groupType, $this->connection); + $this->resolver()->addGroups($this->groups); + + MemoryLimit::raiseTo('1024M'); + + $this->prepared = true; + + return $this; + } + + public function check() + { + $this->prepare(); + + $resolver = $this->resolver()->checkDb(); + + return array($resolver->getNewMappings(), $resolver->getOutdatedMappings()); + } + + public function update() + { + $this->prepare(); + + $this->resolver()->refreshDb(true); + } + + protected function prepareCache() + { + PrefetchCache::initialize($this->connection); + + IcingaObject::prefetchAllRelationsByType($this->type, $this->connection); + } + + protected function resolver() + { + if ($this->resolver === null) { + /** @var GroupMembershipResolver $class */ + $class = 'Icinga\\Module\\Director\\Objects\\' . ucfirst($this->type) . 'GroupMembershipResolver'; + $this->resolver = new $class($this->connection); + } + + return $this->resolver; + } + + /** + * @return IcingaObject[] + */ + public function getObjects() + { + return $this->objects; + } + + /** + * @return IcingaObjectGroup[] + */ + public function getGroups() + { + return $this->groups; + } +} From a7ad2e7ad3f5e22fb3623a8dd801b6fd44381947 Mon Sep 17 00:00:00 2001 From: Markus Frosch Date: Wed, 5 Sep 2018 16:29:43 +0200 Subject: [PATCH 5/6] HostGroupMembershipResolverTest: Check resolver after test set --- .../Director/Objects/HostGroupMembershipResolverTest.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/php/library/Director/Objects/HostGroupMembershipResolverTest.php b/test/php/library/Director/Objects/HostGroupMembershipResolverTest.php index 547d1c44..902977e0 100644 --- a/test/php/library/Director/Objects/HostGroupMembershipResolverTest.php +++ b/test/php/library/Director/Objects/HostGroupMembershipResolverTest.php @@ -349,9 +349,8 @@ class HostGroupMembershipResolverTest extends BaseTestCase { $resolver = new HostGroupMembershipResolver($this->getDb()); - $resolver->refreshAllMappings(); - $this->assertTrue(true); // we reached this without exception - - // TODO: check results of the recheck - it should not change anything at this point + $resolver->checkDb(); + $this->assertEmpty($resolver->getNewMappings(), 'There should not be any new mappings'); + $this->assertEmpty($resolver->getOutdatedMappings(), 'There should not be any outdated mappings'); } } From 4675a241a8d409c4f94242d355ae7228d98dbcd4 Mon Sep 17 00:00:00 2001 From: Markus Frosch Date: Tue, 18 Sep 2018 12:21:19 +0200 Subject: [PATCH 6/6] Integrate MembershipHousekeeping into Housekeeping --- .../clicommands/HousekeepingCommand.php | 35 ------------------- library/Director/Db/Housekeeping.php | 13 +++++++ .../Director/Db/MembershipHousekeeping.php | 24 +++++++++++++ 3 files changed, 37 insertions(+), 35 deletions(-) diff --git a/application/clicommands/HousekeepingCommand.php b/application/clicommands/HousekeepingCommand.php index 20ea1635..974e28db 100644 --- a/application/clicommands/HousekeepingCommand.php +++ b/application/clicommands/HousekeepingCommand.php @@ -63,41 +63,6 @@ class HousekeepingCommand extends Command } } - /** - * Check and repair membership cache - * - * Options: - * --type host Set the object type (Only host supported currently) - * --apply Actually update the database - */ - public function membershipsAction() - { - $type = $this->params->get('type', 'host'); - $apply = $this->params->shift('apply'); - - /** @var MembershipHousekeeping $class */ - $class = 'Icinga\\Module\\Director\\Db\\' . ucfirst($type) . 'MembershipHousekeeping'; - /** @var MembershipHousekeeping $helper */ - $helper = new $class($this->db()); - - printf("Checking %s memberships\n", $type); - - list($new, $outdated) = $helper->check(); - $newCount = count($new); - $outdatedCount = count($outdated); - $objects = $helper->getObjects(); - $groups = $helper->getGroups(); - - printf("%d objects - %d groups\n", count($objects), count($groups)); - - printf("Found %d new and %d outdated mappings\n", $newCount, $outdatedCount); - - if ($apply && ($newCount > 0 || $outdatedCount > 0)) { - $helper->update(); - printf("Update complete.\n"); - } - } - protected function housekeeping() { if ($this->housekeeping === null) { diff --git a/library/Director/Db/Housekeeping.php b/library/Director/Db/Housekeeping.php index 366fe976..8816cb2a 100644 --- a/library/Director/Db/Housekeeping.php +++ b/library/Director/Db/Housekeeping.php @@ -51,6 +51,7 @@ class Housekeeping 'unlinkedImportedRowSets' => N_('Unlinked imported row sets'), 'unlinkedImportedRows' => N_('Unlinked imported rows'), 'unlinkedImportedProperties' => N_('Unlinked imported properties'), + 'resolveCache' => N_('(Host) group resolve cache'), ); } @@ -189,4 +190,16 @@ class Housekeeping return $this->db->exec($sql); } + + public function countResolveCache() + { + $helper = MembershipHousekeeping::instance('host', $this->connection); + return array_sum($helper->check()); + } + + public function wipeResolveCache() + { + $helper = MembershipHousekeeping::instance('host', $this->connection); + return $helper->update(); + } } diff --git a/library/Director/Db/MembershipHousekeeping.php b/library/Director/Db/MembershipHousekeeping.php index 7f1ec010..4d1ae883 100644 --- a/library/Director/Db/MembershipHousekeeping.php +++ b/library/Director/Db/MembershipHousekeeping.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\Db; use Icinga\Module\Director\Application\MemoryLimit; +use Icinga\Module\Director\Data\Db\DbConnection; use Icinga\Module\Director\Db; use Icinga\Module\Director\Db\Cache\PrefetchCache; use Icinga\Module\Director\Objects\GroupMembershipResolver; @@ -28,6 +29,8 @@ abstract class MembershipHousekeeping protected $prepared = false; + protected static $instances = []; + public function __construct(Db $connection) { $this->connection = $connection; @@ -37,6 +40,25 @@ abstract class MembershipHousekeeping } } + /** + * @param string $type + * @param DbConnection $connection + * + * @return static + */ + public static function instance($type, $connection) + { + if (! array_key_exists($type, self::$instances)) { + /** @var MembershipHousekeeping $class */ + $class = 'Icinga\\Module\\Director\\Db\\' . ucfirst($type) . 'MembershipHousekeeping'; + + /** @var MembershipHousekeeping $helper */ + self::$instances[$type] = new $class($connection); + } + + return self::$instances[$type]; + } + protected function prepare() { if ($this->prepared) { @@ -73,6 +95,8 @@ abstract class MembershipHousekeeping $this->prepare(); $this->resolver()->refreshDb(true); + + return true; } protected function prepareCache()