001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.redis.contribs;
020
021import java.io.IOException;
022import java.util.Calendar;
023import java.util.Collections;
024import java.util.List;
025
026import org.apache.commons.lang.StringUtils;
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029import org.nuxeo.ecm.core.api.Lock;
030import org.nuxeo.ecm.core.api.NuxeoException;
031import org.nuxeo.ecm.core.model.LockManager;
032import org.nuxeo.ecm.core.redis.RedisAdmin;
033import org.nuxeo.ecm.core.redis.RedisCallable;
034import org.nuxeo.ecm.core.redis.RedisExecutor;
035import org.nuxeo.runtime.api.Framework;
036
037import redis.clients.jedis.Jedis;
038
039/**
040 * Redis-based lock manager.
041 *
042 * @since 6.0
043 */
044public class RedisLockManager implements LockManager {
045
046    @SuppressWarnings("unused")
047    private static final Log log = LogFactory.getLog(RedisLockManager.class);
048
049    protected final String redisNamespace;
050
051    protected final String repositoryName;
052
053    protected RedisExecutor redisExecutor;
054
055    protected String prefix;
056
057    protected String scriptSetSha;
058
059    protected String scriptRemoveSha;
060
061    protected RedisAdmin redisAdmin;
062
063    /**
064     * Creates a lock manager for the given repository.
065     * <p>
066     * {@link #close} must be called when done with the lock manager.
067     */
068    public RedisLockManager(String repositoryName) {
069        this.repositoryName = repositoryName;
070        redisExecutor = Framework.getService(RedisExecutor.class);
071        redisAdmin = Framework.getService(RedisAdmin.class);
072        redisNamespace = redisAdmin.namespace("lock", repositoryName);
073        loadScripts();
074    }
075
076    public void loadScripts() {
077        try {
078            scriptSetSha = redisAdmin.load("org.nuxeo.ecm.core.redis", "set-lock");
079            scriptRemoveSha = redisAdmin.load("org.nuxeo.ecm.core.redis", "remove-lock");
080        } catch (IOException cause) {
081            throw new NuxeoException("Cannot load lock scripts in redis", cause);
082        }
083    }
084
085    protected String stringFromLock(Lock lock) {
086        if (lock == null) {
087            throw new NullPointerException("null lock");
088        }
089        return lock.getOwner() + ":" + lock.getCreated().getTimeInMillis();
090    }
091
092    protected Lock lockFromString(String lockString) {
093        if (lockString == null) {
094            return null;
095        }
096        String[] split = lockString.split(":");
097        if (split.length != 2 || !StringUtils.isNumeric(split[1])) {
098            throw new IllegalArgumentException("Invalid Redis lock : " + lockString + ", should be " + redisNamespace
099                    + "<id>");
100        }
101        Calendar created = Calendar.getInstance();
102        created.setTimeInMillis(Long.parseLong(split[1]));
103        return new Lock(split[0], created);
104    }
105
106    @Override
107    public Lock getLock(final String id) {
108        return redisExecutor.execute(new RedisCallable<Lock>() {
109            @Override
110            public Lock call(Jedis jedis) {
111                String lockString = jedis.get(redisNamespace + id);
112                return lockFromString(lockString);
113            }
114        });
115    }
116
117    @Override
118    public Lock setLock(final String id, final Lock lock) {
119        List<String> keys = Collections.singletonList(redisNamespace + id);
120        List<String> args = Collections.singletonList(stringFromLock(lock));
121        String lockString = (String) redisExecutor.evalsha(scriptSetSha, keys, args);
122        return lockFromString(lockString); // existing lock
123    }
124
125    @Override
126    public Lock removeLock(final String id, final String owner) {
127        List<String> keys = Collections.singletonList(redisNamespace + id);
128        List<String> args = Collections.singletonList(owner == null ? "" : owner);
129        String lockString = (String) redisExecutor.evalsha(scriptRemoveSha, keys, args);
130        Lock lock = lockFromString(lockString);
131        if (lock != null && owner != null && !owner.equals(lock.getOwner())) {
132            lock = new Lock(lock, true); // failed removal
133        }
134        return lock;
135    }
136
137    @Override
138    public void closeLockManager() {
139    }
140
141    @Override
142    public void clearLockManagerCaches() {
143    }
144
145    @Override
146    public String toString() {
147        return getClass().getSimpleName() + '(' + repositoryName + ')';
148    }
149
150}