1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This class responsibility is twofold: |
4
|
|
|
* 1) Holding the data for a prioritized queue of URLs that needs to be static cached |
5
|
|
|
* 2) Interaction with that queue |
6
|
|
|
* |
7
|
|
|
* @TODO: would be good to refactor this queue to hold not only URLSegment, but also ClassName and ID of the |
8
|
|
|
* associated object (or any other metadata). This would allow FilesystemPublisher::publishPages and others |
9
|
|
|
* to stop having to smuggle the metadata within the URL (see URLArrayData::get_object). |
10
|
|
|
* |
11
|
|
|
*/ |
12
|
|
|
class StaticPagesQueue extends DataObject |
13
|
|
|
{ |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* |
17
|
|
|
* @var array |
18
|
|
|
*/ |
19
|
|
|
public static $create_table_options = array( |
20
|
|
|
'MySQLDatabase' => 'ENGINE=InnoDB' |
21
|
|
|
); |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* |
25
|
|
|
* @var array |
26
|
|
|
*/ |
27
|
|
|
public static $db = array( |
28
|
|
|
'Priority' => 'Int', |
29
|
|
|
'URLSegment' => 'Varchar(255)', |
30
|
|
|
'Freshness' => "Enum('stale, regenerating, error', 'stale')" |
31
|
|
|
); |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* |
35
|
|
|
* @var array |
36
|
|
|
*/ |
37
|
|
|
public static $defaults = array( |
38
|
|
|
"Priority" => 3 |
39
|
|
|
); |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* |
43
|
|
|
* @var array |
44
|
|
|
*/ |
45
|
|
|
public static $default_sort = "\"Priority\""; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Sets database indexes |
49
|
|
|
* |
50
|
|
|
* @var array |
51
|
|
|
*/ |
52
|
|
|
public static $indexes = array( |
53
|
|
|
'freshness_priority_created' => '(Freshness, Priority, Created)', |
54
|
|
|
); |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* |
58
|
|
|
* @var boolean |
59
|
|
|
*/ |
60
|
|
|
private static $realtime = false; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* |
64
|
|
|
* @var int |
65
|
|
|
*/ |
66
|
|
|
protected static $minutes_until_force_regeneration = 1; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* |
70
|
|
|
* @var array |
71
|
|
|
*/ |
72
|
|
|
protected static $insert_statements = array(); |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* |
76
|
|
|
* @var array |
77
|
|
|
*/ |
78
|
|
|
protected static $urls = array(); |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* |
82
|
|
|
* @return bool |
83
|
|
|
*/ |
84
|
|
|
public static function is_realtime() |
85
|
|
|
{ |
86
|
|
|
return Config::inst()->get('StaticPagesQueue', 'realtime'); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* |
91
|
|
|
* @param type $priority |
92
|
|
|
* @param type $URLSegment |
93
|
|
|
* @return type |
94
|
|
|
*/ |
95
|
|
|
public static function add_to_queue($priority, $URLSegment) |
96
|
|
|
{ |
97
|
|
|
$now = date("Y-m-d H:i:s"); |
98
|
|
|
self::$insert_statements[$URLSegment] = '(\''.$now.'\',\''.$now.'\', \''.Convert::raw2sql($priority).'\',\''.Convert::raw2sql($URLSegment).'\')'; |
99
|
|
|
self::$urls[md5($URLSegment)] = $URLSegment; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* This will push all the currently cached insert statements to be pushed |
104
|
|
|
* into the database |
105
|
|
|
* |
106
|
|
|
* @return void |
107
|
|
|
*/ |
108
|
|
|
public static function push_urls_to_db() |
109
|
|
|
{ |
110
|
|
|
foreach (self::$insert_statements as $stmt) { |
111
|
|
|
$insertSQL = 'INSERT INTO "StaticPagesQueue" ("Created", "LastEdited", "Priority", "URLSegment") VALUES ' . $stmt; |
112
|
|
|
DB::query($insertSQL); |
113
|
|
|
} |
114
|
|
|
self::remove_old_cache(self::$urls); |
115
|
|
|
// Flush the cache so DataObject::get works correctly |
116
|
|
|
if (!empty(self::$insert_statements) && DB::affectedRows()) { |
|
|
|
|
117
|
|
|
singleton(__CLASS__)->flushCache(); |
118
|
|
|
} |
119
|
|
|
self::$insert_statements = array(); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Remove an object by the url |
124
|
|
|
* |
125
|
|
|
* @param string $URLSegment |
126
|
|
|
* @return bool - if there was an queue item removed |
127
|
|
|
* |
128
|
|
|
*/ |
129
|
|
|
public static function delete_by_link($URLSegment) |
130
|
|
|
{ |
131
|
|
|
$object = self::get_by_link($URLSegment); |
132
|
|
|
if (!$object) { |
133
|
|
|
return false; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
$object->delete(); |
137
|
|
|
unset($object); |
138
|
|
|
return true; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Update the queue with the information that this url renders an error somehow |
143
|
|
|
* |
144
|
|
|
* @param string $url |
145
|
|
|
*/ |
146
|
|
|
public static function has_error($url) |
147
|
|
|
{ |
148
|
|
|
if (!$url) { |
149
|
|
|
return; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
$existingObject = self::get_by_link($url); |
153
|
|
|
$existingObject->Freshness = 'error'; |
154
|
|
|
$existingObject->write(); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* Returns a single queue object according to a particular priority and freshness measure. |
159
|
|
|
* This method removes any duplicates and makes the object as "regenerating", so other calls to this method |
160
|
|
|
* don't grab the same object. |
161
|
|
|
* If we are using MySQLDatabase with InnoDB, we do row-level locking when updating the dataobject to allow for |
162
|
|
|
* distributed cache rebuilds |
163
|
|
|
* @static |
164
|
|
|
* @param $freshness |
165
|
|
|
* @param $sortOrder |
166
|
|
|
*/ |
167
|
|
|
protected static function get_queue_object($freshness, $interval = null, $sortOrder = array('Priority'=>'DESC', 'ID'=>'ASC')) |
168
|
|
|
{ |
169
|
|
|
$className = __CLASS__; |
170
|
|
|
$queueObject = null; |
171
|
|
|
$filterQuery = array("Freshness" => $freshness); |
172
|
|
|
if ($interval) { |
173
|
|
|
$filterQuery["LastEdited:LessThan"] = $interval; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
$query = self::get(); |
177
|
|
|
if ($query->Count() > 0) { |
178
|
|
|
$offset = 0; |
179
|
|
|
$filteredQuery = $query->filter($filterQuery)->sort($sortOrder); |
180
|
|
|
|
181
|
|
|
if ($filteredQuery->Count() > 0) { |
182
|
|
|
if (DB::getConn() instanceof MySQLDatabase) { //locking currently only works on MySQL |
|
|
|
|
183
|
|
|
|
184
|
|
|
do { |
185
|
|
|
$queueObject = $filteredQuery->limit(1, $offset)->first(); //get first item |
186
|
|
|
|
187
|
|
|
if ($queueObject) { |
188
|
|
|
$lockName = md5($queueObject->URLSegment . $className); |
189
|
|
|
} |
190
|
|
|
//try to locking the item's URL, keep trying new URLs until we find one that is free to lock |
191
|
|
|
$offset++; |
192
|
|
|
} while ($queueObject && !LockMySQL::isFreeToLock($lockName)); |
|
|
|
|
193
|
|
|
|
194
|
|
|
if ($queueObject) { |
195
|
|
|
$lockSuccess = LockMySQL::getLock($lockName); //acquire a lock with the URL of the queue item we have just fetched |
196
|
|
|
if ($lockSuccess) { |
197
|
|
|
self::remove_duplicates($queueObject->ID); //remove any duplicates |
198
|
|
|
self::mark_as_regenerating($queueObject); //mark as regenerating so nothing else grabs it |
199
|
|
|
LockMySQL::releaseLock($lockName); //return the object and release the lock |
200
|
|
|
} |
201
|
|
|
} |
202
|
|
|
} else { |
203
|
|
|
$queueObject = $filteredQuery->first(); |
204
|
|
|
self::remove_duplicates($queueObject->ID); |
205
|
|
|
self::mark_as_regenerating($queueObject); |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
return $queueObject; //return the object or null |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Finds the next most prioritized url that needs recaching |
215
|
|
|
* |
216
|
|
|
* @return string |
217
|
|
|
*/ |
218
|
|
|
public static function get_next_url() |
219
|
|
|
{ |
220
|
|
|
$object = self::get_queue_object('stale'); |
221
|
|
|
if ($object) { |
222
|
|
|
return $object->URLSegment; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
$interval = date('Y-m-d H:i:s', strtotime('-'.self::$minutes_until_force_regeneration.' minutes')); |
226
|
|
|
|
227
|
|
|
// Find URLs that has been stuck in regeneration |
228
|
|
|
$object = self::get_queue_object('regenerating', $interval); |
229
|
|
|
if ($object) { |
230
|
|
|
return $object->URLSegment; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
// Find URLs that is erronous and might work now (flush issues etc) |
234
|
|
|
$object = self::get_queue_object('error', $interval); |
235
|
|
|
if ($object) { |
236
|
|
|
return $object->URLSegment; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
return ''; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Removes the .html fresh copy of the cache. |
244
|
|
|
* Keeps the *.stale.html copy in place, |
245
|
|
|
* in order to notify the user of the stale content. |
246
|
|
|
* |
247
|
|
|
* @param array $URLSegments |
248
|
|
|
*/ |
249
|
|
|
protected static function remove_old_cache(array $URLSegments) |
250
|
|
|
{ |
251
|
|
|
$publisher = singleton('SiteTree')->getExtensionInstance('FilesystemPublisher'); |
252
|
|
|
if ($publisher) { |
253
|
|
|
$paths = $publisher->urlsToPaths($URLSegments); |
254
|
|
|
foreach ($paths as $absolutePath) { |
255
|
|
|
if (!file_exists($publisher->getDestDir().'/'.$absolutePath)) { |
256
|
|
|
continue; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
unlink($publisher->getDestDir().'/'.$absolutePath); |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* Mark this current StaticPagesQueue as a work in progress |
266
|
|
|
* |
267
|
|
|
* @param StaticPagesQueue $object |
268
|
|
|
*/ |
269
|
|
|
protected static function mark_as_regenerating(StaticPagesQueue $object) |
270
|
|
|
{ |
271
|
|
|
$now = date('Y-m-d H:i:s'); |
272
|
|
|
DB::query('UPDATE "StaticPagesQueue" SET "LastEdited" = \''.$now.'\', "Freshness"=\'regenerating\' WHERE "ID" = '.$object->ID); |
273
|
|
|
singleton(__CLASS__)->flushCache(); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* Removes all duplicates that has the same URLSegment as $ID |
278
|
|
|
* |
279
|
|
|
* @param int $ID - ID of the object whose duplicates we want to remove |
280
|
|
|
* @return void |
281
|
|
|
*/ |
282
|
|
|
public static function remove_duplicates($ID) |
283
|
|
|
{ |
284
|
|
|
$obj = DataObject::get_by_id('StaticPagesQueue', $ID); |
285
|
|
|
if (!$obj) { |
286
|
|
|
return 0; |
287
|
|
|
} |
288
|
|
|
DB::query( |
289
|
|
|
sprintf('DELETE FROM "StaticPagesQueue" WHERE "URLSegment" = \'%s\' AND "ID" != %d', $obj->URLSegment, (int)$ID) |
290
|
|
|
); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* |
295
|
|
|
* @param string $url |
296
|
|
|
* @param bool $onlyStale - Get only stale entries |
|
|
|
|
297
|
|
|
* @return DataObject || false - The first item matching the query |
298
|
|
|
*/ |
299
|
|
|
protected static function get_by_link($url) |
300
|
|
|
{ |
301
|
|
|
$filter = '"URLSegment" = \''.Convert::raw2sql($url).'\''; |
302
|
|
|
$res = DB::query('SELECT * FROM "StaticPagesQueue" WHERE '.$filter.' LIMIT 1;'); |
303
|
|
|
if (!$res->numRecords()) { |
304
|
|
|
return false; |
305
|
|
|
} |
306
|
|
|
return new StaticPagesQueue($res->first()); |
307
|
|
|
} |
308
|
|
|
} |
309
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.