001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Florent Guillaume
016 */
017package org.nuxeo.ecm.core.redis.contribs;
018
019import java.io.IOException;
020import java.util.Arrays;
021import java.util.Calendar;
022
023import org.apache.commons.lang.StringUtils;
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026import org.nuxeo.ecm.core.api.Lock;
027import org.nuxeo.ecm.core.api.NuxeoException;
028import org.nuxeo.ecm.core.model.LockManager;
029import org.nuxeo.ecm.core.redis.RedisAdmin;
030import org.nuxeo.ecm.core.redis.RedisCallable;
031import org.nuxeo.ecm.core.redis.RedisExecutor;
032import org.nuxeo.runtime.api.Framework;
033
034import redis.clients.jedis.Jedis;
035
036/**
037 * Redis-based lock manager.
038 *
039 * @since 6.0
040 */
041public class RedisLockManager implements LockManager {
042
043    @SuppressWarnings("unused")
044    private static final Log log = LogFactory.getLog(RedisLockManager.class);
045
046    protected final String redisNamespace;
047
048    protected final String repositoryName;
049
050    protected RedisExecutor redisExecutor;
051
052    protected String prefix;
053
054    protected String scriptSetSha;
055
056    protected String scriptRemoveSha;
057
058    protected RedisAdmin redisAdmin;
059
060    /**
061     * Creates a lock manager for the given repository.
062     * <p>
063     * {@link #close} must be called when done with the lock manager.
064     */
065    public RedisLockManager(String repositoryName) {
066        this.repositoryName = repositoryName;
067        redisExecutor = Framework.getService(RedisExecutor.class);
068        redisAdmin = Framework.getService(RedisAdmin.class);
069        redisNamespace = redisAdmin.namespace("lock", repositoryName);
070        loadScripts();
071    }
072
073    public void loadScripts() {
074        try {
075            scriptSetSha = redisAdmin.load("org.nuxeo.ecm.core.redis", "set-lock");
076            scriptRemoveSha = redisAdmin.load("org.nuxeo.ecm.core.redis", "remove-lock");
077        } catch (IOException cause) {
078            throw new NuxeoException("Cannot load lock scripts in redis", cause);
079        }
080    }
081
082    protected String stringFromLock(Lock lock) {
083        if (lock == null) {
084            throw new NullPointerException("null lock");
085        }
086        return lock.getOwner() + ":" + lock.getCreated().getTimeInMillis();
087    }
088
089    protected Lock lockFromString(String lockString) {
090        if (lockString == null) {
091            return null;
092        }
093        String[] split = lockString.split(":");
094        if (split.length != 2 || !StringUtils.isNumeric(split[1])) {
095            throw new IllegalArgumentException("Invalid Redis lock : " + lockString + ", should be " + redisNamespace
096                    + "<id>");
097        }
098        Calendar created = Calendar.getInstance();
099        created.setTimeInMillis(Long.parseLong(split[1]));
100        return new Lock(split[0], created);
101    }
102
103    @Override
104    public Lock getLock(final String id) {
105        return redisExecutor.execute(new RedisCallable<Lock>() {
106            @Override
107            public Lock call(Jedis jedis) {
108                String lockString = jedis.get(redisNamespace + id);
109                return lockFromString(lockString);
110            }
111        });
112    }
113
114    @Override
115    public Lock setLock(final String id, final Lock lock) {
116        return redisExecutor.execute(new RedisCallable<Lock>() {
117            @Override
118            public Lock call(Jedis jedis) {
119                String lockString = (String) jedis.evalsha(scriptSetSha, Arrays.asList(redisNamespace + id),
120                        Arrays.asList(stringFromLock(lock)));
121                return lockFromString(lockString); // existing lock
122            }
123        });
124    }
125
126    @Override
127    public Lock removeLock(final String id, final String owner) {
128        return redisExecutor.execute(new RedisCallable<Lock>() {
129            @Override
130            public Lock call(Jedis jedis) {
131                String lockString = (String) jedis.evalsha(scriptRemoveSha, Arrays.asList(redisNamespace + id),
132                        Arrays.asList(owner == null ? "" : owner));
133                Lock lock = lockFromString(lockString);
134                if (lock != null && owner != null && !owner.equals(lock.getOwner())) {
135                    lock = new Lock(lock, true); // failed removal
136                }
137                return lock;
138            }
139        });
140    }
141
142    @Override
143    public void closeLockManager() {
144    }
145
146    @Override
147    public void clearLockManagerCaches() {
148    }
149
150    @Override
151    public String toString() {
152        return getClass().getSimpleName() + '(' + repositoryName + ')';
153    }
154
155}