Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php
13419 views
1
<?php
2
3
/**
4
* Add and remove edges between objects. You can use
5
* @{class:PhabricatorEdgeQuery} to load object edges. For more information
6
* on edges, see @{article:Using Edges}.
7
*
8
* Edges are not directly policy aware, and this editor makes low-level changes
9
* below the policy layer.
10
*
11
* name=Adding Edges
12
* $src = $earth_phid;
13
* $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE;
14
* $dst = $moon_phid;
15
*
16
* id(new PhabricatorEdgeEditor())
17
* ->addEdge($src, $type, $dst)
18
* ->save();
19
*
20
* @task edit Editing Edges
21
* @task cycles Cycle Prevention
22
* @task internal Internals
23
*/
24
final class PhabricatorEdgeEditor extends Phobject {
25
26
private $addEdges = array();
27
private $remEdges = array();
28
private $openTransactions = array();
29
30
31
/* -( Editing Edges )------------------------------------------------------ */
32
33
34
/**
35
* Add a new edge (possibly also adding its inverse). Changes take effect when
36
* you call @{method:save}. If the edge already exists, it will not be
37
* overwritten, but if data is attached to the edge it will be updated.
38
* Removals queued with @{method:removeEdge} are executed before
39
* adds, so the effect of removing and adding the same edge is to overwrite
40
* any existing edge.
41
*
42
* The `$options` parameter accepts these values:
43
*
44
* - `data` Optional, data to write onto the edge.
45
* - `inverse_data` Optional, data to write on the inverse edge. If not
46
* provided, `data` will be written.
47
*
48
* @param phid Source object PHID.
49
* @param const Edge type constant.
50
* @param phid Destination object PHID.
51
* @param map Options map (see documentation).
52
* @return this
53
*
54
* @task edit
55
*/
56
public function addEdge($src, $type, $dst, array $options = array()) {
57
foreach ($this->buildEdgeSpecs($src, $type, $dst, $options) as $spec) {
58
$this->addEdges[] = $spec;
59
}
60
return $this;
61
}
62
63
64
/**
65
* Remove an edge (possibly also removing its inverse). Changes take effect
66
* when you call @{method:save}. If an edge does not exist, the removal
67
* will be ignored. Edges are added after edges are removed, so the effect of
68
* a remove plus an add is to overwrite.
69
*
70
* @param phid Source object PHID.
71
* @param const Edge type constant.
72
* @param phid Destination object PHID.
73
* @return this
74
*
75
* @task edit
76
*/
77
public function removeEdge($src, $type, $dst) {
78
foreach ($this->buildEdgeSpecs($src, $type, $dst) as $spec) {
79
$this->remEdges[] = $spec;
80
}
81
return $this;
82
}
83
84
85
/**
86
* Apply edge additions and removals queued by @{method:addEdge} and
87
* @{method:removeEdge}. Note that transactions are opened, all additions and
88
* removals are executed, and then transactions are saved. Thus, in some cases
89
* it may be slightly more efficient to perform multiple edit operations
90
* (e.g., adds followed by removals) if their outcomes are not dependent,
91
* since transactions will not be held open as long.
92
*
93
* @return this
94
* @task edit
95
*/
96
public function save() {
97
98
$cycle_types = $this->getPreventCyclesEdgeTypes();
99
100
$locks = array();
101
$caught = null;
102
try {
103
104
// NOTE: We write edge data first, before doing any transactions, since
105
// it's OK if we just leave it hanging out in space unattached to
106
// anything.
107
$this->writeEdgeData();
108
109
// If we're going to perform cycle detection, lock the edge type before
110
// doing edits.
111
if ($cycle_types) {
112
$src_phids = ipull($this->addEdges, 'src');
113
foreach ($cycle_types as $cycle_type) {
114
$key = 'edge.cycle:'.$cycle_type;
115
$locks[] = PhabricatorGlobalLock::newLock($key)->lock(15);
116
}
117
}
118
119
static $id = 0;
120
$id++;
121
122
// NOTE: Removes first, then adds, so that "remove + add" is a useful
123
// operation meaning "overwrite".
124
125
$this->executeRemoves();
126
$this->executeAdds();
127
128
foreach ($cycle_types as $cycle_type) {
129
$this->detectCycles($src_phids, $cycle_type);
130
}
131
132
$this->saveTransactions();
133
} catch (Exception $ex) {
134
$caught = $ex;
135
}
136
137
if ($caught) {
138
$this->killTransactions();
139
}
140
141
foreach ($locks as $lock) {
142
$lock->unlock();
143
}
144
145
if ($caught) {
146
throw $caught;
147
}
148
}
149
150
151
/* -( Internals )---------------------------------------------------------- */
152
153
154
/**
155
* Build the specification for an edge operation, and possibly build its
156
* inverse as well.
157
*
158
* @task internal
159
*/
160
private function buildEdgeSpecs($src, $type, $dst, array $options = array()) {
161
$data = array();
162
if (!empty($options['data'])) {
163
$data['data'] = $options['data'];
164
}
165
166
$src_type = phid_get_type($src);
167
$dst_type = phid_get_type($dst);
168
169
$specs = array();
170
$specs[] = array(
171
'src' => $src,
172
'src_type' => $src_type,
173
'dst' => $dst,
174
'dst_type' => $dst_type,
175
'type' => $type,
176
'data' => $data,
177
);
178
179
$type_obj = PhabricatorEdgeType::getByConstant($type);
180
$inverse = $type_obj->getInverseEdgeConstant();
181
if ($inverse !== null) {
182
183
// If `inverse_data` is set, overwrite the edge data. Normally, just
184
// write the same data to the inverse edge.
185
if (array_key_exists('inverse_data', $options)) {
186
$data['data'] = $options['inverse_data'];
187
}
188
189
$specs[] = array(
190
'src' => $dst,
191
'src_type' => $dst_type,
192
'dst' => $src,
193
'dst_type' => $src_type,
194
'type' => $inverse,
195
'data' => $data,
196
);
197
}
198
199
return $specs;
200
}
201
202
203
/**
204
* Write edge data.
205
*
206
* @task internal
207
*/
208
private function writeEdgeData() {
209
$adds = $this->addEdges;
210
211
$writes = array();
212
foreach ($adds as $key => $edge) {
213
if ($edge['data']) {
214
$writes[] = array($key, $edge['src_type'], json_encode($edge['data']));
215
}
216
}
217
218
foreach ($writes as $write) {
219
list($key, $src_type, $data) = $write;
220
$conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
221
queryfx(
222
$conn_w,
223
'INSERT INTO %T (data) VALUES (%s)',
224
PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA,
225
$data);
226
$this->addEdges[$key]['data_id'] = $conn_w->getInsertID();
227
}
228
}
229
230
231
/**
232
* Add queued edges.
233
*
234
* @task internal
235
*/
236
private function executeAdds() {
237
$adds = $this->addEdges;
238
$adds = igroup($adds, 'src_type');
239
240
// Assign stable sequence numbers to each edge, so we have a consistent
241
// ordering across edges by source and type.
242
foreach ($adds as $src_type => $edges) {
243
$edges_by_src = igroup($edges, 'src');
244
foreach ($edges_by_src as $src => $src_edges) {
245
$seq = 0;
246
foreach ($src_edges as $key => $edge) {
247
$src_edges[$key]['seq'] = $seq++;
248
$src_edges[$key]['dateCreated'] = time();
249
}
250
$edges_by_src[$src] = $src_edges;
251
}
252
$adds[$src_type] = array_mergev($edges_by_src);
253
}
254
255
$inserts = array();
256
foreach ($adds as $src_type => $edges) {
257
$conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
258
$sql = array();
259
foreach ($edges as $edge) {
260
$sql[] = qsprintf(
261
$conn_w,
262
'(%s, %d, %s, %d, %d, %nd)',
263
$edge['src'],
264
$edge['type'],
265
$edge['dst'],
266
$edge['dateCreated'],
267
$edge['seq'],
268
idx($edge, 'data_id'));
269
}
270
$inserts[] = array($conn_w, $sql);
271
}
272
273
foreach ($inserts as $insert) {
274
list($conn_w, $sql) = $insert;
275
$conn_w->openTransaction();
276
$this->openTransactions[] = $conn_w;
277
278
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
279
queryfx(
280
$conn_w,
281
'INSERT INTO %T (src, type, dst, dateCreated, seq, dataID)
282
VALUES %LQ ON DUPLICATE KEY UPDATE dataID = VALUES(dataID)',
283
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
284
$chunk);
285
}
286
}
287
}
288
289
290
/**
291
* Remove queued edges.
292
*
293
* @task internal
294
*/
295
private function executeRemoves() {
296
$rems = $this->remEdges;
297
$rems = igroup($rems, 'src_type');
298
299
$deletes = array();
300
foreach ($rems as $src_type => $edges) {
301
$conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
302
$sql = array();
303
foreach ($edges as $edge) {
304
$sql[] = qsprintf(
305
$conn_w,
306
'(src = %s AND type = %d AND dst = %s)',
307
$edge['src'],
308
$edge['type'],
309
$edge['dst']);
310
}
311
$deletes[] = array($conn_w, $sql);
312
}
313
314
foreach ($deletes as $delete) {
315
list($conn_w, $sql) = $delete;
316
317
$conn_w->openTransaction();
318
$this->openTransactions[] = $conn_w;
319
320
foreach (array_chunk($sql, 256) as $chunk) {
321
queryfx(
322
$conn_w,
323
'DELETE FROM %T WHERE %LO',
324
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
325
$chunk);
326
}
327
}
328
}
329
330
331
/**
332
* Save open transactions.
333
*
334
* @task internal
335
*/
336
private function saveTransactions() {
337
foreach ($this->openTransactions as $key => $conn_w) {
338
$conn_w->saveTransaction();
339
unset($this->openTransactions[$key]);
340
}
341
}
342
343
private function killTransactions() {
344
foreach ($this->openTransactions as $key => $conn_w) {
345
$conn_w->killTransaction();
346
unset($this->openTransactions[$key]);
347
}
348
}
349
350
351
/* -( Cycle Prevention )--------------------------------------------------- */
352
353
354
/**
355
* Get a list of all edge types which are being added, and which we should
356
* prevent cycles on.
357
*
358
* @return list<const> List of edge types which should have cycles prevented.
359
* @task cycle
360
*/
361
private function getPreventCyclesEdgeTypes() {
362
$edge_types = array();
363
foreach ($this->addEdges as $edge) {
364
$edge_types[$edge['type']] = true;
365
}
366
foreach ($edge_types as $type => $ignored) {
367
$type_obj = PhabricatorEdgeType::getByConstant($type);
368
if (!$type_obj->shouldPreventCycles()) {
369
unset($edge_types[$type]);
370
}
371
}
372
return array_keys($edge_types);
373
}
374
375
376
/**
377
* Detect graph cycles of a given edge type. If the edit introduces a cycle,
378
* a @{class:PhabricatorEdgeCycleException} is thrown with details.
379
*
380
* @return void
381
* @task cycle
382
*/
383
private function detectCycles(array $phids, $edge_type) {
384
// For simplicity, we just seed the graph with the affected nodes rather
385
// than seeding it with their edges. To do this, we just add synthetic
386
// edges from an imaginary '<seed>' node to the known edges.
387
388
389
$graph = id(new PhabricatorEdgeGraph())
390
->setEdgeType($edge_type)
391
->addNodes(
392
array(
393
'<seed>' => $phids,
394
))
395
->loadGraph();
396
397
foreach ($phids as $phid) {
398
$cycle = $graph->detectCycles($phid);
399
if ($cycle) {
400
throw new PhabricatorEdgeCycleException($edge_type, $cycle);
401
}
402
}
403
}
404
405
406
}
407
408