2727 */
2828namespace OC \Lock ;
2929
30+ use OCP \AppFramework \Utility \ITimeFactory ;
3031use OCP \IMemcache ;
3132use OCP \IMemcacheTTL ;
3233use OCP \Lock \LockedException ;
3334
3435class MemcacheLockingProvider extends AbstractLockingProvider {
3536 private IMemcache $ memcache ;
37+ private ITimeFactory $ timeFactory ;
38+
39+ /** @var array<string, array{time: int, ttl: int}> */
40+ private $ oldTTLs = [];
3641
3742 public function __construct (IMemcache $ memcache , int $ ttl = 3600 ) {
3843 $ this ->memcache = $ memcache ;
3944 $ this ->ttl = $ ttl ;
45+ $ this ->timeFactory = \OC ::$ server ->get (ITimeFactory::class);
46+ }
47+
48+ private function setTTL (string $ path , int $ ttl = null , $ compare = null ): void {
49+ if (is_null ($ ttl )) {
50+ $ ttl = $ this ->ttl ;
51+ }
52+ if ($ this ->memcache instanceof IMemcacheTTL) {
53+ if ($ compare !== null ) {
54+ $ this ->memcache ->compareSetTTL ($ path , $ compare , $ ttl );
55+ } else {
56+ $ this ->memcache ->setTTL ($ path , $ ttl );
57+ }
58+ }
4059 }
4160
42- private function setTTL (string $ path ): void {
61+ private function getTTL (string $ path ): int {
4362 if ($ this ->memcache instanceof IMemcacheTTL) {
44- $ this ->memcache ->setTTL ($ path , $ this ->ttl );
63+ $ ttl = $ this ->memcache ->getTTL ($ path );
64+ return $ ttl === false ? -1 : $ ttl ;
65+ } else {
66+ return -1 ;
4567 }
4668 }
4769
@@ -58,14 +80,22 @@ public function isLocked(string $path, int $type): bool {
5880
5981 public function acquireLock (string $ path , int $ type , ?string $ readablePath = null ): void {
6082 if ($ type === self ::LOCK_SHARED ) {
83+ // save the old TTL to for `restoreTTL`
84+ $ this ->oldTTLs [$ path ] = [
85+ "ttl " => $ this ->getTTL ($ path ),
86+ "time " => $ this ->timeFactory ->getTime ()
87+ ];
6188 if (!$ this ->memcache ->inc ($ path )) {
6289 throw new LockedException ($ path , null , $ this ->getExistingLockForException ($ path ), $ readablePath );
6390 }
6491 } else {
92+ // when getting exclusive locks, we know there are no old TTLs to restore
6593 $ this ->memcache ->add ($ path , 0 );
94+ // ttl is updated automatically when the `set` succeeds
6695 if (!$ this ->memcache ->cas ($ path , 0 , 'exclusive ' )) {
6796 throw new LockedException ($ path , null , $ this ->getExistingLockForException ($ path ), $ readablePath );
6897 }
98+ unset($ this ->oldTTLs [$ path ]);
6999 }
70100 $ this ->setTTL ($ path );
71101 $ this ->markAcquire ($ path , $ type );
@@ -88,6 +118,12 @@ public function releaseLock(string $path, int $type): void {
88118 $ newValue = $ this ->memcache ->dec ($ path );
89119 }
90120
121+ if ($ newValue > 0 ) {
122+ $ this ->restoreTTL ($ path );
123+ } else {
124+ unset($ this ->oldTTLs [$ path ]);
125+ }
126+
91127 // if we somehow release more locks then exists, reset the lock
92128 if ($ newValue < 0 ) {
93129 $ this ->memcache ->cad ($ path , $ newValue );
@@ -106,13 +142,52 @@ public function changeLock(string $path, int $targetType): void {
106142 } elseif ($ targetType === self ::LOCK_EXCLUSIVE ) {
107143 // we can only change a shared lock to an exclusive if there's only a single owner of the shared lock
108144 if (!$ this ->memcache ->cas ($ path , 1 , 'exclusive ' )) {
145+ $ this ->restoreTTL ($ path );
109146 throw new LockedException ($ path , null , $ this ->getExistingLockForException ($ path ));
110147 }
148+ unset($ this ->oldTTLs [$ path ]);
111149 }
112150 $ this ->setTTL ($ path );
113151 $ this ->markChange ($ path , $ targetType );
114152 }
115153
154+ /**
155+ * With shared locks, each time the lock is acquired, the ttl for the path is reset.
156+ *
157+ * Due to this "ttl extension" when a shared lock isn't freed correctly for any reason
158+ * the lock won't expire until no shared locks are required for the path for 1h.
159+ * This can lead to a client repeatedly trying to upload a file, and failing forever
160+ * because the lock never gets the opportunity to expire.
161+ *
162+ * To help the lock expire in this case, we lower the TTL back to what it was before we
163+ * took the shared lock *only* if nobody else got a shared lock after we did.
164+ *
165+ * This doesn't handle all cases where multiple requests are acquiring shared locks
166+ * but it should handle some of the more common ones and not hurt things further
167+ */
168+ private function restoreTTL (string $ path ): void {
169+ if (isset ($ this ->oldTTLs [$ path ])) {
170+ $ saved = $ this ->oldTTLs [$ path ];
171+ $ elapsed = $ this ->timeFactory ->getTime () - $ saved ['time ' ];
172+
173+ // old value to compare to when setting ttl in case someone else changes the lock in the middle of this function
174+ $ value = $ this ->memcache ->get ($ path );
175+
176+ $ currentTtl = $ this ->getTTL ($ path );
177+
178+ // what the old ttl would be given the time elapsed since we acquired the lock
179+ // note that if this gets negative the key will be expired directly when we set the ttl
180+ $ remainingOldTtl = $ saved ['ttl ' ] - $ elapsed ;
181+ // what the currently ttl would be if nobody else acquired a lock since we did (+1 to cover rounding errors)
182+ $ expectedTtl = $ this ->ttl - $ elapsed + 1 ;
183+
184+ // check if another request has acquired a lock (and didn't release it yet)
185+ if ($ currentTtl <= $ expectedTtl ) {
186+ $ this ->setTTL ($ path , $ remainingOldTtl , $ value );
187+ }
188+ }
189+ }
190+
116191 private function getExistingLockForException (string $ path ): string {
117192 $ existing = $ this ->memcache ->get ($ path );
118193 if (!$ existing ) {
0 commit comments