Skip to content

Commit

Permalink
Add pessimistic locking. Closes etrepat#89.
Browse files Browse the repository at this point in the history
Use pessimistic locks when performing certain operations which
may result in a race condition. Mainly:

+ When moving nodes (updates bounds and depth for whole subbranches).

+ While looking for rightmost node (at every node creation) which may
result in some error, mainly if it gets updated while doing this.

+ When pruning whole subtrees.
  • Loading branch information
etrepat committed Jul 17, 2014
1 parent ae9d7e5 commit 8af3c83
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 5 deletions.
23 changes: 19 additions & 4 deletions src/Baum/Move.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,7 @@ public function perform() {

$this->target->reload();

$this->node->setDepth();

foreach($this->node->getDescendants() as $descendant)
$descendant->save();
$this->node->setDepthWithSubtree();

$this->node->reload();
}
Expand All @@ -129,6 +126,9 @@ public function perform() {
public function updateStructure() {
list($a, $b, $c, $d) = $this->boundaries();

// select the rows between the leftmost & the rightmost boundaries and apply a lock
$this->applyLockBetween($a, $d);

$connection = $this->node->getConnection();
$grammar = $connection->getQueryGrammar();

Expand Down Expand Up @@ -348,4 +348,19 @@ protected function quoteIdentifier($value) {
return $pdo->quote($value);
}

/**
* Applies a lock to the rows between the supplied index boundaries.
*
* @param int $lft
* @param int $rgt
* @return void
*/
protected function applyLockBetween($lft, $rgt) {
$this->node->newQuery()
->where($this->node->getLeftColumnName(), '>=', $lft)
->where($this->node->getRightColumnName(), '<=', $rgt)
->select($this->node->getKeyName())
->lockForUpdate()
->get();
}
}
33 changes: 32 additions & 1 deletion src/Baum/Node.php
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,7 @@ public function insideSubtree($node) {
* @return void
*/
public function setDefaultLeftAndRight() {
$withHighestRight = $this->newNestedSetQuery()->reOrderBy($this->getRightColumnName(), 'desc')->take(1)->first();
$withHighestRight = $this->newNestedSetQuery()->reOrderBy($this->getRightColumnName(), 'desc')->take(1)->sharedLock()->first();

$maxRgt = 0;
if ( !is_null($withHighestRight) ) $maxRgt = $withHighestRight->getRight();
Expand Down Expand Up @@ -1030,6 +1030,34 @@ public function setDepth() {
return $this;
}

/**
* Sets the depth attribute for the current node and all of its descendants.
*
* @return \Baum\Node
*/
public function setDepthWithSubtree() {
$self = $this;

$this->getConnection()->transaction(function() use ($self) {
$self->reload();

$self->descendantsAndSelf()->select($self->getKeyName())->lockForUpdate()->get();

$oldDepth = !is_null($self->getDepth()) ?: 0;

$newDepth = $self->getLevel();

$self->newNestedSetQuery()->where($self->getKeyName(), '=', $self->getKey())->update(array($self->getDepthColumnName() => $newDepth));
$self->setAttribute($self->getDepthColumnName(), $newDepth);

$diff = $newDepth - $oldDepth;
if ( !$self->isLeaf() && $diff != 0 )
$self->descendants()->increment($self->getDepthColumnName(), $diff);
});

return $this;
}

/**
* Prunes a branch off the tree, shifting all the elements on the right
* back to the left so the counts work.
Expand All @@ -1049,6 +1077,9 @@ public function destroyDescendants() {
$lft = $self->getLeft();
$rgt = $self->getRight();

// Apply a lock to the rows which fall past the deletion point
$self->newNestedSetQuery()->where($lftCol, '>=', $lft)->select($self->getKeyName())->lockForUpdate()->get();

// Prune children
$self->newNestedSetQuery()->where($lftCol, '>', $lft)->where($rgtCol, '<', $rgt)->delete();

Expand Down

0 comments on commit 8af3c83

Please sign in to comment.