Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
QuorumLockManager
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 4
552
0.00% covered (danger)
0.00%
0 / 1
 doLockByType
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 doUnlockByType
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 doLockingRequest
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
42
 doUnlockingRequest
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 isServerUp
n/a
0 / 0
n/a
0 / 0
0
 getLocksOnServer
n/a
0 / 0
n/a
0 / 0
0
 freeLocksOnServer
n/a
0 / 0
n/a
0 / 0
0
 releaseAllLocks
n/a
0 / 0
n/a
0 / 0
0
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6namespace Wikimedia\LockManager;
7
8use StatusValue;
9use Wikimedia\ArrayUtils\ArrayUtils;
10
11/**
12 * Base class for lock managers that use a quorum of peer servers for locks.
13 *
14 * The resource space can also be sharded into separate peer groups.
15 *
16 * See MemcLockManager and RedisLockManager.
17 *
18 * @stable to extend
19 * @ingroup LockManager
20 * @since 1.20
21 */
22abstract class QuorumLockManager extends LockManager {
23    /** @var array Map server names to data used by subclasses */
24    protected $lockServers = [];
25    /** @var array list of down server names */
26    protected $downServers = [];
27    /** @var int Minimum number of success to consider lock/unlock successful */
28    protected $minVotes = 2;
29
30    /** @inheritDoc */
31    final protected function doLockByType( array $pathsByType ) {
32        $status = StatusValue::newGood();
33        $lockedPaths = []; // files locked in this attempt (type => paths)
34        foreach ( $pathsByType as $type => $paths ) {
35            foreach ( $paths as $path ) {
36                if ( isset( $this->locksHeld[$path][$type] ) ) {
37                    ++$this->locksHeld[$path][$type];
38                } else {
39                    $lockedPaths[$type][] = $path;
40                    $status->merge( $this->doLockingRequest( $path, $type ) );
41                    if ( !$status->isOK() ) {
42                        $status->merge( $this->doUnlockByType( $lockedPaths ) );
43                        return $status;
44                    } else {
45                        $this->locksHeld[$path][$type] = 1; // locked
46                    }
47                }
48            }
49        }
50
51        return $status;
52    }
53
54    /**
55     * @stable to override
56     *
57     * @param array $pathsByType
58     *
59     * @return StatusValue
60     */
61    protected function doUnlockByType( array $pathsByType ) {
62        $status = StatusValue::newGood();
63
64        foreach ( $pathsByType as $type => $paths ) {
65            foreach ( $paths as $path ) {
66                if ( !isset( $this->locksHeld[$path][$type] ) ) {
67                    $status->warning( 'lockmanager-notlocked', $path );
68                } else {
69                    --$this->locksHeld[$path][$type];
70                    // Reference count the locks held and release locks when zero
71                    if ( $this->locksHeld[$path][$type] <= 0 ) {
72                        $status->merge( $this->doUnlockingRequest( $path, $type ) );
73                        unset( $this->locksHeld[$path][$type] );
74                    }
75                    if ( $this->locksHeld[$path] === [] ) {
76                        unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
77                    }
78                }
79            }
80        }
81
82        if ( $this->locksHeld === [] ) {
83            $status->merge( $this->releaseAllLocks() );
84        }
85
86        return $status;
87    }
88
89    /**
90     * Attempt to acquire locks with the peers.
91     * This is all or nothing; if any key is locked then this totally fails.
92     *
93     * @param string $path path to be locked
94     * @param string $type type of lock to be passed to ::getLocksOnServer()
95     * @return StatusValue
96     */
97    final protected function doLockingRequest( string $path, string $type ) {
98        $status = StatusValue::newGood();
99        $yesVotes = 0; // locks made on trustable servers
100        $votesLeft = count( $this->lockServers ); // remaining peers
101        $sortedServers = array_keys( $this->lockServers );
102        // shuffle the servers based on hashing of the keys
103        ArrayUtils::consistentHashSort( $sortedServers, $path );
104        $minVotes = min( $this->minVotes, count( $this->lockServers ) );
105        foreach ( $sortedServers as $lockServer ) {
106            if ( !$this->isServerUp( $lockServer ) ) {
107                --$votesLeft;
108                $status->warning( 'lockmanager-fail-svr-acquire', $lockServer );
109                $this->downServers[$lockServer] = time();
110                continue;
111            }
112            $status->merge( $this->getLocksOnServer( $lockServer, [ $type => [ $path ] ] ) );
113            if ( !$status->isOK() ) {
114                return $status; // vetoed; resource locked
115            }
116            ++$yesVotes; // success for this peer
117            if ( $yesVotes >= $minVotes ) {
118                return $status; // lock obtained
119            }
120            --$votesLeft;
121            $votesNeeded = $minVotes - $yesVotes;
122            if ( $votesNeeded > $votesLeft ) {
123                break; // short-circuit
124            }
125        }
126        // At this point, we must not have met the quorum
127        $status->setResult( false );
128
129        return $status;
130    }
131
132    /**
133     * Attempt to release locks with the peers
134     *
135     * @param string $path the path to be unlocked
136     * @param string $type the type to be passed to ::freeLocksOnServer()
137     * @return StatusValue
138     */
139    final protected function doUnlockingRequest( string $path, string $type ) {
140        $status = StatusValue::newGood();
141
142        $yesVotes = 0; // locks freed on trustable servers
143        $isDegraded = count( $this->downServers ); // not the normal quorum?
144        $sortedServers = array_keys( $this->lockServers );
145        // shuffle the servers based on hashing of the keys
146        ArrayUtils::consistentHashSort( $sortedServers, $path );
147        $minVotes = min( $this->minVotes, count( $this->lockServers ) );
148        foreach ( $sortedServers as $lockServer ) {
149            if ( !$this->isServerUp( $lockServer ) ) {
150                $status->warning( 'lockmanager-fail-svr-release', $lockServer );
151            } else {
152                // Attempt to release the lock on this peer
153                $status->merge( $this->freeLocksOnServer( $lockServer, [ $type => [ $path ] ] ) );
154                ++$yesVotes; // success for this peer
155                // Normally the first peers form the quorum, and the others are ignored.
156                // Ignore them in this case, but not when an alternative quorum was used.
157                if ( $yesVotes >= $minVotes && !$isDegraded ) {
158                    break; // lock released
159                }
160            }
161        }
162
163        // Set a bad StatusValue if the quorum was not met.
164        // Assumes the same "up" servers as during the acquire step.
165        $status->setResult( $yesVotes >= $minVotes );
166
167        return $status;
168    }
169
170    /**
171     * Check if a lock server is up.
172     * This should process cache results to reduce RTT.
173     *
174     * @param string $lockServer
175     * @return bool
176     */
177    abstract protected function isServerUp( $lockServer );
178
179    /**
180     * Get a connection to a lock server and acquire locks
181     *
182     * @param string $lockServer
183     * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
184     * @return StatusValue
185     */
186    abstract protected function getLocksOnServer( $lockServer, array $pathsByType );
187
188    /**
189     * Get a connection to a lock server and release locks on $paths.
190     *
191     * Subclasses must effectively implement this or releaseAllLocks().
192     *
193     * @param string $lockServer
194     * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
195     * @return StatusValue
196     */
197    abstract protected function freeLocksOnServer( $lockServer, array $pathsByType );
198
199    /**
200     * Release all locks that this session is holding.
201     *
202     * Subclasses must effectively implement this or freeLocksOnServer().
203     *
204     * @return StatusValue
205     */
206    abstract protected function releaseAllLocks();
207}
208/** @deprecated class alias since 1.46 */
209class_alias( QuorumLockManager::class, 'QuorumLockManager' );