From 6c2d1f008bb3e119d26dc9475580d485bca857b8 Mon Sep 17 00:00:00 2001 From: Duncan Sommerville Date: Thu, 19 Nov 2015 16:08:25 -0500 Subject: [PATCH] SAAS-1229 - initial work on bandwidth limit within Airtime; overhaul TaskFactory to get tasks reflectively --- .../application/common/TaskManager.php | 87 ++++++++++++++----- .../application/controllers/ApiController.php | 39 +++++++-- .../plugins/PageLayoutInitPlugin.php | 3 +- airtime_mvc/application/models/Preference.php | 29 +++++++ 4 files changed, 126 insertions(+), 32 deletions(-) diff --git a/airtime_mvc/application/common/TaskManager.php b/airtime_mvc/application/common/TaskManager.php index d8ab9c10d..dbb7ccb60 100644 --- a/airtime_mvc/application/common/TaskManager.php +++ b/airtime_mvc/application/common/TaskManager.php @@ -3,8 +3,7 @@ /** * Class TaskManager * - * When adding a new task, the new AirtimeTask class will also need to be added - * as a class constant and to the array in TaskFactory + * Background class for 'asynchronous' task management for Airtime stations */ final class TaskManager { @@ -34,8 +33,8 @@ final class TaskManager { * Private constructor so class is uninstantiable */ private function __construct() { - foreach (array_keys(TaskFactory::$tasks) as $v) { - $this->_taskList[$v] = false; + foreach (TaskFactory::getTasks() as $k => $task) { + $this->_taskList[$task] = false; } } @@ -278,12 +277,12 @@ class ImportCleanupTask implements AirtimeTask { /** * Class StationPodcastTask * - * Checks the Station podcast rollover timer and resets monthly allotted + * Checks the Station podcast rollover timer and resets allotted * downloads if enough time has passed (default: 1 month) */ class StationPodcastTask implements AirtimeTask { - const STATION_PODCAST_RESET_TIMER_SECONDS = 2.628e+6; // 1 month XXX: should we use datetime roll for this instead? + const STATION_PODCAST_RESET_TIMER_SECONDS = 2.628e+6; // 1 month /** * Check whether or not the download counter for the station podcast should be reset @@ -305,32 +304,74 @@ class StationPodcastTask implements AirtimeTask { } +/** + * TODO: this and the above StationPodcastTask should probably be unified since the + * behaviour is essentially identical + * + * Class BandwidthLimitTask + * + * Checks the bandwidth limit rollover timer and resets the allotted + * limit if enough time has passed (default: 1 month) + */ +class BandwidthLimitTask implements AirtimeTask { + + const BANDWIDTH_LIMIT_RESET_TIMER_SECONDS = 2.628e+6; + + /** + * Check whether the task should be run + * + * @return bool true if the task needs to be run, otherwise false + */ + public function shouldBeRun() { + $lastReset = Application_Model_Preference::getBandwidthLimitResetTimer(); + return empty($lastReset) || (microtime(true) > ($lastReset + self::BANDWIDTH_LIMIT_RESET_TIMER_SECONDS)); + } + + /** + * Run the task + * + * @return void + */ + public function run() { + Application_Model_Preference::resetStationPodcastDownloadCounter(); + Application_Model_Preference::setBandwidthLimitResetTimer(microtime(true)); + } + +} + /** * Class TaskFactory Factory class to abstract task instantiation */ class TaskFactory { /** - * PHP doesn't have ENUMs so declare them as constants - * Task types - values don't really matter as long as they're unique + * Check if the class with the given name implements AirtimeTask + * + * @param $c string class name + * + * @return bool true if the class $c implements AirtimeTask */ - - const UPGRADE = "upgrade"; - const CELERY = "celery"; - const PODCAST = "podcast"; - const IMPORT = "import"; - const STATION_PODCAST = "station-podcast"; + private static function _isTask($c) { + $reflect = new ReflectionClass($c); + $clazz = version_compare(phpversion(), '5.5.0', '<') ? "AirtimeTask" : AirtimeTask::class; + return $reflect->implementsInterface($clazz); + } /** - * @var array map of arbitrary identifiers to class names to be instantiated reflectively + * Filter all declared classes to get all classes implementing the AirtimeTask interface + * + * @return array all classes implementing the AirtimeTask interface */ - public static $tasks = array( - "upgrade" => "UpgradeTask", - "celery" => "CeleryTask", - "podcast" => "PodcastTask", - "import" => "ImportCleanupTask", - "station-podcast" => "StationPodcastTask", - ); + private static function _getTaskClasses() { + return array_filter(get_declared_classes(), array(__CLASS__, "_isTask")); + } + + /** + * @return array + */ + public static function getTasks() { + return self::_getTaskClasses(); + } /** * Get an AirtimeTask based on a task type @@ -341,7 +382,7 @@ class TaskFactory { * task exists or is implemented */ public static function getTask($task) { - return new self::$tasks[$task](); + return new $task(); } } diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index de7c7f156..41d6a5c9b 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -11,6 +11,8 @@ class ApiController extends Zend_Controller_Action public function init() { + $this->view->layout()->disableLayout(); + $this->_helper->viewRenderer->setNoRender(true); //Ignore API key and session authentication for these APIs: $ignoreAuth = array("live-info", @@ -101,14 +103,6 @@ class ApiController extends Zend_Controller_Action exit(); } - public function pollCeleryAction() { - $this->view->layout()->disableLayout(); - $this->_helper->viewRenderer->setNoRender(true); - - $taskManager = TaskManager::getInstance(); - $taskManager->runTask(TaskFactory::CELERY); - } - public function versionAction() { $this->_helper->json->sendJson( array( @@ -136,6 +130,35 @@ class ApiController extends Zend_Controller_Action $this->_helper->json->sendJson(array()); } + /** + * Manually trigger the TaskManager task to poll for completed Celery tasks + */ + public function pollCeleryAction() { + $taskManager = TaskManager::getInstance(); + $clazz = version_compare(phpversion(), '5.5.0', '<') ? get_class(new CeleryTask) : CeleryTask::class; + $taskManager->runTask($clazz); + } + + /** + * TODO: move this function into a more generic analytics REST controller + * + * Update station bandwidth usage based on icecast log data + */ + public function bandwidthUsageAction() { + // FIXME: this function is using placeholder names + $bandwidthUsage = json_decode($this->getRequest()->getParam("bandwidth_usage")); + $usageBytes = 0; + foreach ($bandwidthUsage as $entry) { + Logging::info($entry); + // TODO: store the IP address for future use + $ts = strtotime($entry->timestamp); + if ($ts > Application_Model_Preference::getBandwidthLimitUpdateTimer()) { + $usageBytes += $entry->bytes; + } + } + Application_Model_Preference::incrementBandwidthLimitCounter($usageBytes); + } + //Used by the SaaS monitoring public function onAirLightAction() { diff --git a/airtime_mvc/application/controllers/plugins/PageLayoutInitPlugin.php b/airtime_mvc/application/controllers/plugins/PageLayoutInitPlugin.php index db342159d..feb18573a 100644 --- a/airtime_mvc/application/controllers/plugins/PageLayoutInitPlugin.php +++ b/airtime_mvc/application/controllers/plugins/PageLayoutInitPlugin.php @@ -64,7 +64,8 @@ class PageLayoutInitPlugin extends Zend_Controller_Plugin_Abstract // We can't afford to wait 7 minutes to run an upgrade: users could // have several minutes of database errors while waiting for a // schema change upgrade to happen after a deployment - $taskManager->runTask(TaskFactory::UPGRADE); + $clazz = version_compare(phpversion(), '5.5.0', '<') ? get_class(new UpgradeTask) : UpgradeTask::class; + $taskManager->runTask($clazz); // Piggyback the TaskManager onto API calls. This provides guaranteed consistency // (there is at least one API call made from pypo to Airtime every 7 minutes) and diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php index 82731f524..68d790249 100644 --- a/airtime_mvc/application/models/Preference.php +++ b/airtime_mvc/application/models/Preference.php @@ -1584,4 +1584,33 @@ class Application_Model_Preference public static function setStationPodcastPrivacy($value) { self::setValue("station_podcast_privacy", $value); } + + public static function getBandwidthLimitCounter() { + return self::getValue("bandwidth_limit_counter"); + } + + public static function incrementBandwidthLimitCounter($value) { + $counter = intval(self::getValue("bandwidth_limit_counter")); + self::setValue("bandwidth_limit_counter", $counter + intval($value)); + } + + public static function resetBandwidthLimitCounter() { + self::setValue("bandwidth_limit_counter", 0); + } + + public static function getBandwidthLimitResetTimer() { + return self::getValue("bandwidth_limit_reset_timer"); + } + + public static function setBandwidthLimitResetTimer($value) { + self::setValue("bandwidth_limit_reset_timer", $value); + } + + public static function getBandwidthLimitUpdateTimer() { + return self::getValue("bandwidth_limit_reset_timer"); + } + + public static function setBandwidthLimitUpdateTimer() { + self::setValue("bandwidth_limit_reset_timer", microtime(true)); + } }