aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteve Clay <steve@mrclay.org>2012-06-01 17:02:18 -0400
committerSteve Clay <steve@mrclay.org>2012-09-03 22:26:27 -0400
commit61274b8cec92ee86dec24c99fd6ef180c1681ab2 (patch)
treece4c16e9ac2ee8fc78d78df59b5f8701082ad54f
parent9ccbd106a87a1742a61cc4df0e9ead921046772a (diff)
downloadelgg-61274b8cec92ee86dec24c99fd6ef180c1681ab2.tar.gz
elgg-61274b8cec92ee86dec24c99fd6ef180c1681ab2.tar.bz2
Fixes #4290: adds volatile metadata cache, unit tests, and pre-loading for fetched entities
-rw-r--r--engine/classes/ElggEntity.php44
-rw-r--r--engine/classes/ElggMetadata.php34
-rw-r--r--engine/classes/ElggPlugin.php6
-rw-r--r--engine/classes/ElggVolatileMetadataCache.php344
-rw-r--r--engine/lib/entities.php52
-rw-r--r--engine/lib/metadata.php122
-rw-r--r--engine/tests/api/metadata_cache.php169
7 files changed, 687 insertions, 84 deletions
diff --git a/engine/classes/ElggEntity.php b/engine/classes/ElggEntity.php
index 77c2bbf4d..929abceb2 100644
--- a/engine/classes/ElggEntity.php
+++ b/engine/classes/ElggEntity.php
@@ -248,7 +248,9 @@ abstract class ElggEntity extends ElggData implements
* @return mixed The value, or NULL if not found.
*/
public function getMetaData($name) {
- if ((int) ($this->guid) == 0) {
+ $guid = $this->getGUID();
+
+ if (! $guid) {
if (isset($this->temp_metadata[$name])) {
// md is returned as an array only if more than 1 entry
if (count($this->temp_metadata[$name]) == 1) {
@@ -261,21 +263,38 @@ abstract class ElggEntity extends ElggData implements
}
}
+ // upon first cache miss, just load/cache all the metadata and retry.
+ // if this works, the rest of this function may not be needed!
+ $cache = elgg_get_metadata_cache();
+ if ($cache->isKnown($guid, $name)) {
+ return $cache->load($guid, $name);
+ } else {
+ $cache->populateFromEntities(array($guid));
+ // in case ignore_access was on, we have to check again...
+ if ($cache->isKnown($guid, $name)) {
+ return $cache->load($guid, $name);
+ }
+ }
+
$md = elgg_get_metadata(array(
- 'guid' => $this->getGUID(),
+ 'guid' => $guid,
'metadata_name' => $name,
'limit' => 0,
));
+ $value = null;
+
if ($md && !is_array($md)) {
- return $md->value;
+ $value = $md->value;
} elseif (count($md) == 1) {
- return $md[0]->value;
+ $value = $md[0]->value;
} else if ($md && is_array($md)) {
- return metadata_array_to_values($md);
+ $value = metadata_array_to_values($md);
}
- return null;
+ $cache->save($guid, $name, $value);
+
+ return $value;
}
/**
@@ -1007,7 +1026,7 @@ abstract class ElggEntity extends ElggData implements
/**
* Returns the guid.
*
- * @return int GUID
+ * @return int|null GUID
*/
public function getGUID() {
return $this->get('guid');
@@ -1245,16 +1264,16 @@ abstract class ElggEntity extends ElggData implements
/**
* Save an entity.
*
- * @return bool/int
+ * @return bool|int
* @throws IOException
*/
public function save() {
- $guid = (int) $this->guid;
+ $guid = $this->getGUID();
if ($guid > 0) {
cache_entity($this);
return update_entity(
- $this->get('guid'),
+ $guid,
$this->get('owner_guid'),
$this->get('access_id'),
$this->get('container_guid'),
@@ -1301,10 +1320,7 @@ abstract class ElggEntity extends ElggData implements
$this->attributes['subtype'] = get_subtype_id($this->attributes['type'],
$this->attributes['subtype']);
- // Cache object handle
- if ($this->attributes['guid']) {
- cache_entity($this);
- }
+ cache_entity($this);
return $this->attributes['guid'];
}
diff --git a/engine/classes/ElggMetadata.php b/engine/classes/ElggMetadata.php
index 634a122e5..7f45dc3ea 100644
--- a/engine/classes/ElggMetadata.php
+++ b/engine/classes/ElggMetadata.php
@@ -26,8 +26,6 @@ class ElggMetadata extends ElggExtender {
* Construct a metadata object
*
* @param mixed $id ID of metadata or a database row as stdClass object
- *
- * @return void
*/
function __construct($id = null) {
$this->initializeAttributes();
@@ -54,7 +52,7 @@ class ElggMetadata extends ElggExtender {
*
* @param int $user_guid The GUID of the user (defaults to currently logged in user)
*
- * @return true|false Depending on permissions
+ * @return bool Depending on permissions
*/
function canEdit($user_guid = 0) {
if ($entity = get_entity($this->get('entity_guid'))) {
@@ -64,9 +62,11 @@ class ElggMetadata extends ElggExtender {
}
/**
- * Save matadata object
+ * Save metadata object
*
- * @return int the metadata object id
+ * @return int|bool the metadata object id or true if updated
+ *
+ * @throws IOException
*/
function save() {
if ($this->id > 0) {
@@ -89,7 +89,13 @@ class ElggMetadata extends ElggExtender {
* @return bool
*/
function delete() {
- return elgg_delete_metastring_based_object_by_id($this->id, 'metadata');
+ $success = elgg_delete_metastring_based_object_by_id($this->id, 'metadata');
+ if ($success) {
+ // we mark unknown here because this deletes only one value
+ // under this name, and there may be others remaining.
+ elgg_get_metadata_cache()->markUnknown($this->entity_guid, $this->name);
+ }
+ return $success;
}
/**
@@ -99,17 +105,27 @@ class ElggMetadata extends ElggExtender {
* @since 1.8
*/
function disable() {
- return elgg_set_metastring_based_object_enabled_by_id($this->id, 'no', 'metadata');
+ $success = elgg_set_metastring_based_object_enabled_by_id($this->id, 'no', 'metadata');
+ if ($success) {
+ // we mark unknown here because this disables only one value
+ // under this name, and there may be others remaining.
+ elgg_get_metadata_cache()->markUnknown($this->entity_guid, $this->name);
+ }
+ return $success;
}
/**
- * Disable the metadata
+ * Enable the metadata
*
* @return bool
* @since 1.8
*/
function enable() {
- return elgg_set_metastring_based_object_enabled_by_id($this->id, 'yes', 'metadata');
+ $success = elgg_set_metastring_based_object_enabled_by_id($this->id, 'yes', 'metadata');
+ if ($success) {
+ elgg_get_metadata_cache()->markUnknown($this->entity_guid, $this->name);
+ }
+ return $success;
}
/**
diff --git a/engine/classes/ElggPlugin.php b/engine/classes/ElggPlugin.php
index 8c9093834..3e43c8e81 100644
--- a/engine/classes/ElggPlugin.php
+++ b/engine/classes/ElggPlugin.php
@@ -101,7 +101,6 @@ class ElggPlugin extends ElggObject {
$missing_attributes = array_diff_key($expected_attributes, $row);
if ($missing_attributes) {
$needs_loaded = true;
- $old_guid = $guid;
$guid = $row['guid'];
} else {
$this->attributes = $row;
@@ -132,10 +131,7 @@ class ElggPlugin extends ElggObject {
// guid needs to be an int http://trac.elgg.org/ticket/4111
$this->attributes['guid'] = (int)$this->attributes['guid'];
- // cache the entity
- if ($this->attributes['guid']) {
- cache_entity($this);
- }
+ cache_entity($this);
return true;
}
diff --git a/engine/classes/ElggVolatileMetadataCache.php b/engine/classes/ElggVolatileMetadataCache.php
new file mode 100644
index 000000000..24ae58d42
--- /dev/null
+++ b/engine/classes/ElggVolatileMetadataCache.php
@@ -0,0 +1,344 @@
+<?php
+/**
+ * ElggVolatileMetadataCache
+ * In memory cache of known metadata values stored by entity.
+ *
+ * @package Elgg.Core
+ * @subpackage Cache
+ *
+ * @access private
+ */
+class ElggVolatileMetadataCache {
+
+ /**
+ * The cached values (or null for known to be empty). If the portion of the cache
+ * is synchronized, missing values are assumed to indicate that values do not
+ * exist in storage, otherwise, we don't know what's there.
+ *
+ * @var array
+ */
+ protected $values = array();
+
+ /**
+ * Does the cache know that it contains all names fetch-able from storage?
+ * The keys are entity GUIDs and either the value exists (true) or it's not set.
+ *
+ * @var array
+ */
+ protected $isSynchronized = array();
+
+ /**
+ * @var null|bool
+ */
+ protected $ignoreAccess = null;
+
+ /**
+ * @param int $entity_guid
+ *
+ * @param array $values
+ */
+ public function saveAll($entity_guid, array $values) {
+ if (!$this->getIgnoreAccess()) {
+ $this->values[$entity_guid] = $values;
+ $this->isSynchronized[$entity_guid] = true;
+ }
+ }
+
+ /**
+ * @param int $entity_guid
+ *
+ * @return array
+ */
+ public function loadAll($entity_guid) {
+ if (isset($this->values[$entity_guid])) {
+ return $this->values[$entity_guid];
+ } else {
+ return array();
+ }
+ }
+
+ /**
+ * Declare that there may be fetch-able metadata names in storage that this
+ * cache doesn't know about
+ *
+ * @param int $entity_guid
+ */
+ public function markOutOfSync($entity_guid) {
+ unset($this->isSynchronized[$entity_guid]);
+ }
+
+ /**
+ * @param $entity_guid
+ *
+ * @return bool
+ */
+ public function isSynchronized($entity_guid) {
+ return isset($this->isSynchronized[$entity_guid]);
+ }
+
+ /**
+ * @param int $entity_guid
+ *
+ * @param string $name
+ *
+ * @param array|int|string|null $value null means it is known that there is no
+ * fetch-able metadata under this name
+ * @param bool $allow_multiple
+ */
+ public function save($entity_guid, $name, $value, $allow_multiple = false) {
+ if ($this->getIgnoreAccess()) {
+ // we don't know if what gets saves here will be available to user once
+ // access control returns, hence it's best to forget :/
+ $this->markUnknown($entity_guid, $name);
+ } else {
+ if ($allow_multiple) {
+ if ($this->isKnown($entity_guid, $name)) {
+ $existing = $this->load($entity_guid, $name);
+ if ($existing !== null) {
+ $existing = (array) $existing;
+ $existing[] = $value;
+ $value = $existing;
+ }
+ } else {
+ // we don't know whether there are unknown values, so it's
+ // safest to leave that assumption
+ $this->markUnknown($entity_guid, $name);
+ return;
+ }
+ }
+ $this->values[$entity_guid][$name] = $value;
+ }
+ }
+
+ /**
+ * Warning: You should always call isKnown() beforehand to verify that this
+ * function's return value should be trusted (otherwise a null return value
+ * is ambiguous).
+ *
+ * @param int $entity_guid
+ *
+ * @param string $name
+ *
+ * @return array|string|int|null null = value does not exist
+ */
+ public function load($entity_guid, $name) {
+ if (isset($this->values[$entity_guid]) && array_key_exists($name, $this->values[$entity_guid])) {
+ return $this->values[$entity_guid][$name];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Forget about this metadata entry. We don't want to try to guess what the
+ * next fetch from storage will return
+ *
+ * @param int $entity_guid
+ *
+ * @param string $name
+ */
+ public function markUnknown($entity_guid, $name) {
+ unset($this->values[$entity_guid][$name]);
+ $this->markOutOfSync($entity_guid);
+ }
+
+ /**
+ * If true, load() will return an accurate value for this name
+ *
+ * @param int $entity_guid
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function isKnown($entity_guid, $name) {
+ if (isset($this->isSynchronized[$entity_guid])) {
+ return true;
+ } else {
+ return (isset($this->values[$entity_guid]) && array_key_exists($name, $this->values[$entity_guid]));
+ }
+
+ }
+
+ /**
+ * Declare that metadata under this name is known to be not fetch-able from storage
+ *
+ * @param int $entity_guid
+ *
+ * @param string $name
+ *
+ * @return array
+ */
+ public function markEmpty($entity_guid, $name) {
+ $this->values[$entity_guid][$name] = null;
+ }
+
+ /**
+ * Forget about all metadata for an entity
+ *
+ * @param int $entity_guid
+ */
+ public function clear($entity_guid) {
+ $this->values[$entity_guid] = array();
+ $this->markOutOfSync($entity_guid);
+ }
+
+ /**
+ * Clear entire cache and mark all entities as out of sync
+ */
+ public function flush() {
+ $this->values = array();
+ $this->isSynchronized = array();
+ }
+
+ /**
+ * Use this value instead of calling elgg_get_ignore_access(). By default that
+ * function will be called.
+ *
+ * This setting makes this component a little more loosely-coupled.
+ *
+ * @param bool $ignore
+ */
+ public function setIgnoreAccess($ignore) {
+ $this->ignoreAccess = (bool) $ignore;
+ }
+
+ /**
+ * Tell the cache to call elgg_get_ignore_access() to determing access status.
+ */
+ public function unsetIgnoreAccess() {
+ $this->ignoreAccess = null;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function getIgnoreAccess() {
+ if (null === $this->ignoreAccess) {
+ return elgg_get_ignore_access();
+ } else {
+ return $this->ignoreAccess;
+ }
+ }
+
+ /**
+ * Invalidate based on options passed to the global *_metadata functions
+ *
+ * @param string $action Action performed on metadata. "delete", "disable", or "enable"
+ *
+ * @param array $options Options passed to elgg_(delete|disable|enable)_metadata
+ *
+ * "guid" if given, invalidation will be limited to this entity
+ *
+ * "metadata_name" if given, invalidation will be limited to metadata with this name
+ */
+ public function invalidateByOptions($action, array $options) {
+ // remove as little as possible, optimizing for common cases
+ if (empty($options['guid'])) {
+ // safest to clear everything unless we want to make this even more complex :(
+ $this->flush();
+ } else {
+ if (empty($options['metadata_name'])) {
+ // safest to clear the whole entity
+ $this->clear($options['guid']);
+ } else {
+ switch ($action) {
+ case 'delete':
+ $this->markEmpty($options['guid'], $options['metadata_name']);
+ break;
+ default:
+ $this->markUnknown($options['guid'], $options['metadata_name']);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param int|array $guids
+ */
+ public function populateFromEntities($guids) {
+ if (empty($guids)) {
+ return;
+ }
+ if (!is_array($guids)) {
+ $guids = array($guids);
+ }
+ $guids = array_unique($guids);
+
+ // could be useful at some point in future
+ //$guids = $this->filterMetadataHeavyEntities($guids);
+
+ $db_prefix = elgg_get_config('dbprefix');
+ $options = array(
+ 'guids' => $guids,
+ 'limit' => 0,
+ 'callback' => false,
+ 'joins' => array(
+ "JOIN {$db_prefix}metastrings v ON n_table.value_id = v.id",
+ "JOIN {$db_prefix}metastrings n ON n_table.name_id = n.id",
+ ),
+ 'selects' => array('n.string AS name', 'v.string AS value'),
+ 'order_by' => 'n_table.entity_guid, n_table.time_created ASC',
+ );
+ $data = elgg_get_metadata($options);
+
+ // build up metadata for each entity, save when GUID changes (or data ends)
+ $last_guid = null;
+ $metadata = array();
+ $last_row_idx = count($data) - 1;
+ foreach ($data as $i => $row) {
+ $name = $row->name;
+ $value = ($row->value_type === 'text') ? $row->value : (int) $row->value;
+ $guid = $row->entity_guid;
+ if ($guid !== $last_guid) {
+ if ($last_guid) {
+ $this->saveAll($last_guid, $metadata);
+ }
+ $metadata = array();
+ }
+ if (isset($metadata[$name])) {
+ $metadata[$name] = (array) $metadata[$name];
+ $metadata[$name][] = $value;
+ } else {
+ $metadata[$name] = $value;
+ }
+ if (($i == $last_row_idx)) {
+ $this->saveAll($guid, $metadata);
+ }
+ $last_guid = $guid;
+ }
+ }
+
+ /**
+ * Filter out entities whose concatenated metadata values (INTs casted as string)
+ * exceed a threshold in characters. This could be used to avoid overpopulating the
+ * cache if RAM usage becomes an issue.
+ *
+ * @param array $guids GUIDs of entities to examine
+ *
+ * @param int $limit Limit in characters of all metadata (with ints casted to strings)
+ *
+ * @return array
+ */
+ public function filterMetadataHeavyEntities(array $guids, $limit = 1024000) {
+ $db_prefix = elgg_get_config('dbprefix');
+
+ $options = array(
+ 'guids' => $guids,
+ 'limit' => 0,
+ 'callback' => false,
+ 'joins' => "JOIN {$db_prefix}metastrings v ON n_table.value_id = v.id",
+ 'selects' => array('SUM(LENGTH(v.string)) AS bytes'),
+ 'order_by' => 'n_table.entity_guid, n_table.time_created ASC',
+ 'group_by' => 'n_table.entity_guid',
+ );
+ $data = elgg_get_metadata($options);
+ // don't cache if metadata for entity is over 10MB (or rolled INT)
+ foreach ($data as $row) {
+ if ($row->bytes > $limit || $row->bytes < 0) {
+ array_splice($guids, array_search($row->entity_guid, $guids), 1);
+ }
+ }
+ return $guids;
+ }
+}
diff --git a/engine/lib/entities.php b/engine/lib/entities.php
index 3896cd58f..e7f84b15b 100644
--- a/engine/lib/entities.php
+++ b/engine/lib/entities.php
@@ -39,6 +39,8 @@ function invalidate_cache_for_entity($guid) {
$guid = (int)$guid;
unset($ENTITY_CACHE[$guid]);
+
+ elgg_get_metadata_cache()->clear($guid);
}
/**
@@ -606,12 +608,14 @@ function get_entity_as_row($guid) {
*
* @param stdClass $row The row of the entry in the entities table.
*
- * @return object|false
+ * @return ElggEntity|false
* @link http://docs.elgg.org/DataModel/Entities
* @see get_entity_as_row()
* @see add_subtype()
* @see get_entity()
* @access private
+ *
+ * @throws ClassException|InstallationException
*/
function entity_row_to_elggstar($row) {
if (!($row instanceof stdClass)) {
@@ -969,17 +973,25 @@ function elgg_get_entities(array $options = array()) {
$dt = get_data($query, $options['callback']);
if ($dt) {
- foreach ($dt as $entity) {
- // If a custom callback is provided, it could return something other than ElggEntity,
- // so we have to do an explicit check here.
- if ($entity instanceof ElggEntity) {
- cache_entity($entity);
+ // populate entity and metadata caches
+ $guids = array();
+ foreach ($dt as $item) {
+ // A custom callback could result in items that aren't ElggEntity's, so check for them
+ if ($item instanceof ElggEntity) {
+ cache_entity($item);
+ // plugins usually have only settings
+ if (!$item instanceof ElggPlugin) {
+ $guids[] = $item->guid;
+ }
}
}
// @todo Without this, recursive delete fails. See #4568
reset($dt);
- }
+ if ($guids) {
+ elgg_get_metadata_cache()->populateFromEntities($guids);
+ }
+ }
return $dt;
} else {
$total = get_data_row($query);
@@ -1153,7 +1165,7 @@ function elgg_get_entity_type_subtype_where_sql($table, $types, $subtypes, $pair
* best to provide in table.column format.
* @param NULL|array $guids Array of GUIDs.
*
- * @return false|str
+ * @return false|string
* @since 1.8.0
* @access private
*/
@@ -1202,7 +1214,7 @@ function elgg_get_guid_based_where_sql($column, $guids) {
* @param NULL|int $time_updated_upper Time updated upper limit
* @param NULL|int $time_updated_lower Time updated lower limit
*
- * @return FALSE|str FALSE on fail, string on success.
+ * @return FALSE|string FALSE on fail, string on success.
* @since 1.7.0
* @access private
*/
@@ -1304,7 +1316,7 @@ function elgg_list_entities(array $options = array(), $getter = 'elgg_get_entiti
* @param string $subtype The subtype of entity
* @param int $container_guid The container GUID that the entinties belong to
* @param int $site_guid The site GUID
- * @param str $order_by Order_by SQL order by clause
+ * @param string $order_by Order_by SQL order by clause
*
* @return array|false Either an array months as YYYYMM, or false on failure
*/
@@ -1649,7 +1661,7 @@ function delete_entity($guid, $recursive = true) {
* @param string $returnvalue Return value from previous hook
* @param array $params The parameters, passed 'guid' and 'varname'
*
- * @return void
+ * @return ElggMetadata|null
* @elgg_plugin_hook_handler volatile metadata
* @todo investigate more.
* @access private
@@ -1694,6 +1706,8 @@ function volatile_data_export_plugin_hook($hook, $entity_type, $returnvalue, $pa
* @elgg_event_handler export all
* @return mixed
* @access private
+ *
+ * @throws InvalidParameterException|InvalidClassException
*/
function export_entity_plugin_hook($hook, $entity_type, $returnvalue, $params) {
// Sanity check values
@@ -1736,6 +1750,8 @@ function export_entity_plugin_hook($hook, $entity_type, $returnvalue, $params) {
* @return ElggEntity the unsaved entity which should be populated by items.
* @todo Remove this.
* @access private
+ *
+ * @throws ClassException|InstallationException|ImportException
*/
function oddentity_to_elggentity(ODDEntity $element) {
$class = $element->getAttribute('class');
@@ -1807,6 +1823,8 @@ function oddentity_to_elggentity(ODDEntity $element) {
* @elgg_plugin_hook_handler import all
* @todo document
* @access private
+ *
+ * @throws ImportException
*/
function import_entity_plugin_hook($hook, $entity_type, $returnvalue, $params) {
$element = $params['element'];
@@ -1853,8 +1871,6 @@ function import_entity_plugin_hook($hook, $entity_type, $returnvalue, $params) {
* @link http://docs.elgg.org/Entities/AccessControl
*/
function can_edit_entity($entity_guid, $user_guid = 0) {
- global $CONFIG;
-
$user_guid = (int)$user_guid;
$user = get_entity($user_guid);
if (!$user) {
@@ -1978,7 +1994,7 @@ function get_entity_url($entity_guid) {
* @param string $entity_subtype The entity subtype
* @param string $function_name The function to register
*
- * @return true|false Depending on success
+ * @return bool Depending on success
* @see get_entity_url()
* @see ElggEntity::getURL()
* @since 1.8.0
@@ -2014,7 +2030,7 @@ function elgg_register_entity_url_handler($entity_type, $entity_subtype, $functi
* @param string $type The type of entity (object, site, user, group)
* @param string $subtype The subtype to register (may be blank)
*
- * @return true|false Depending on success
+ * @return bool Depending on success
* @see get_registered_entity_types()
* @link http://docs.elgg.org/Search
* @link http://docs.elgg.org/Tutorials/Search
@@ -2051,7 +2067,7 @@ function elgg_register_entity_type($type, $subtype = null) {
* @param string $type The type of entity (object, site, user, group)
* @param string $subtype The subtype to register (may be blank)
*
- * @return true|false Depending on success
+ * @return bool Depending on success
* @see elgg_register_entity_type()
*/
function unregister_entity_type($type, $subtype) {
@@ -2118,7 +2134,7 @@ function get_registered_entity_types($type = null) {
* @param string $type The type of entity (object, site, user, group)
* @param string $subtype The subtype (may be blank)
*
- * @return true|false Depending on whether or not the type has been registered
+ * @return bool Depending on whether or not the type has been registered
*/
function is_registered_entity_type($type, $subtype = null) {
global $CONFIG;
@@ -2318,7 +2334,7 @@ function entities_gc() {
/**
* Runs unit tests for the entity objects.
*
- * @param sting $hook unit_test
+ * @param string $hook unit_test
* @param string $type system
* @param mixed $value Array of tests
* @param mixed $params Params
diff --git a/engine/lib/metadata.php b/engine/lib/metadata.php
index 77fa30e41..f76c20f24 100644
--- a/engine/lib/metadata.php
+++ b/engine/lib/metadata.php
@@ -12,7 +12,7 @@
*
* @param stdClass $row An object from the database
*
- * @return stdClass or ElggMetadata
+ * @return stdClass|ElggMetadata
* @access private
*/
function row_to_elggmetadata($row) {
@@ -30,7 +30,7 @@ function row_to_elggmetadata($row) {
*
* @param int $id The id of the metadata object being retrieved.
*
- * @return false|ElggMetadata
+ * @return ElggMetadata|false FALSE if not found
*/
function elgg_get_metadata_from_id($id) {
return elgg_get_metastring_based_object_from_id($id, 'metadata');
@@ -64,7 +64,7 @@ function elgg_delete_metadata_by_id($id) {
* @param int $access_id Default is ACCESS_PRIVATE
* @param bool $allow_multiple Allow multiple values for one key. Default is FALSE
*
- * @return int/bool id of metadata or FALSE if failure
+ * @return int|false id of metadata or FALSE if failure
*/
function create_metadata($entity_guid, $name, $value, $value_type = '', $owner_guid = 0,
$access_id = ACCESS_PRIVATE, $allow_multiple = false) {
@@ -90,8 +90,6 @@ function create_metadata($entity_guid, $name, $value, $value_type = '', $owner_g
$access_id = (int)$access_id;
- $id = false;
-
$query = "SELECT * from {$CONFIG->dbprefix}metadata"
. " WHERE entity_guid = $entity_guid and name_id=" . add_metastring($name) . " limit 1";
@@ -106,34 +104,33 @@ function create_metadata($entity_guid, $name, $value, $value_type = '', $owner_g
} else {
// Support boolean types
if (is_bool($value)) {
- if ($value) {
- $value = 1;
- } else {
- $value = 0;
- }
+ $value = (int) $value;
}
// Add the metastrings
- $value = add_metastring($value);
- if (!$value) {
+ $value_id = add_metastring($value);
+ if (!$value_id) {
return false;
}
- $name = add_metastring($name);
- if (!$name) {
+ $name_id = add_metastring($name);
+ if (!$name_id) {
return false;
}
// If ok then add it
$query = "INSERT into {$CONFIG->dbprefix}metadata"
. " (entity_guid, name_id, value_id, value_type, owner_guid, time_created, access_id)"
- . " VALUES ($entity_guid, '$name','$value','$value_type', $owner_guid, $time, $access_id)";
+ . " VALUES ($entity_guid, '$name_id','$value_id','$value_type', $owner_guid, $time, $access_id)";
$id = insert_data($query);
if ($id !== false) {
$obj = elgg_get_metadata_from_id($id);
if (elgg_trigger_event('create', 'metadata', $obj)) {
+
+ elgg_get_metadata_cache()->save($entity_guid, $name, $value, $allow_multiple);
+
return $id;
} else {
elgg_delete_metadata_by_id($id);
@@ -175,6 +172,7 @@ function update_metadata($id, $name, $value, $value_type, $owner_guid, $access_i
}
if ($metabyname_memcache) {
+ // @todo fix memcache (name_id is not a property of ElggMetadata)
$metabyname_memcache->delete("{$md->entity_guid}:{$md->name_id}");
}
@@ -187,15 +185,9 @@ function update_metadata($id, $name, $value, $value_type, $owner_guid, $access_i
$access_id = (int)$access_id;
- $access = get_access_sql_suffix();
-
// Support boolean types (as integers)
if (is_bool($value)) {
- if ($value) {
- $value = 1;
- } else {
- $value = 0;
- }
+ $value = (int) $value;
}
// Add the metastring
@@ -216,6 +208,9 @@ function update_metadata($id, $name, $value, $value_type, $owner_guid, $access_i
$result = update_data($query);
if ($result !== false) {
+
+ elgg_get_metadata_cache()->save($md->entity_guid, $name, $value);
+
// @todo this event tells you the metadata has been updated, but does not
// let you do anything about it. What is needed is a plugin hook before
// the update that passes old and new values.
@@ -234,7 +229,7 @@ function update_metadata($id, $name, $value, $value_type, $owner_guid, $access_i
* associative arrays and there is no guarantee on the ordering in the array.
*
* @param int $entity_guid The entity to attach the metadata to
- * @param string $name_and_values Associative array - a value can be a string, number, bool
+ * @param array $name_and_values Associative array - a value can be a string, number, bool
* @param string $value_type 'text', 'integer', or '' for automatic detection
* @param int $owner_guid GUID of entity that owns the metadata
* @param int $access_id Default is ACCESS_PRIVATE
@@ -308,6 +303,8 @@ function elgg_delete_metadata(array $options) {
return false;
}
+ elgg_get_metadata_cache()->invalidateByOptions('delete', $options);
+
$options['metastring_type'] = 'metadata';
return elgg_batch_metastring_based_objects($options, 'elgg_batch_delete_callback', false);
}
@@ -328,6 +325,8 @@ function elgg_disable_metadata(array $options) {
return false;
}
+ elgg_get_metadata_cache()->invalidateByOptions('disable', $options);
+
$options['metastring_type'] = 'metadata';
return elgg_batch_metastring_based_objects($options, 'elgg_batch_disable_callback', false);
}
@@ -348,6 +347,8 @@ function elgg_enable_metadata(array $options) {
return false;
}
+ elgg_get_metadata_cache()->invalidateByOptions('enable', $options);
+
$options['metastring_type'] = 'metadata';
return elgg_batch_metastring_based_objects($options, 'elgg_batch_enable_callback');
}
@@ -449,16 +450,16 @@ function elgg_get_entities_from_metadata(array $options = array()) {
* This function is reused for annotations because the tables are
* exactly the same.
*
- * @param string $e_table Entities table name
- * @param string $n_table Normalized metastrings table name (Where entities,
+ * @param string $e_table Entities table name
+ * @param string $n_table Normalized metastrings table name (Where entities,
* values, and names are joined. annotations / metadata)
- * @param arr|null $names Array of names
- * @param arr|null $values Array of values
- * @param arr|null $pairs Array of names / values / operands
- * @param and|or $pair_operator Operator to use to join the where clauses for pairs
- * @param bool $case_sensitive Case sensitive metadata names?
- * @param arr|null $order_by_metadata Array of names / direction
- * @param arr|null $owner_guids Array of owner GUIDs
+ * @param array|null $names Array of names
+ * @param array|null $values Array of values
+ * @param array|null $pairs Array of names / values / operands
+ * @param string $pair_operator ("AND" or "OR") Operator to use to join the where clauses for pairs
+ * @param bool $case_sensitive Case sensitive metadata names?
+ * @param array|null $order_by_metadata Array of names / direction
+ * @param array|null $owner_guids Array of owner GUIDs
*
* @return FALSE|array False on fail, array('joins', 'wheres')
* @since 1.7.0
@@ -732,6 +733,8 @@ function elgg_list_entities_from_metadata($options) {
*
* @return array
* @access private
+ *
+ * @throws InvalidParameterException
*/
function export_metadata_plugin_hook($hook, $entity_type, $returnvalue, $params) {
// Sanity check values
@@ -743,15 +746,13 @@ function export_metadata_plugin_hook($hook, $entity_type, $returnvalue, $params)
throw new InvalidParameterException(elgg_echo('InvalidParameterException:NonArrayReturnValue'));
}
- $guid = (int)$params['guid'];
- $name = $params['name'];
-
$result = elgg_get_metadata(array(
- 'guid' => $guid,
- 'limit' => 0
+ 'guid' => (int)$params['guid'],
+ 'limit' => 0,
));
if ($result) {
+ /* @var ElggMetadata[] $result */
foreach ($result as $r) {
$returnvalue[] = $r->export();
}
@@ -889,6 +890,50 @@ function elgg_register_metadata_url_handler($extender_name, $function) {
return elgg_register_extender_url_handler('metadata', $extender_name, $function);
}
+/**
+ * Get the global metadata cache instance
+ *
+ * @return ElggVolatileMetadataCache
+ *
+ * @access private
+ */
+function elgg_get_metadata_cache() {
+ global $CONFIG;
+ if (empty($CONFIG->local_metadata_cache)) {
+ $CONFIG->local_metadata_cache = new ElggVolatileMetadataCache();
+ }
+ return $CONFIG->local_metadata_cache;
+}
+
+/**
+ * Invalidate the metadata cache based on options passed to various *_metadata functions
+ *
+ * @param string $action Action performed on metadata. "delete", "disable", or "enable"
+ *
+ * @param array $options Options passed to elgg_(delete|disable|enable)_metadata
+ */
+function elgg_invalidate_metadata_cache($action, array $options) {
+ // remove as little as possible, optimizing for common cases
+ $cache = elgg_get_metadata_cache();
+ if (empty($options['guid'])) {
+ // safest to clear everything unless we want to make this even more complex :(
+ $cache->flush();
+ } else {
+ if (empty($options['metadata_name'])) {
+ // safest to clear the whole entity
+ $cache->clear($options['guid']);
+ } else {
+ switch ($action) {
+ case 'delete':
+ $cache->markEmpty($options['guid'], $options['metadata_name']);
+ break;
+ default:
+ $cache->markUnknown($options['guid'], $options['metadata_name']);
+ }
+ }
+ }
+}
+
/** Register the hook */
elgg_register_plugin_hook_handler("export", "all", "export_metadata_plugin_hook", 2);
@@ -912,5 +957,6 @@ elgg_register_plugin_hook_handler('unit_test', 'system', 'metadata_test');
function metadata_test($hook, $type, $value, $params) {
global $CONFIG;
$value[] = $CONFIG->path . 'engine/tests/api/metadata.php';
+ $value[] = $CONFIG->path . 'engine/tests/api/metadata_cache.php';
return $value;
-} \ No newline at end of file
+}
diff --git a/engine/tests/api/metadata_cache.php b/engine/tests/api/metadata_cache.php
new file mode 100644
index 000000000..846116a7b
--- /dev/null
+++ b/engine/tests/api/metadata_cache.php
@@ -0,0 +1,169 @@
+<?php
+/**
+ * Elgg Test metadata cache
+ *
+ * @package Elgg
+ * @subpackage Test
+ */
+class ElggCoreMetadataCacheTest extends ElggCoreUnitTest {
+
+ /**
+ * @var ElggVolatileMetadataCache
+ */
+ protected $cache;
+
+ /**
+ * @var ElggObject
+ */
+ protected $obj1;
+
+ /**
+ * @var int
+ */
+ protected $guid1;
+
+ /**
+ * @var ElggObject
+ */
+ protected $obj2;
+
+ /**
+ * @var int
+ */
+ protected $guid2;
+
+ protected $name = 'test';
+ protected $value = 'test';
+ protected $ignoreAccess;
+
+ /**
+ * Called before each test method.
+ */
+ public function setUp() {
+ $this->ignoreAccess = elgg_set_ignore_access(false);
+
+ $this->cache = elgg_get_metadata_cache();
+
+ $this->obj1 = new ElggObject();
+ $this->obj1->save();
+ $this->guid1 = $this->obj1->guid;
+
+ $this->obj2 = new ElggObject();
+ $this->obj2->save();
+ $this->guid2 = $this->obj2->guid;
+ }
+
+ /**
+ * Called after each test method.
+ */
+ public function tearDown() {
+ $this->obj1->delete();
+ $this->obj2->delete();
+
+ elgg_set_ignore_access($this->ignoreAccess);
+ }
+
+ public function testBasicApi() {
+ // test de-coupled instance
+ $cache = new ElggVolatileMetadataCache();
+ $cache->setIgnoreAccess(false);
+ $guid = 1;
+
+ $this->assertFalse($cache->isKnown($guid, $this->name));
+
+ $cache->markEmpty($guid, $this->name);
+ $this->assertTrue($cache->isKnown($guid, $this->name));
+ $this->assertNull($cache->load($guid, $this->name));
+
+ $cache->markUnknown($guid, $this->name);
+ $this->assertFalse($cache->isKnown($guid, $this->name));
+
+ $cache->save($guid, $this->name, $this->value);
+ $this->assertIdentical($cache->load($guid, $this->name), $this->value);
+
+ $cache->save($guid, $this->name, 1, true);
+ $this->assertIdentical($cache->load($guid, $this->name), array($this->value, 1));
+
+ $cache->clear($guid);
+ $this->assertFalse($cache->isKnown($guid, $this->name));
+ }
+
+ public function testReadsAreCached() {
+ // test that reads fill cache
+ $this->obj1->setMetaData($this->name, $this->value);
+ $this->cache->flush();
+
+ $this->obj1->getMetaData($this->name);
+ $this->assertIdentical($this->cache->load($this->guid1, $this->name), $this->value);
+ }
+
+ public function testWritesAreCached() {
+ // delete should mark cache as known to be empty
+ $this->obj1->deleteMetadata($this->name);
+ $this->assertTrue($this->cache->isKnown($this->guid1, $this->name));
+ $this->assertNull($this->cache->load($this->guid1, $this->name));
+
+ // without name, delete should invalidate the entire entity
+ $this->cache->save($this->guid1, $this->name, $this->value);
+ elgg_delete_metadata(array(
+ 'guid' => $this->guid1,
+ ));
+ $this->assertFalse($this->cache->isKnown($this->guid1, $this->name));
+
+ // test set
+ $this->obj1->setMetaData($this->name, $this->value);
+ $this->assertIdentical($this->cache->load($this->guid1, $this->name), $this->value);
+
+ // test set multiple
+ $this->obj1->setMetaData($this->name, 1, 'integer', true);
+ $this->assertIdentical($this->cache->load($this->guid1, $this->name), array($this->value, 1));
+
+ // writes when access is ignore should invalidate
+ $tmp_ignore = elgg_set_ignore_access(true);
+ $this->obj1->setMetaData($this->name, $this->value);
+ $this->assertFalse($this->cache->isKnown($this->guid1, $this->name));
+ elgg_set_ignore_access($tmp_ignore);
+ }
+
+ public function testDisableAndEnable() {
+ // both should mark cache unknown
+ $this->obj1->setMetaData($this->name, $this->value);
+ $this->obj1->disableMetadata($this->name);
+ $this->assertFalse($this->cache->isKnown($this->guid1, $this->name));
+
+ $this->cache->save($this->guid1, $this->name, $this->value);
+ $this->obj1->enableMetadata($this->name);
+ $this->assertFalse($this->cache->isKnown($this->guid1, $this->name));
+ }
+
+ public function testPopulateFromEntities() {
+ // test populating cache from set of entities
+ $this->obj1->setMetaData($this->name, $this->value);
+ $this->obj1->setMetaData($this->name, 4, 'integer', true);
+ $this->obj1->setMetaData("{$this->name}-2", "{$this->value}-2");
+ $this->obj2->setMetaData($this->name, $this->value);
+
+ $this->cache->flush();
+ $this->cache->populateFromEntities(array($this->guid1, $this->guid2));
+
+ $expected = array();
+ $expected[$this->name][] = $this->value;
+ $expected[$this->name][] = 4;
+ $expected["{$this->name}-2"] = "{$this->value}-2";
+ $this->assertIdentical($this->cache->loadAll($this->guid1), $expected);
+
+ $expected = array();
+ $expected[$this->name] = $this->value;
+ $this->assertIdentical($this->cache->loadAll($this->guid2), $expected);
+ }
+
+ public function testFilterHeavyEntities() {
+ $big_str = str_repeat('-', 5000);
+ $this->obj2->setMetaData($this->name, array($big_str, $big_str));
+
+ $guids = array($this->guid1, $this->guid2);
+ $expected = array($this->guid1);
+ $actual = $this->cache->filterMetadataHeavyEntities($guids, 6000);
+ $this->assertIdentical($actual, $expected);
+ }
+}