Skip to content

Commit

Permalink
feature(upgrades): Introduces a new upgrading feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Juho Jaakkola authored and juho-jaakkola committed Oct 18, 2016
1 parent d9cee3f commit 6e221f0
Show file tree
Hide file tree
Showing 20 changed files with 943 additions and 207 deletions.
19 changes: 19 additions & 0 deletions actions/admin/upgrade.php
@@ -0,0 +1,19 @@
<?php
/**
* Runs batch upgrades
*/

$guid = get_input('guid');

$upgrade = get_entity($guid);

if (!$upgrade instanceof \ElggUpgrade) {
register_error(elgg_echo('admin:upgrades:error:invalid_upgrade', array($entity->title, $guid)));
exit;
}

$upgrader = _elgg_services()->batchUpgrader;
$upgrader->setUpgrade($upgrade);
$result = $upgrader->run();

echo json_encode($result);
46 changes: 0 additions & 46 deletions actions/admin/upgrades/upgrade_database_guid_columns.php

This file was deleted.

1 change: 1 addition & 0 deletions docs/guides/index.rst
Expand Up @@ -36,5 +36,6 @@ Customize Elgg's behavior with plugins.
walled-garden
web-services
upgrading
upgrading-data
events-list
hooks-list
177 changes: 177 additions & 0 deletions docs/guides/upgrading-data.rst
@@ -0,0 +1,177 @@
Upgrading plugin data
#####################

Every now and then there comes a time when a plugin needs to change the contents
or the structure of the data it has stored either in the database or the dataroot.

The motivation for this may be that the data structure needs to be converted
to more efficient or flexible structure. Or perhaps due to a bug the data items have
been saved in an invalid way, and they needs to be converted to the correct format.

Migrations and convertions like this may take a long time if there is a lot of
data to be processed. This is why Elgg provides the ``Elgg\Upgrade\Batch`` interface
that can be used for implementing long-running upgrades.

Declaring a plugin upgrade
--------------------------

Plugin can communicate the need for an upgrade under the ``upgrades`` key in
``elgg-plugin.php`` file. Each value of the array must be the fully qualified
name of an upgrade class that implements the ``Elgg\Upgrade\Batch`` interface.

Example from ``mod/blog/elgg-plugin.php`` file:

.. code:: php
return [
'upgrades' => [
'Blog\Upgrades\AccessLevelFix',
'Blog\Upgrades\DraftStatusUpgrade',
]
];
The class names in the example refer to the classes:
- `mod/blog/classes/Blog/Upgrades/AccessLevelFix`
- `mod/blog/classes/Blog/Upgrades/DraftStatusUpgrade`

The upgrade class
-----------------

A class implemening the ``Elgg\Upgrade\Batch`` interface has a lot of freedom
on how it wants to handle the actual processing of the data. It must however
declare some constant variables and also take care of marking whether each
processed item was upgraded successfully or not.

The basic structure of the class is the following:

.. code:: php
<?php
namespace Blog\Upgrades;
use Elgg\Upgrade\Batch;
use Elgg\Upgrade\Result;
/**
* Fixes invalid blog access values
*/
class AccessLevelFix implements BatchUpgrade {
const INCREMENT_OFFSET = true;
const VERSION = 2016120300;
/**
* Run the upgrade
*
* @param Result $result
* @param int $offset
* @return Result result
*/
public function run(Result $result, $offset) {
}
}
Class constants
~~~~~~~~~~~~~~~

The class must declare the following constant variables:

INCREMENT_OFFSET
^^^^^^^^^^^^^^^^

This is a boolean value that tells Elgg core whether it should increment
the offset of the upgrade after each run. If the upgrade leaves the data
itself intact and simply modifies it in some way, the value should be
set to ``true``. If the upgrade either moves or completely deletes the
items within the data, the value should be ``false``.

VERSION
^^^^^^^

The version constant tells the date when the upgrade was added. It consists
of eight digits and is in format ``yyyymmddnn`` where:

- ``yyyy`` is the year
- ``mm`` is the month (with leading zero)
- ``dd`` is the day (with leading zero)
- ``nn`` is an incrementing number (starting from ``00``) that is used in case
two separate upgrades have been added during the same day

Class methods
~~~~~~~~~~~~~

countItems()
^^^^^^^^^^^^

Counts and returns the total amount of items that need to be processed
by the upgrade.

run()
^^^^^

Takes care of the actual upgrade. It takes two parameters:

- ``$result``: An instance of ``Elgg\Upgrade\Result`` object
- ``$offset``: The offset where the next upgrade batch should start

For each item the method processes, it must call either:

- ``$result->addSuccesses()``: If the item was upgraded successfully
- ``$result->addFailures()``: If it failed to upgrade the item

Both methods default to one item, but you can optionally pass in
the number of items.

Additionally it can set as many error messages as it sees necessary in case
something goes wrong:

- ``$result->addError("Error message goes here")``

In most cases the ``$offset`` parameter is passed directly to one of the
``elgg_get_entities*()`` functions:

.. code:: php
/**
* Process blog posts
*
* @param Result $result Object that holds results of the batch
* @param int $offset Starting point of the batch
* @return Result Instance of \Elgg\Upgrade\Result;
*/
public function run(Result $result, $offset) {
$blogs = elgg_get_entitites([
'type' => 'object'
'subtype' => 'blog'
'offset' => $offset,
]);
foreach ($blogs as $blog) {
// Do something to the blog objects here
if (do_something($blog)) {
$result->addSuccesses()
} else {
$result->addFailures();
$result->addError("Failed to fix the blog {$blog->guid}.");
}
}
return $result;
}
Administration interface
------------------------

Each upgrade implementing the ``Elgg\Upgrade\Batch`` interface gets
listed in the admin panel after triggering the site upgrade from the
Administration dashboard.

While running the upgrades Elgg provides:

- Estimated duration of the upgrade
- Count of processed items
- Number of errors
- Possible error messages
7 changes: 7 additions & 0 deletions engine/classes/Elgg/Application.php
Expand Up @@ -506,6 +506,13 @@ public static function upgrade() {
}

if (get_input('upgrade') == 'upgrade') {
// Find unprocessed batch uprade classes and save them as ElggUpgrade objects
$has_pending_upgrades = _elgg_services()->upgradeLocator->run();

if ($has_pending_upgrades) {
// Forward to the list of pending upgrades
$forward_url = '/admin/upgrades';
}

$upgrader = _elgg_services()->upgrades;
$result = $upgrader->run();
Expand Down
124 changes: 124 additions & 0 deletions engine/classes/Elgg/BatchUpgrader.php
@@ -0,0 +1,124 @@
<?php

namespace Elgg;

use ElggUpgrade;
use Elgg\Config;
use Elgg\Timer;
use Elgg\Upgrade\Result;

/**
* Runs long running upgrades and gives feedback to UI after each batch.
*
* WARNING: API IN FLUX. DO NOT USE DIRECTLY.
*
* @access private
*
* @since 3.0.0
*/
class BatchUpgrader {

/**
* @var $upgrade ElggUpgrade
*/
private $upgrade;

/**
* @var $config Config
*/
private $config;

/**
* Constructor
*
* @param Config $config Site configuration
*/
public function __construct(Config $config) {
$this->config = $config;

// Custom limit can be defined in settings.php if necessary
if (empty($this->config->get('batch_run_time_in_secs'))) {
$this->config->set('batch_run_time_in_secs', 4);
}
}

/**
* Set ElggUpgrade object
*
* @param ElggUpgrade $upgrade ElggEntity representing the upgrade
* @return void
*/
public function setUpgrade(ElggUpgrade $upgrade) {
$this->upgrade = $upgrade;
}

/**
* Run single upgrade batch
*
* @return void
*/
public function run() {
// Upgrade also disabled data, so the compatibility is
// preserved in case the data ever gets enabled again
global $ENTITY_SHOW_HIDDEN_OVERRIDE;
$ENTITY_SHOW_HIDDEN_OVERRIDE = true;

// Defined in Elgg\Application
global $START_MICROTIME;

$result = new Result;

// Get the class taking care of the actual upgrading
$upgrade = $this->upgrade->getUpgrade();

do {
$upgrade->run($result, $this->upgrade->offset);

$failure_count = $result->getFailureCount();
$success_count = $result->getSuccessCount();

$total = $failure_count + $success_count;

if ($upgrade::INCREMENT_OFFSET) {
// Offset needs to incremented by the total amount of processed
// items so the upgrade we won't get stuck upgrading the same
// items over and over.
$this->upgrade->offset += $total;
} else {
// Offset doesn't need to be incremented, so we mark only
// the items that caused a failure.
$this->upgrade->offset += $failure_count;
}

if ($failure_count > 0) {
$this->upgrade->has_errors = true;
}

$this->upgrade->processed += $total;
} while ((microtime(true) - $START_MICROTIME) < $this->config->get('batch_run_time_in_secs'));

if ($this->upgrade->processed >= $this->upgrade->total) {
// Upgrade is finished
if ($this->upgrade->has_errors) {
// The upgrade was finished with errors. Reset offset
// and errors so the upgrade can start from a scratch
// if attempted to run again.
$this->upgrade->offset = 0;
$this->upgrade->has_errors = false;

// TODO Should $this->upgrade->count be updated again?
} else {
// Everything has been processed without errors
// so the upgrade can be marked as completed.
$this->upgrade->setCompleted();
}
}

// Give feedback to the user interface about the current batch.
return array(
'errors' => $result->getErrors(),
'numErrors' => $failure_count,
'numSuccess' => $success_count,
);
}
}

0 comments on commit 6e221f0

Please sign in to comment.