Completed
Push — master ( ee70e4...91224a )
by mw
37:03
created

CachedQueryResultPrefetcher::resetCacheBy()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 12
nop 1
dl 0
loc 22
ccs 12
cts 12
cp 1
crap 5
rs 8.6737
c 0
b 0
f 0
1
<?php
2
3
namespace SMW;
4
5
use Onoi\BlobStore\BlobStore;
6
use SMWQuery as Query;
7
use SMWQueryResult as QueryResult;
8
use SMW\Store;
9
use SMW\DIWikiPage;
10
use SMW\QueryEngine;
11
use SMW\QueryFactory;
12
use Psr\Log\LoggerInterface;
13
use Psr\Log\LoggerAwareInterface;
14
use RuntimeException;
15
16
/**
17
 * The prefetcher only contains cached subject list from a computed a query
18
 * condition. The result is processed before an individual
19
 * query printer has access to the query result hence it does not interfere
20
 * with the final string output manipulation.
21
 *
22
 * The main objective is to avoid unnecessary computing of results for queries
23
 * that represent the same query signature. PrintRequests as part of a QueryResult
24
 * object are not cached and are not part of a query signature.
25
 *
26
 * Cache eviction is carried out either manually (action=purge) or executed
27
 * through the QueryDepedencyLinksStore.
28
 *
29
 * @license GNU GPL v2+
30
 * @since 2.5
31
 *
32
 * @author mwjames
33
 */
34
class CachedQueryResultPrefetcher implements QueryEngine, LoggerAwareInterface {
35
36
	/**
37
	 * Update this version number when the serialization format
38
	 * changes.
39
	 */
40
	const VERSION = '0.2';
41
42
	/**
43
	 * Namespace occupied by the BlobStore
44
	 */
45
	const CACHE_NAMESPACE = 'smw:query:store';
46
47
	/**
48
	 * ID used by the TransientStatsdCollector
49
	 *
50
	 * PHP 5.6 can do self::CACHE_NAMESPACE . ':' . self::VERSION
51
	 */
52
	const STATSD_ID = 'smw:query:store:0.2';
53
54
	/**
55
	 * @var Store
56
	 */
57
	private $store;
58
59
	/**
60
	 * @var QueryFactory
61
	 */
62
	private $queryFactory;
63
64
	/**
65
	 * @var BlobStore
66
	 */
67
	private $blobStore;
68
69
	/**
70
	 * @var QueryEngine
71
	 */
72
	private $queryEngine;
73
74
	/**
75
	 * @var TransientStatsdCollector
76
	 */
77
	private $transientStatsdCollector;
78
79
	/**
80
	 * @var integer|boolean
81
	 */
82
	private $nonEmbeddedCacheLifetime = false;
83
84
	/**
85
	 * @var integer
86
	 */
87
	private $start = 0;
88
89
	/**
90
	 * @var boolean
91
	 */
92
	private $enabledCache = true;
93
94
	/**
95
	 * loggerInterface
96
	 */
97
	private $logger;
98
99
	/**
100
	 * @since 2.5
101
	 *
102
	 * @param Store $store
103
	 * @param QueryFactory $queryFactory
104
	 * @param BlobStore $blobStore
105
	 * @param TransientStatsdCollector $transientStatsdCollector
106
	 */
107 183
	public function __construct( Store $store, QueryFactory $queryFactory, BlobStore $blobStore, TransientStatsdCollector $transientStatsdCollector ) {
108 183
		$this->store = $store;
109 183
		$this->queryFactory = $queryFactory;
110 183
		$this->blobStore = $blobStore;
111 183
		$this->transientStatsdCollector = $transientStatsdCollector;
112
113 183
		$this->initStats( date( 'Y-m-d H:i:s' ) );
114 183
	}
115
116
	/**
117
	 * @since 2.5
118
	 *
119
	 * @return array
120
	 */
121 1
	public function getStats() {
122
123
		$stats = array_filter( $this->transientStatsdCollector->getStats(), function( $key ) {
124
			return $key !== false;
125 1
		} );
126
127 1
		return $stats;
128
	}
129
130
	/**
131
	 * @see LoggerAwareInterface::setLogger
132
	 *
133
	 * @since 2.5
134
	 *
135
	 * @param LoggerInterface $logger
136
	 */
137 176
	public function setLogger( LoggerInterface $logger ) {
138 176
		$this->logger = $logger;
139 176
	}
140
141
	/**
142
	 * @since 2.5
143
	 *
144
	 * @param QueryEngine $queryEngine
145
	 */
146 179
	public function setQueryEngine( QueryEngine $queryEngine ) {
147 179
		$this->queryEngine = $queryEngine;
148 179
	}
149
150
	/**
151
	 * @since 2.5
152
	 *
153
	 * @param boolean
154
	 */
155 183
	public function isEnabled() {
156 183
		return $this->blobStore->canUse();
157
	}
158
159
	/**
160
	 * @since 2.5
161
	 *
162
	 * @param QueryEngine $queryEngine
0 ignored issues
show
Bug introduced by
There is no parameter named $queryEngine. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
163
	 */
164
	public function disableCache() {
165
		$this->enabledCache = false;
166
	}
167
168
	/**
169
	 * @since 2.5
170
	 */
171
	public function recordStats() {
172
		$this->transientStatsdCollector->recordStats();
173 176
	}
174 176
175 176
	/**
176
	 * @since 2.5
177
	 *
178
	 * @param integer|boolean $nonEmbeddedCacheLifetime
179
	 */
180
	public function setNonEmbeddedCacheLifetime( $nonEmbeddedCacheLifetime ) {
181
		$this->nonEmbeddedCacheLifetime = $nonEmbeddedCacheLifetime;
182
	}
183
184 172
	/**
185
	 * @since 2.5
186 172
	 *
187 1
	 * @param Query $query
188
	 *
189
	 * @return QueryResult|string
190 171
	 */
191 133
	public function getQueryResult( Query $query ) {
192 133
193
		if ( !$this->queryEngine instanceof QueryEngine ) {
194
			throw new RuntimeException( "Missing a QueryEngine instance." );
195 39
		}
196
197
		if ( !$this->canUse( $query ) || $query->getLimit() < 1 || $query->getOptionBy( Query::NO_CACHE ) === true ) {
198
			$this->transientStatsdCollector->incr( 'noCache' );
199 39
			return $this->queryEngine->getQueryResult( $query );
200
		}
201 39
202 39
		$this->start = microtime( true );
0 ignored issues
show
Documentation Bug introduced by
The property $start was declared of type integer, but microtime(true) is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
203
204
		// Use the queryId without a subject to reuse the content among other
205 39
		// entities that may have embedded a query with the same query signature
206 11
		$queryId = $query->getQueryId();
207
208
		$container = $this->blobStore->read(
209 39
			$this->getHashFrom( $queryId )
210
		);
211 39
212 39
		if ( $container->has( 'results' ) ) {
213
			return $this->newQueryResultFromCache( $queryId, $query, $container );
214 39
		}
215 39
216
		$queryResult = $this->queryEngine->getQueryResult( $query );
217
218 39
		$time = round( ( microtime( true ) - $this->start ), 5 );
219
		$this->log( __METHOD__ . ' from backend in (sec): ' . $time . " ($queryId)" );
220
221
		if ( $this->canUse( $query ) && $queryResult instanceof QueryResult ) {
222
			$this->addQueryResultToCache( $queryResult, $queryId, $container, $query );
223
		}
224
225
		return $queryResult;
226 77
	}
227
228 77
	/**
229 3
	 * @since 2.5
230
	 *
231
	 * @param DIWikiPage|array $list
0 ignored issues
show
Bug introduced by
There is no parameter named $list. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
232 77
	 */
233 77
	public function resetCacheBy( $item ) {
234 77
235
		if ( !is_array( $item ) ) {
236 77
			$item = array( $item );
237
		}
238 171
239 171
		$recordStats = false;
240
241
		foreach ( $item as $id ) {
242 11
			$id = $this->getHashFrom( $id );
243
244 11
			if ( $this->blobStore->exists( $id ) ) {
245
				$recordStats = true;
246 11
				$this->transientStatsdCollector->incr( 'deletes' );
247 11
				$this->blobStore->delete( $id );
248
			}
249
		}
250 11
251 11
		if ( $recordStats ) {
252 11
			$this->recordStats();
253
		}
254
	}
255 11
256 9
	private function canUse( $query ) {
257
		return $this->enabledCache && $this->blobStore->canUse() && ( $query->getContextPage() !== null || ( $query->getContextPage() === null && $this->nonEmbeddedCacheLifetime > 0 ) );
258
	}
259 11
260 11
	private function newQueryResultFromCache( $queryId, $query, $container ) {
261
262
		$results = array();
263 11
264
		$this->transientStatsdCollector->incr(
265
			( $query->getContextPage() !== null ? 'hits.embedded' : 'hits.nonEmbedded' )
266 11
		);
267 11
268
		$this->transientStatsdCollector->calcMedian(
269 11
			'medianRetrievalResponseTime.cached',
270
			round( ( microtime( true ) - $this->start ), 5 )
271 11
		);
272
273 11
		foreach ( $container->get( 'results' ) as $hash ) {
274
			$results[] = DIWikiPage::doUnserialize( $hash );
275
		}
276 39
277
		$queryResult = $this->queryFactory->newQueryResult(
278 39
			$this->store,
279
			$query,
280 39
			$results,
281 39
			$container->get( 'continue' )
282 39
		);
283
284
		$queryResult->setCountValue( $container->get( 'count' ) );
285 39
		$queryResult->setFromCache( true );
286 39
287 39
		$time = round( ( microtime( true ) - $this->start ), 5 );
288
289 39
		$this->log( __METHOD__ . ' (sec): ' . $time . " ($queryId)" );
290
291
		return $queryResult;
292
	}
293 39
294 39
	private function addQueryResultToCache( $queryResult, $queryId, $container, $query ) {
295 39
296 39
		$this->transientStatsdCollector->incr( 'misses' );
297
298 39
		$this->transientStatsdCollector->calcMedian(
299
			'medianRetrievalResponseTime.uncached',
300 39
			round( ( microtime( true ) - $this->start ), 5 )
301
		);
302
303
		$callback = function() use( $queryResult, $queryId, $container, $query ) {
304 39
			$this->doCacheQueryResult( $queryResult, $queryId, $container, $query );
305 31
		};
306
307
		$deferredCallableUpdate = ApplicationFactory::getInstance()->newDeferredCallableUpdate(
308 39
			$callback
309 39
		);
310 39
311
		$deferredCallableUpdate->setOrigin( __METHOD__ );
312 39
		$deferredCallableUpdate->setFingerprint( __METHOD__ . $queryId );
313 39
		$deferredCallableUpdate->pushToUpdateQueue();
314
	}
315 39
316 33
	private function doCacheQueryResult( $queryResult, $queryId, $container, $query ) {
317 33
318
		$results = array();
319 6
320 6
		// Keep the simple string representation to avoid unnecessary data cruft
321
		// during using PHP serialize( ... )
322
		foreach ( $queryResult->getResults() as $dataItem ) {
323 39
			$results[] = $dataItem->getSerialization();
324
		}
325
326
		$container->set( 'results', $results );
327 39
		$container->set( 'continue', $queryResult->hasFurtherResults() );
328
		$container->set( 'count', $queryResult->getCountValue() );
329 39
330
		$queryResult->reset();
331 39
		$contextPage = $query->getContextPage();
332
333
		if ( $contextPage === null ) {
334 6
			$container->setExpiryInSeconds( $this->nonEmbeddedCacheLifetime );
335
			$hash = 'nonEmbedded';
0 ignored issues
show
Unused Code introduced by
$hash is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
336
		} else {
337
			$this->addToLinkedList( $contextPage, $queryId );
338 6
			$hash = $contextPage->getHash();
0 ignored issues
show
Unused Code introduced by
$hash is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
339 6
		}
340
341
		$this->blobStore->save(
342
			$container
343
		);
344 6
345 6
		$time = round( ( microtime( true ) - $this->start ), 5 );
346
347
		$this->log( __METHOD__ . ' cache storage (sec): ' . $time . " ($queryId)" );
348 6
349
		return $queryResult;
350
	}
351 6
352
	private function addToLinkedList( $contextPage, $queryId ) {
353 114
354
		// Ensure that without QueryDependencyLinksStore being enabled recorded
355 114
		// subjects related to a query can be discoverable and purged separately
356 9
		$container = $this->blobStore->read(
357
			$this->getHashFrom( $contextPage )
358
		);
359 114
360
		// If a subject gets purged the the linked list of queries associated
361
		// with that subject allows for an immediate associated removal
362 39
		$container->addToLinkedList(
363
			$this->getHashFrom( $queryId )
364 39
		);
365
366
		$this->blobStore->save(
367
			$container
368 39
		);
369 39
	}
370
371 183
	private function getHashFrom( $subject ) {
372
373 183
		if ( $subject instanceof DIWikiPage ) {
374
			$subject = $subject->getHash();
375 183
		}
376 183
377 183
		return md5( $subject . self::VERSION );
378 183
	}
379 183
380 183
	private function log( $message, $context = array() ) {
381 183
382 183
		if ( $this->logger === null ) {
383 183
			return;
384 183
		}
385 183
386
		$this->logger->info( $message, $context );
387
	}
388
389
	private function initStats( $date ) {
0 ignored issues
show
Coding Style introduced by
initStats uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
390
391
		$this->transientStatsdCollector->shouldRecord( $this->isEnabled() );
392
393
		$this->transientStatsdCollector->init( 'misses', 0 );
394
		$this->transientStatsdCollector->init( 'deletes', 0 );
395
		$this->transientStatsdCollector->init( 'noCache', 0 );
396
		$this->transientStatsdCollector->init( 'hits', array() );
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a string|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
397
		$this->transientStatsdCollector->init( 'medianRetrievalResponseTime', array() );
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a string|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
398
		$this->transientStatsdCollector->set( 'meta.version', self::VERSION );
399
		$this->transientStatsdCollector->set( 'meta.cacheLifetime.embedded', $GLOBALS['smwgQueryResultCacheLifetime'] );
400
		$this->transientStatsdCollector->set( 'meta.cacheLifetime.nonEmbedded', $GLOBALS['smwgQueryResultNonEmbeddedCacheLifetime']  );
401
		$this->transientStatsdCollector->init( 'meta.collectionDate.start', $date );
402
		$this->transientStatsdCollector->set(  'meta.collectionDate.update', $date );
403
	}
404
405
}
406