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 {
36+ /** @var array<string, array{time: int, ttl: int}> */
37+ private array $ oldTTLs = [];
38+
3539 public function __construct (
3640 private IMemcache $ memcache ,
41+ private ITimeFactory $ timeFactory ,
3742 int $ ttl = 3600 ,
3843 ) {
3944 parent ::__construct ($ ttl );
4045 }
4146
42- private function setTTL (string $ path ): void {
47+ private function setTTL (string $ path , int $ ttl = null , ?int $ compare = null ): void {
48+ if (is_null ($ ttl )) {
49+ $ ttl = $ this ->ttl ;
50+ }
51+ if ($ this ->memcache instanceof IMemcacheTTL) {
52+ if ($ compare !== null ) {
53+ $ this ->memcache ->compareSetTTL ($ path , $ compare , $ ttl );
54+ } else {
55+ $ this ->memcache ->setTTL ($ path , $ ttl );
56+ }
57+ }
58+ }
59+
60+ private function getTTL (string $ path ): int {
4361 if ($ this ->memcache instanceof IMemcacheTTL) {
44- $ this ->memcache ->setTTL ($ path , $ this ->ttl );
62+ $ ttl = $ this ->memcache ->getTTL ($ path );
63+ return $ ttl === false ? -1 : $ ttl ;
64+ } else {
65+ return -1 ;
4566 }
4667 }
4768
@@ -58,14 +79,22 @@ public function isLocked(string $path, int $type): bool {
5879
5980 public function acquireLock (string $ path , int $ type , ?string $ readablePath = null ): void {
6081 if ($ type === self ::LOCK_SHARED ) {
82+ // save the old TTL to for `restoreTTL`
83+ $ this ->oldTTLs [$ path ] = [
84+ "ttl " => $ this ->getTTL ($ path ),
85+ "time " => $ this ->timeFactory ->getTime ()
86+ ];
6187 if (!$ this ->memcache ->inc ($ path )) {
6288 throw new LockedException ($ path , null , $ this ->getExistingLockForException ($ path ), $ readablePath );
6389 }
6490 } else {
91+ // when getting exclusive locks, we know there are no old TTLs to restore
6592 $ this ->memcache ->add ($ path , 0 );
93+ // ttl is updated automatically when the `set` succeeds
6694 if (!$ this ->memcache ->cas ($ path , 0 , 'exclusive ' )) {
6795 throw new LockedException ($ path , null , $ this ->getExistingLockForException ($ path ), $ readablePath );
6896 }
97+ unset($ this ->oldTTLs [$ path ]);
6998 }
7099 $ this ->setTTL ($ path );
71100 $ this ->markAcquire ($ path , $ type );
@@ -88,6 +117,12 @@ public function releaseLock(string $path, int $type): void {
88117 $ newValue = $ this ->memcache ->dec ($ path );
89118 }
90119
120+ if ($ newValue > 0 ) {
121+ $ this ->restoreTTL ($ path );
122+ } else {
123+ unset($ this ->oldTTLs [$ path ]);
124+ }
125+
91126 // if we somehow release more locks then exists, reset the lock
92127 if ($ newValue < 0 ) {
93128 $ this ->memcache ->cad ($ path , $ newValue );
@@ -106,13 +141,52 @@ public function changeLock(string $path, int $targetType): void {
106141 } elseif ($ targetType === self ::LOCK_EXCLUSIVE ) {
107142 // we can only change a shared lock to an exclusive if there's only a single owner of the shared lock
108143 if (!$ this ->memcache ->cas ($ path , 1 , 'exclusive ' )) {
144+ $ this ->restoreTTL ($ path );
109145 throw new LockedException ($ path , null , $ this ->getExistingLockForException ($ path ));
110146 }
147+ unset($ this ->oldTTLs [$ path ]);
111148 }
112149 $ this ->setTTL ($ path );
113150 $ this ->markChange ($ path , $ targetType );
114151 }
115152
153+ /**
154+ * With shared locks, each time the lock is acquired, the ttl for the path is reset.
155+ *
156+ * Due to this "ttl extension" when a shared lock isn't freed correctly for any reason
157+ * the lock won't expire until no shared locks are required for the path for 1h.
158+ * This can lead to a client repeatedly trying to upload a file, and failing forever
159+ * because the lock never gets the opportunity to expire.
160+ *
161+ * To help the lock expire in this case, we lower the TTL back to what it was before we
162+ * took the shared lock *only* if nobody else got a shared lock after we did.
163+ *
164+ * This doesn't handle all cases where multiple requests are acquiring shared locks
165+ * but it should handle some of the more common ones and not hurt things further
166+ */
167+ private function restoreTTL (string $ path ): void {
168+ if (isset ($ this ->oldTTLs [$ path ])) {
169+ $ saved = $ this ->oldTTLs [$ path ];
170+ $ elapsed = $ this ->timeFactory ->getTime () - $ saved ['time ' ];
171+
172+ // old value to compare to when setting ttl in case someone else changes the lock in the middle of this function
173+ $ value = $ this ->memcache ->get ($ path );
174+
175+ $ currentTtl = $ this ->getTTL ($ path );
176+
177+ // what the old ttl would be given the time elapsed since we acquired the lock
178+ // note that if this gets negative the key will be expired directly when we set the ttl
179+ $ remainingOldTtl = $ saved ['ttl ' ] - $ elapsed ;
180+ // what the currently ttl would be if nobody else acquired a lock since we did (+1 to cover rounding errors)
181+ $ expectedTtl = $ this ->ttl - $ elapsed + 1 ;
182+
183+ // check if another request has acquired a lock (and didn't release it yet)
184+ if ($ currentTtl <= $ expectedTtl ) {
185+ $ this ->setTTL ($ path , $ remainingOldTtl , $ value );
186+ }
187+ }
188+ }
189+
116190 private function getExistingLockForException (string $ path ): string {
117191 $ existing = $ this ->memcache ->get ($ path );
118192 if (!$ existing ) {
0 commit comments