Merge branch 'redisson/master'
commit
39f876ee8f
@ -0,0 +1,42 @@
|
||||
Redis based Tomcat Session Manager
|
||||
===
|
||||
|
||||
Implements non-sticky session management backed by Redis.
|
||||
Supports Tomcat 6.x, 7.x, 8.x
|
||||
|
||||
Advantages
|
||||
===
|
||||
|
||||
Current implementation differs from any other Tomcat Session Manager in terms of efficient storage and optimized writes. Each session attribute is written into Redis during each `HttpSession.setAttribute` invocation. While other solutions serialize whole session each time.
|
||||
|
||||
Usage
|
||||
===
|
||||
1. Add `RedissonSessionManager` into `context.xml`
|
||||
```xml
|
||||
<Manager className="org.redisson.tomcat.RedissonSessionManager"
|
||||
configPath="${catalina.base}/redisson.conf" />
|
||||
```
|
||||
`configPath` - path to Redisson JSON or YAML config. See [configuration wiki page](https://github.com/redisson/redisson/wiki/2.-Configuration) for more details.
|
||||
|
||||
2. Copy two jars into `TOMCAT_BASE/lib` directory:
|
||||
|
||||
1. __For JDK 1.8+__
|
||||
[redisson-all-3.3.0.jar](https://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=org.redisson&a=redisson-all&v=3.3.0&e=jar)
|
||||
|
||||
for Tomcat 6.x
|
||||
[redisson-tomcat-6-3.3.0.jar](https://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=org.redisson&a=redisson-tomcat-6&v=3.3.0&e=jar)
|
||||
for Tomcat 7.x
|
||||
[redisson-tomcat-7-3.3.0.jar](https://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=org.redisson&a=redisson-tomcat-7&v=3.3.0&e=jar)
|
||||
for Tomcat 8.x
|
||||
[redisson-tomcat-8-3.3.0.jar](https://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=org.redisson&a=redisson-tomcat-8&v=3.3.0&e=jar)
|
||||
|
||||
1. __For JDK 1.6+__
|
||||
[redisson-all-2.8.0.jar](https://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=org.redisson&a=redisson-all&v=2.8.0&e=jar)
|
||||
|
||||
for Tomcat 6.x
|
||||
[redisson-tomcat-6-2.8.0.jar](https://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=org.redisson&a=redisson-tomcat-6&v=2.8.0&e=jar)
|
||||
for Tomcat 7.x
|
||||
[redisson-tomcat-7-2.8.0.jar](https://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=org.redisson&a=redisson-tomcat-7&v=2.8.0&e=jar)
|
||||
for Tomcat 8.x
|
||||
[redisson-tomcat-8-2.8.0.jar](https://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=org.redisson&a=redisson-tomcat-8&v=2.8.0&e=jar)
|
||||
|
@ -0,0 +1,86 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-parent</artifactId>
|
||||
<version>2.8.1-SNAPSHOT</version>
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>redisson-tomcat</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>Redisson/Tomcat</name>
|
||||
|
||||
<modules>
|
||||
<module>redisson-tomcat-6</module>
|
||||
<module>redisson-tomcat-7</module>
|
||||
<module>redisson-tomcat-8</module>
|
||||
</modules>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<configuration>
|
||||
<source>${source.version}</source>
|
||||
<target>${source.version}</target>
|
||||
<optimize>true</optimize>
|
||||
<showDeprecations>true</showDeprecations>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>default-testCompile</id>
|
||||
<phase>process-test-sources</phase>
|
||||
<goals>
|
||||
<goal>testCompile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<fork>true</fork>
|
||||
<source>${test.source.version}</source>
|
||||
<target>${test.source.version}</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>2.10.4</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>fluent-hc</artifactId>
|
||||
<version>4.5.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,64 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-tomcat</artifactId>
|
||||
<version>2.8.1-SNAPSHOT</version>
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>redisson-tomcat-6</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Redisson/Tomcat-6</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>catalina</artifactId>
|
||||
<version>6.0.48</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.mycila</groupId>
|
||||
<artifactId>license-maven-plugin</artifactId>
|
||||
<version>3.0</version>
|
||||
<configuration>
|
||||
<basedir>${basedir}</basedir>
|
||||
<header>${basedir}/../../header.txt</header>
|
||||
<quiet>false</quiet>
|
||||
<failIfMissing>true</failIfMissing>
|
||||
<aggregate>false</aggregate>
|
||||
<includes>
|
||||
<include>src/main/java/org/redisson/</include>
|
||||
</includes>
|
||||
<excludes>
|
||||
<exclude>target/**</exclude>
|
||||
</excludes>
|
||||
<useDefaultExcludes>true</useDefaultExcludes>
|
||||
<mapping>
|
||||
<java>JAVADOC_STYLE</java>
|
||||
</mapping>
|
||||
<strictCheck>true</strictCheck>
|
||||
<useDefaultMapping>true</useDefaultMapping>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.catalina.session.StandardSession;
|
||||
import org.redisson.api.RMap;
|
||||
|
||||
/**
|
||||
* Redisson Session object for Apache Tomcat
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public class RedissonSession extends StandardSession {
|
||||
|
||||
private final RedissonSessionManager redissonManager;
|
||||
private final Map<String, Object> attrs;
|
||||
private RMap<String, Object> map;
|
||||
|
||||
public RedissonSession(RedissonSessionManager manager) {
|
||||
super(manager);
|
||||
this.redissonManager = manager;
|
||||
|
||||
try {
|
||||
Field attr = StandardSession.class.getDeclaredField("attributes");
|
||||
attrs = (Map<String, Object>) attr.get(this);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = -2518607181636076487L;
|
||||
|
||||
@Override
|
||||
public void setId(String id, boolean notify) {
|
||||
super.setId(id, notify);
|
||||
map = redissonManager.getMap(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCreationTime(long time) {
|
||||
super.setCreationTime(time);
|
||||
|
||||
if (map != null) {
|
||||
Map<String, Object> newMap = new HashMap<String, Object>(3);
|
||||
newMap.put("session:creationTime", creationTime);
|
||||
newMap.put("session:lastAccessedTime", lastAccessedTime);
|
||||
newMap.put("session:thisAccessedTime", thisAccessedTime);
|
||||
map.putAll(newMap);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void access() {
|
||||
super.access();
|
||||
|
||||
if (map != null) {
|
||||
Map<String, Object> newMap = new HashMap<String, Object>(2);
|
||||
newMap.put("session:lastAccessedTime", lastAccessedTime);
|
||||
newMap.put("session:thisAccessedTime", thisAccessedTime);
|
||||
map.putAll(newMap);
|
||||
if (getMaxInactiveInterval() >= 0) {
|
||||
map.expire(getMaxInactiveInterval(), TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(int interval) {
|
||||
super.setMaxInactiveInterval(interval);
|
||||
|
||||
if (map != null) {
|
||||
map.fastPut("session:maxInactiveInterval", maxInactiveInterval);
|
||||
if (maxInactiveInterval >= 0) {
|
||||
map.expire(getMaxInactiveInterval(), TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValid(boolean isValid) {
|
||||
super.setValid(isValid);
|
||||
|
||||
if (map != null) {
|
||||
map.fastPut("session:isValid", isValid);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNew(boolean isNew) {
|
||||
super.setNew(isNew);
|
||||
|
||||
if (map != null) {
|
||||
map.fastPut("session:isNew", isNew);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAccess() {
|
||||
boolean oldValue = isNew;
|
||||
super.endAccess();
|
||||
|
||||
if (isNew != oldValue) {
|
||||
map.fastPut("session:isNew", isNew);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, Object value, boolean notify) {
|
||||
super.setAttribute(name, value, notify);
|
||||
|
||||
if (map != null && value != null) {
|
||||
map.fastPut(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeAttributeInternal(String name, boolean notify) {
|
||||
super.removeAttributeInternal(name, notify);
|
||||
|
||||
if (map != null) {
|
||||
map.fastRemove(name);
|
||||
}
|
||||
}
|
||||
|
||||
public void save() {
|
||||
Map<String, Object> newMap = new HashMap<String, Object>();
|
||||
newMap.put("session:creationTime", creationTime);
|
||||
newMap.put("session:lastAccessedTime", lastAccessedTime);
|
||||
newMap.put("session:thisAccessedTime", thisAccessedTime);
|
||||
newMap.put("session:maxInactiveInterval", maxInactiveInterval);
|
||||
newMap.put("session:isValid", isValid);
|
||||
newMap.put("session:isNew", isNew);
|
||||
|
||||
for (Entry<String, Object> entry : attrs.entrySet()) {
|
||||
newMap.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
map.putAll(newMap);
|
||||
|
||||
if (maxInactiveInterval >= 0) {
|
||||
map.expire(getMaxInactiveInterval(), TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
public void load() {
|
||||
Set<Entry<String, Object>> entrySet = map.readAllEntrySet();
|
||||
for (Entry<String, Object> entry : entrySet) {
|
||||
if ("session:creationTime".equals(entry.getKey())) {
|
||||
creationTime = (Long) entry.getValue();
|
||||
} else if ("session:lastAccessedTime".equals(entry.getKey())) {
|
||||
lastAccessedTime = (Long) entry.getValue();
|
||||
} else if ("session:thisAccessedTime".equals(entry.getKey())) {
|
||||
thisAccessedTime = (Long) entry.getValue();
|
||||
} else if ("session:maxInactiveInterval".equals(entry.getKey())) {
|
||||
maxInactiveInterval = (Integer) entry.getValue();
|
||||
} else if ("session:isValid".equals(entry.getKey())) {
|
||||
isValid = (Boolean) entry.getValue();
|
||||
} else if ("session:isNew".equals(entry.getKey())) {
|
||||
isNew = (Boolean) entry.getValue();
|
||||
} else {
|
||||
setAttribute(entry.getKey(), entry.getValue(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.juli.logging.Log;
|
||||
import org.apache.juli.logging.LogFactory;
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.Lifecycle;
|
||||
import org.apache.catalina.LifecycleException;
|
||||
import org.apache.catalina.LifecycleListener;
|
||||
import org.apache.catalina.Session;
|
||||
import org.apache.catalina.session.ManagerBase;
|
||||
import org.apache.catalina.util.LifecycleSupport;
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RMap;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.config.Config;
|
||||
|
||||
/**
|
||||
* Redisson Session Manager for Apache Tomcat
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public class RedissonSessionManager extends ManagerBase implements Lifecycle {
|
||||
|
||||
private final Log log = LogFactory.getLog(RedissonSessionManager.class);
|
||||
|
||||
protected LifecycleSupport lifecycle = new LifecycleSupport(this);
|
||||
|
||||
private RedissonClient redisson;
|
||||
private String configPath;
|
||||
|
||||
public void setConfigPath(String configPath) {
|
||||
this.configPath = configPath;
|
||||
}
|
||||
|
||||
public String getConfigPath() {
|
||||
return configPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRejectedSessions() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() throws ClassNotFoundException, IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRejectedSessions(int sessions) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.addLifecycleListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifecycleListener[] findLifecycleListeners() {
|
||||
return lifecycle.findLifecycleListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.removeLifecycleListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session createSession(String sessionId) {
|
||||
RedissonSession session = (RedissonSession) createEmptySession();
|
||||
|
||||
session.setNew(true);
|
||||
session.setValid(true);
|
||||
session.setCreationTime(System.currentTimeMillis());
|
||||
session.setMaxInactiveInterval(((Context) getContainer()).getSessionTimeout() * 60);
|
||||
|
||||
if (sessionId == null) {
|
||||
sessionId = generateSessionId();
|
||||
}
|
||||
|
||||
session.setId(sessionId);
|
||||
session.save();
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
public RMap<String, Object> getMap(String sessionId) {
|
||||
return redisson.getMap("redisson_tomcat_session:" + sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session findSession(String id) throws IOException {
|
||||
Session result = super.findSession(id);
|
||||
if (result == null && id != null) {
|
||||
RedissonSession session = (RedissonSession) createEmptySession();
|
||||
session.setId(id);
|
||||
session.load();
|
||||
return session;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session createEmptySession() {
|
||||
return new RedissonSession(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Session session) {
|
||||
super.remove(session);
|
||||
|
||||
getMap(session.getId()).delete();
|
||||
}
|
||||
|
||||
public RedissonClient getRedisson() {
|
||||
return redisson;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws LifecycleException {
|
||||
Config config = null;
|
||||
try {
|
||||
config = Config.fromJSON(new File(configPath));
|
||||
} catch (IOException e) {
|
||||
// trying next format
|
||||
try {
|
||||
config = Config.fromYAML(new File(configPath));
|
||||
} catch (IOException e1) {
|
||||
log.error("Can't parse json config " + configPath, e);
|
||||
throw new LifecycleException("Can't parse yaml config " + configPath, e1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
redisson = Redisson.create(config);
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException(e);
|
||||
}
|
||||
|
||||
lifecycle.fireLifecycleEvent(START_EVENT, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws LifecycleException {
|
||||
try {
|
||||
if (redisson != null) {
|
||||
redisson.shutdown();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException(e);
|
||||
}
|
||||
|
||||
lifecycle.fireLifecycleEvent(STOP_EVENT, null);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.catalina.LifecycleException;
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.fluent.Executor;
|
||||
import org.apache.http.client.fluent.Request;
|
||||
import org.apache.http.cookie.Cookie;
|
||||
import org.apache.http.impl.client.BasicCookieStore;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class RedissonSessionManagerTest {
|
||||
|
||||
@Test
|
||||
public void testSwitchServer() throws LifecycleException, InterruptedException, ClientProtocolException, IOException {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "/src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
BasicCookieStore cookieStore = new BasicCookieStore();
|
||||
executor.use(cookieStore);
|
||||
|
||||
write(executor, "test", "1234");
|
||||
Cookie cookie = cookieStore.getCookies().get(0);
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
|
||||
server = new TomcatServer("myapp", 8080, "/src/test/");
|
||||
server.start();
|
||||
|
||||
executor = Executor.newInstance();
|
||||
cookieStore = new BasicCookieStore();
|
||||
cookieStore.addCookie(cookie);
|
||||
executor.use(cookieStore);
|
||||
read(executor, "test", "1234");
|
||||
remove(executor, "test", "null");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testWriteReadRemove() throws LifecycleException, InterruptedException, ClientProtocolException, IOException {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "/src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
|
||||
write(executor, "test", "1234");
|
||||
read(executor, "test", "1234");
|
||||
remove(executor, "test", "null");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecreate() throws LifecycleException, InterruptedException, ClientProtocolException, IOException {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "/src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
|
||||
write(executor, "test", "1");
|
||||
recreate(executor, "test", "2");
|
||||
read(executor, "test", "2");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdate() throws LifecycleException, InterruptedException, ClientProtocolException, IOException {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "/src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
|
||||
write(executor, "test", "1");
|
||||
read(executor, "test", "1");
|
||||
write(executor, "test", "2");
|
||||
read(executor, "test", "2");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testInvalidate() throws LifecycleException, InterruptedException, ClientProtocolException, IOException {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "/src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
BasicCookieStore cookieStore = new BasicCookieStore();
|
||||
executor.use(cookieStore);
|
||||
|
||||
write(executor, "test", "1234");
|
||||
Cookie cookie = cookieStore.getCookies().get(0);
|
||||
invalidate(executor);
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
|
||||
executor = Executor.newInstance();
|
||||
cookieStore = new BasicCookieStore();
|
||||
cookieStore.addCookie(cookie);
|
||||
executor.use(cookieStore);
|
||||
read(executor, "test", "null");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
private void write(Executor executor, String key, String value) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/write?key=" + key + "&value=" + value;
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals("OK", response);
|
||||
}
|
||||
|
||||
private void read(Executor executor, String key, String value) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/read?key=" + key;
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals(value, response);
|
||||
}
|
||||
|
||||
private void remove(Executor executor, String key, String value) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/remove?key=" + key;
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals(value, response);
|
||||
}
|
||||
|
||||
private void invalidate(Executor executor) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/invalidate";
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals("OK", response);
|
||||
}
|
||||
|
||||
private void recreate(Executor executor, String key, String value) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/recreate?key=" + key + "&value=" + value;
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals("OK", response);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
public class TestServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 1243830648280853203L;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
HttpSession session = req.getSession();
|
||||
|
||||
if (req.getPathInfo().equals("/write")) {
|
||||
String[] params = req.getQueryString().split("&");
|
||||
String key = null;
|
||||
String value = null;
|
||||
for (String param : params) {
|
||||
String[] paramLine = param.split("=");
|
||||
String keyParam = paramLine[0];
|
||||
String valueParam = paramLine[1];
|
||||
|
||||
if ("key".equals(keyParam)) {
|
||||
key = valueParam;
|
||||
}
|
||||
if ("value".equals(keyParam)) {
|
||||
value = valueParam;
|
||||
}
|
||||
}
|
||||
session.setAttribute(key, value);
|
||||
|
||||
resp.getWriter().print("OK");
|
||||
} else if (req.getPathInfo().equals("/read")) {
|
||||
String[] params = req.getQueryString().split("&");
|
||||
String key = null;
|
||||
for (String param : params) {
|
||||
String[] line = param.split("=");
|
||||
String keyParam = line[0];
|
||||
if ("key".equals(keyParam)) {
|
||||
key = line[1];
|
||||
}
|
||||
}
|
||||
|
||||
Object attr = session.getAttribute(key);
|
||||
resp.getWriter().print(attr);
|
||||
} else if (req.getPathInfo().equals("/remove")) {
|
||||
String[] params = req.getQueryString().split("&");
|
||||
String key = null;
|
||||
for (String param : params) {
|
||||
String[] line = param.split("=");
|
||||
String keyParam = line[0];
|
||||
if ("key".equals(keyParam)) {
|
||||
key = line[1];
|
||||
}
|
||||
}
|
||||
|
||||
session.removeAttribute(key);
|
||||
resp.getWriter().print(String.valueOf(session.getAttribute(key)));
|
||||
} else if (req.getPathInfo().equals("/invalidate")) {
|
||||
session.invalidate();
|
||||
|
||||
resp.getWriter().print("OK");
|
||||
} else if (req.getPathInfo().equals("/recreate")) {
|
||||
session.invalidate();
|
||||
|
||||
session = req.getSession();
|
||||
|
||||
String[] params = req.getQueryString().split("&");
|
||||
String key = null;
|
||||
String value = null;
|
||||
for (String param : params) {
|
||||
String[] paramLine = param.split("=");
|
||||
String keyParam = paramLine[0];
|
||||
String valueParam = paramLine[1];
|
||||
|
||||
if ("key".equals(keyParam)) {
|
||||
key = valueParam;
|
||||
}
|
||||
if ("value".equals(keyParam)) {
|
||||
value = valueParam;
|
||||
}
|
||||
}
|
||||
session.setAttribute(key, value);
|
||||
|
||||
resp.getWriter().print("OK");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import org.apache.catalina.Engine;
|
||||
import org.apache.catalina.Host;
|
||||
import org.apache.catalina.LifecycleException;
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.apache.catalina.core.StandardContext;
|
||||
import org.apache.catalina.startup.Embedded;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class TomcatServer {
|
||||
|
||||
private Embedded server;
|
||||
private int port;
|
||||
private boolean isRunning;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TomcatServer.class);
|
||||
private static final boolean isInfo = LOG.isInfoEnabled();
|
||||
|
||||
|
||||
/**
|
||||
* Create a new Tomcat embedded server instance. Setup looks like:
|
||||
* <pre><Server>
|
||||
* <Service>
|
||||
* <Connector />
|
||||
* <Engine>
|
||||
* <Host>
|
||||
* <Context />
|
||||
* </Host>
|
||||
* </Engine>
|
||||
* </Service>
|
||||
*</Server></pre>
|
||||
* <Server> & <Service> will be created automcatically. We need to hook the remaining to an {@link Embedded} instnace
|
||||
* @param contextPath Context path for the application
|
||||
* @param port Port number to be used for the embedded Tomcat server
|
||||
* @param appBase Path to the Application files (for Maven based web apps, in general: <code>/src/main/</code>)
|
||||
* @param shutdownHook If true, registers a server' shutdown hook with JVM. This is useful to shutdown the server
|
||||
* in erroneous cases.
|
||||
* @throws Exception
|
||||
*/
|
||||
public TomcatServer(String contextPath, int port, String appBase) {
|
||||
if(contextPath == null || appBase == null || appBase.length() == 0) {
|
||||
throw new IllegalArgumentException("Context path or appbase should not be null");
|
||||
}
|
||||
if(!contextPath.startsWith("/")) {
|
||||
contextPath = "/" + contextPath;
|
||||
}
|
||||
|
||||
this.port = port;
|
||||
|
||||
server = new Embedded();
|
||||
server.setName("TomcatEmbeddedServer");
|
||||
|
||||
Host localHost = server.createHost("localhost", appBase);
|
||||
localHost.setAutoDeploy(false);
|
||||
|
||||
StandardContext rootContext = (StandardContext) server.createContext(contextPath, "webapp");
|
||||
rootContext.setDefaultWebXml("web.xml");
|
||||
localHost.addChild(rootContext);
|
||||
|
||||
Engine engine = server.createEngine();
|
||||
engine.setDefaultHost(localHost.getName());
|
||||
engine.setName("TomcatEngine");
|
||||
engine.addChild(localHost);
|
||||
|
||||
server.addEngine(engine);
|
||||
|
||||
Connector connector = server.createConnector(localHost.getName(), port, false);
|
||||
server.addConnector(connector);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the tomcat embedded server
|
||||
*/
|
||||
public void start() throws LifecycleException {
|
||||
if(isRunning) {
|
||||
LOG.warn("Tomcat server is already running @ port={}; ignoring the start", port);
|
||||
return;
|
||||
}
|
||||
|
||||
if(isInfo) LOG.info("Starting the Tomcat server @ port={}", port);
|
||||
|
||||
server.setAwait(true);
|
||||
server.start();
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the tomcat embedded server
|
||||
*/
|
||||
public void stop() throws LifecycleException {
|
||||
if(!isRunning) {
|
||||
LOG.warn("Tomcat server is not running @ port={}", port);
|
||||
return;
|
||||
}
|
||||
|
||||
if(isInfo) LOG.info("Stopping the Tomcat server");
|
||||
|
||||
server.stop();
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Context>
|
||||
|
||||
<Manager className="org.redisson.tomcat.RedissonSessionManager"
|
||||
configPath="${catalina.base}/src/test/webapp/WEB-INF/redisson.yaml" />
|
||||
|
||||
</Context>
|
@ -0,0 +1,2 @@
|
||||
singleServerConfig:
|
||||
address: "redis://127.0.0.1:6379"
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
|
||||
version="2.5">
|
||||
|
||||
<servlet>
|
||||
<servlet-name>testServlet</servlet-name>
|
||||
<servlet-class>org.redisson.tomcat.TestServlet</servlet-class>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>testServlet</servlet-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<session-config>
|
||||
<session-timeout>30</session-timeout>
|
||||
</session-config>
|
||||
|
||||
</web-app>
|
@ -0,0 +1,82 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-tomcat</artifactId>
|
||||
<version>2.8.1-SNAPSHOT</version>
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>redisson-tomcat-7</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Redisson/Tomcat-7</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-core</artifactId>
|
||||
<version>7.0.73</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-logging-juli</artifactId>
|
||||
<version>7.0.73</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-jasper</artifactId>
|
||||
<version>7.0.73</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>tomcat-jasper</artifactId>
|
||||
<version>7.0.73</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.mycila</groupId>
|
||||
<artifactId>license-maven-plugin</artifactId>
|
||||
<version>3.0</version>
|
||||
<configuration>
|
||||
<basedir>${basedir}</basedir>
|
||||
<header>${basedir}/../../header.txt</header>
|
||||
<quiet>false</quiet>
|
||||
<failIfMissing>true</failIfMissing>
|
||||
<aggregate>false</aggregate>
|
||||
<includes>
|
||||
<include>src/main/java/org/redisson/</include>
|
||||
</includes>
|
||||
<excludes>
|
||||
<exclude>target/**</exclude>
|
||||
</excludes>
|
||||
<useDefaultExcludes>true</useDefaultExcludes>
|
||||
<mapping>
|
||||
<java>JAVADOC_STYLE</java>
|
||||
</mapping>
|
||||
<strictCheck>true</strictCheck>
|
||||
<useDefaultMapping>true</useDefaultMapping>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.catalina.session.StandardSession;
|
||||
import org.redisson.api.RMap;
|
||||
|
||||
/**
|
||||
* Redisson Session object for Apache Tomcat
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public class RedissonSession extends StandardSession {
|
||||
|
||||
private final RedissonSessionManager redissonManager;
|
||||
private final Map<String, Object> attrs;
|
||||
private RMap<String, Object> map;
|
||||
|
||||
public RedissonSession(RedissonSessionManager manager) {
|
||||
super(manager);
|
||||
this.redissonManager = manager;
|
||||
try {
|
||||
Field attr = StandardSession.class.getDeclaredField("attributes");
|
||||
attrs = (Map<String, Object>) attr.get(this);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = -2518607181636076487L;
|
||||
|
||||
@Override
|
||||
public void setId(String id, boolean notify) {
|
||||
super.setId(id, notify);
|
||||
map = redissonManager.getMap(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCreationTime(long time) {
|
||||
super.setCreationTime(time);
|
||||
|
||||
if (map != null) {
|
||||
Map<String, Object> newMap = new HashMap<String, Object>(3);
|
||||
newMap.put("session:creationTime", creationTime);
|
||||
newMap.put("session:lastAccessedTime", lastAccessedTime);
|
||||
newMap.put("session:thisAccessedTime", thisAccessedTime);
|
||||
map.putAll(newMap);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void access() {
|
||||
super.access();
|
||||
|
||||
if (map != null) {
|
||||
Map<String, Object> newMap = new HashMap<String, Object>(2);
|
||||
newMap.put("session:lastAccessedTime", lastAccessedTime);
|
||||
newMap.put("session:thisAccessedTime", thisAccessedTime);
|
||||
map.putAll(newMap);
|
||||
if (getMaxInactiveInterval() >= 0) {
|
||||
map.expire(getMaxInactiveInterval(), TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(int interval) {
|
||||
super.setMaxInactiveInterval(interval);
|
||||
|
||||
if (map != null) {
|
||||
map.fastPut("session:maxInactiveInterval", maxInactiveInterval);
|
||||
if (maxInactiveInterval >= 0) {
|
||||
map.expire(getMaxInactiveInterval(), TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValid(boolean isValid) {
|
||||
super.setValid(isValid);
|
||||
|
||||
if (map != null) {
|
||||
map.fastPut("session:isValid", isValid);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNew(boolean isNew) {
|
||||
super.setNew(isNew);
|
||||
|
||||
if (map != null) {
|
||||
map.fastPut("session:isNew", isNew);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAccess() {
|
||||
boolean oldValue = isNew;
|
||||
super.endAccess();
|
||||
|
||||
if (isNew != oldValue) {
|
||||
map.fastPut("session:isNew", isNew);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, Object value, boolean notify) {
|
||||
super.setAttribute(name, value, notify);
|
||||
|
||||
if (map != null && value != null) {
|
||||
map.fastPut(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeAttributeInternal(String name, boolean notify) {
|
||||
super.removeAttributeInternal(name, notify);
|
||||
|
||||
if (map != null) {
|
||||
map.fastRemove(name);
|
||||
}
|
||||
}
|
||||
|
||||
public void save() {
|
||||
Map<String, Object> newMap = new HashMap<String, Object>();
|
||||
newMap.put("session:creationTime", creationTime);
|
||||
newMap.put("session:lastAccessedTime", lastAccessedTime);
|
||||
newMap.put("session:thisAccessedTime", thisAccessedTime);
|
||||
newMap.put("session:maxInactiveInterval", maxInactiveInterval);
|
||||
newMap.put("session:isValid", isValid);
|
||||
newMap.put("session:isNew", isNew);
|
||||
|
||||
for (Entry<String, Object> entry : attrs.entrySet()) {
|
||||
newMap.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
map.putAll(newMap);
|
||||
|
||||
if (maxInactiveInterval >= 0) {
|
||||
map.expire(getMaxInactiveInterval(), TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
public void load() {
|
||||
Set<Entry<String, Object>> entrySet = map.readAllEntrySet();
|
||||
for (Entry<String, Object> entry : entrySet) {
|
||||
if ("session:creationTime".equals(entry.getKey())) {
|
||||
creationTime = (Long) entry.getValue();
|
||||
} else if ("session:lastAccessedTime".equals(entry.getKey())) {
|
||||
lastAccessedTime = (Long) entry.getValue();
|
||||
} else if ("session:thisAccessedTime".equals(entry.getKey())) {
|
||||
thisAccessedTime = (Long) entry.getValue();
|
||||
} else if ("session:maxInactiveInterval".equals(entry.getKey())) {
|
||||
maxInactiveInterval = (Integer) entry.getValue();
|
||||
} else if ("session:isValid".equals(entry.getKey())) {
|
||||
isValid = (Boolean) entry.getValue();
|
||||
} else if ("session:isNew".equals(entry.getKey())) {
|
||||
isNew = (Boolean) entry.getValue();
|
||||
} else {
|
||||
setAttribute(entry.getKey(), entry.getValue(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.Lifecycle;
|
||||
import org.apache.catalina.LifecycleException;
|
||||
import org.apache.catalina.LifecycleListener;
|
||||
import org.apache.catalina.LifecycleState;
|
||||
import org.apache.catalina.Session;
|
||||
import org.apache.catalina.session.ManagerBase;
|
||||
import org.apache.catalina.util.LifecycleSupport;
|
||||
import org.apache.juli.logging.Log;
|
||||
import org.apache.juli.logging.LogFactory;
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RMap;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.config.Config;
|
||||
|
||||
/**
|
||||
* Redisson Session Manager for Apache Tomcat
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public class RedissonSessionManager extends ManagerBase implements Lifecycle {
|
||||
|
||||
private final Log log = LogFactory.getLog(RedissonSessionManager.class);
|
||||
|
||||
protected LifecycleSupport lifecycle = new LifecycleSupport(this);
|
||||
|
||||
private RedissonClient redisson;
|
||||
private String configPath;
|
||||
|
||||
public void setConfigPath(String configPath) {
|
||||
this.configPath = configPath;
|
||||
}
|
||||
|
||||
public String getConfigPath() {
|
||||
return configPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return RedissonSessionManager.class.getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRejectedSessions() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() throws ClassNotFoundException, IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.addLifecycleListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifecycleListener[] findLifecycleListeners() {
|
||||
return lifecycle.findLifecycleListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.removeLifecycleListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session createSession(String sessionId) {
|
||||
RedissonSession session = (RedissonSession) createEmptySession();
|
||||
|
||||
session.setNew(true);
|
||||
session.setValid(true);
|
||||
session.setCreationTime(System.currentTimeMillis());
|
||||
session.setMaxInactiveInterval(((Context) getContainer()).getSessionTimeout() * 60);
|
||||
|
||||
if (sessionId == null) {
|
||||
sessionId = generateSessionId();
|
||||
}
|
||||
|
||||
session.setId(sessionId);
|
||||
session.save();
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
public RMap<String, Object> getMap(String sessionId) {
|
||||
return redisson.getMap("redisson_tomcat_session:" + sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session findSession(String id) throws IOException {
|
||||
Session result = super.findSession(id);
|
||||
if (result == null && id != null) {
|
||||
RedissonSession session = (RedissonSession) createEmptySession();
|
||||
session.setId(id);
|
||||
session.load();
|
||||
return session;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session createEmptySession() {
|
||||
return new RedissonSession(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Session session) {
|
||||
super.remove(session);
|
||||
|
||||
getMap(session.getId()).delete();
|
||||
}
|
||||
|
||||
public RedissonClient getRedisson() {
|
||||
return redisson;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startInternal() throws LifecycleException {
|
||||
super.startInternal();
|
||||
Config config = null;
|
||||
try {
|
||||
config = Config.fromJSON(new File(configPath));
|
||||
} catch (IOException e) {
|
||||
// trying next format
|
||||
try {
|
||||
config = Config.fromYAML(new File(configPath));
|
||||
} catch (IOException e1) {
|
||||
log.error("Can't parse json config " + configPath, e);
|
||||
throw new LifecycleException("Can't parse yaml config " + configPath, e1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
redisson = Redisson.create(config);
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException(e);
|
||||
}
|
||||
|
||||
setState(LifecycleState.STARTING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopInternal() throws LifecycleException {
|
||||
super.stopInternal();
|
||||
|
||||
setState(LifecycleState.STOPPING);
|
||||
|
||||
try {
|
||||
if (redisson != null) {
|
||||
redisson.shutdown();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.fluent.Executor;
|
||||
import org.apache.http.client.fluent.Request;
|
||||
import org.apache.http.cookie.Cookie;
|
||||
import org.apache.http.impl.client.BasicCookieStore;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class RedissonSessionManagerTest {
|
||||
|
||||
@Test
|
||||
public void testSwitchServer() throws Exception {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
BasicCookieStore cookieStore = new BasicCookieStore();
|
||||
executor.use(cookieStore);
|
||||
|
||||
write(executor, "test", "1234");
|
||||
Cookie cookie = cookieStore.getCookies().get(0);
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
|
||||
server = new TomcatServer("myapp", 8080, "src/test/");
|
||||
server.start();
|
||||
|
||||
executor = Executor.newInstance();
|
||||
cookieStore = new BasicCookieStore();
|
||||
cookieStore.addCookie(cookie);
|
||||
executor.use(cookieStore);
|
||||
read(executor, "test", "1234");
|
||||
remove(executor, "test", "null");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testWriteReadRemove() throws Exception {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
|
||||
write(executor, "test", "1234");
|
||||
read(executor, "test", "1234");
|
||||
remove(executor, "test", "null");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecreate() throws Exception {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
|
||||
write(executor, "test", "1");
|
||||
recreate(executor, "test", "2");
|
||||
read(executor, "test", "2");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdate() throws Exception {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
|
||||
write(executor, "test", "1");
|
||||
read(executor, "test", "1");
|
||||
write(executor, "test", "2");
|
||||
read(executor, "test", "2");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testInvalidate() throws Exception {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
BasicCookieStore cookieStore = new BasicCookieStore();
|
||||
executor.use(cookieStore);
|
||||
|
||||
write(executor, "test", "1234");
|
||||
Cookie cookie = cookieStore.getCookies().get(0);
|
||||
invalidate(executor);
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
|
||||
executor = Executor.newInstance();
|
||||
cookieStore = new BasicCookieStore();
|
||||
cookieStore.addCookie(cookie);
|
||||
executor.use(cookieStore);
|
||||
read(executor, "test", "null");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
private void write(Executor executor, String key, String value) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/write?key=" + key + "&value=" + value;
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals("OK", response);
|
||||
}
|
||||
|
||||
private void read(Executor executor, String key, String value) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/read?key=" + key;
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals(value, response);
|
||||
}
|
||||
|
||||
private void remove(Executor executor, String key, String value) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/remove?key=" + key;
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals(value, response);
|
||||
}
|
||||
|
||||
private void invalidate(Executor executor) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/invalidate";
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals("OK", response);
|
||||
}
|
||||
|
||||
private void recreate(Executor executor, String key, String value) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/recreate?key=" + key + "&value=" + value;
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals("OK", response);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
public class TestServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 1243830648280853203L;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
HttpSession session = req.getSession();
|
||||
|
||||
if (req.getPathInfo().equals("/write")) {
|
||||
String[] params = req.getQueryString().split("&");
|
||||
String key = null;
|
||||
String value = null;
|
||||
for (String param : params) {
|
||||
String[] paramLine = param.split("=");
|
||||
String keyParam = paramLine[0];
|
||||
String valueParam = paramLine[1];
|
||||
|
||||
if ("key".equals(keyParam)) {
|
||||
key = valueParam;
|
||||
}
|
||||
if ("value".equals(keyParam)) {
|
||||
value = valueParam;
|
||||
}
|
||||
}
|
||||
session.setAttribute(key, value);
|
||||
|
||||
resp.getWriter().print("OK");
|
||||
} else if (req.getPathInfo().equals("/read")) {
|
||||
String[] params = req.getQueryString().split("&");
|
||||
String key = null;
|
||||
for (String param : params) {
|
||||
String[] line = param.split("=");
|
||||
String keyParam = line[0];
|
||||
if ("key".equals(keyParam)) {
|
||||
key = line[1];
|
||||
}
|
||||
}
|
||||
|
||||
Object attr = session.getAttribute(key);
|
||||
resp.getWriter().print(attr);
|
||||
} else if (req.getPathInfo().equals("/remove")) {
|
||||
String[] params = req.getQueryString().split("&");
|
||||
String key = null;
|
||||
for (String param : params) {
|
||||
String[] line = param.split("=");
|
||||
String keyParam = line[0];
|
||||
if ("key".equals(keyParam)) {
|
||||
key = line[1];
|
||||
}
|
||||
}
|
||||
|
||||
session.removeAttribute(key);
|
||||
resp.getWriter().print(String.valueOf(session.getAttribute(key)));
|
||||
} else if (req.getPathInfo().equals("/invalidate")) {
|
||||
session.invalidate();
|
||||
|
||||
resp.getWriter().print("OK");
|
||||
} else if (req.getPathInfo().equals("/recreate")) {
|
||||
session.invalidate();
|
||||
|
||||
session = req.getSession();
|
||||
|
||||
String[] params = req.getQueryString().split("&");
|
||||
String key = null;
|
||||
String value = null;
|
||||
for (String param : params) {
|
||||
String[] paramLine = param.split("=");
|
||||
String keyParam = paramLine[0];
|
||||
String valueParam = paramLine[1];
|
||||
|
||||
if ("key".equals(keyParam)) {
|
||||
key = valueParam;
|
||||
}
|
||||
if ("value".equals(keyParam)) {
|
||||
value = valueParam;
|
||||
}
|
||||
}
|
||||
session.setAttribute(key, value);
|
||||
|
||||
resp.getWriter().print("OK");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
import org.apache.catalina.LifecycleException;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class TomcatServer {
|
||||
|
||||
private Tomcat tomcat = new Tomcat();
|
||||
private int port;
|
||||
private boolean isRunning;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TomcatServer.class);
|
||||
private static final boolean isInfo = LOG.isInfoEnabled();
|
||||
|
||||
public TomcatServer(String contextPath, int port, String appBase) throws MalformedURLException, ServletException {
|
||||
if(contextPath == null || appBase == null || appBase.length() == 0) {
|
||||
throw new IllegalArgumentException("Context path or appbase should not be null");
|
||||
}
|
||||
if(!contextPath.startsWith("/")) {
|
||||
contextPath = "/" + contextPath;
|
||||
}
|
||||
|
||||
tomcat.setBaseDir("."); // location where temp dir is created
|
||||
tomcat.setPort(port);
|
||||
tomcat.getHost().setAppBase(".");
|
||||
|
||||
tomcat.addWebapp(contextPath, appBase + "webapp");
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the tomcat embedded server
|
||||
*/
|
||||
public void start() throws LifecycleException {
|
||||
tomcat.start();
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the tomcat embedded server
|
||||
*/
|
||||
public void stop() throws LifecycleException {
|
||||
if(!isRunning) {
|
||||
LOG.warn("Tomcat server is not running @ port={}", port);
|
||||
return;
|
||||
}
|
||||
|
||||
if(isInfo) LOG.info("Stopping the Tomcat server");
|
||||
|
||||
tomcat.stop();
|
||||
tomcat.destroy();
|
||||
tomcat.getServer().await();
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Context>
|
||||
|
||||
<Manager className="org.redisson.tomcat.RedissonSessionManager"
|
||||
configPath="${catalina.base}/src/test/webapp/WEB-INF/redisson.yaml" />
|
||||
|
||||
</Context>
|
@ -0,0 +1,2 @@
|
||||
singleServerConfig:
|
||||
address: "redis://127.0.0.1:6379"
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
|
||||
version="2.5">
|
||||
|
||||
<servlet>
|
||||
<servlet-name>testServlet</servlet-name>
|
||||
<servlet-class>org.redisson.tomcat.TestServlet</servlet-class>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>testServlet</servlet-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<session-config>
|
||||
<session-timeout>30</session-timeout>
|
||||
</session-config>
|
||||
|
||||
</web-app>
|
@ -0,0 +1,81 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-tomcat</artifactId>
|
||||
<version>2.8.1-SNAPSHOT</version>
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>redisson-tomcat-8</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Redisson/Tomcat-8</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-core</artifactId>
|
||||
<version>8.0.39</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-logging-juli</artifactId>
|
||||
<version>8.0.39</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-jasper</artifactId>
|
||||
<version>8.0.39</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>tomcat-jasper</artifactId>
|
||||
<version>8.0.39</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.mycila</groupId>
|
||||
<artifactId>license-maven-plugin</artifactId>
|
||||
<version>3.0</version>
|
||||
<configuration>
|
||||
<basedir>${basedir}</basedir>
|
||||
<header>${basedir}/../../header.txt</header>
|
||||
<quiet>false</quiet>
|
||||
<failIfMissing>true</failIfMissing>
|
||||
<aggregate>false</aggregate>
|
||||
<includes>
|
||||
<include>src/main/java/org/redisson/</include>
|
||||
</includes>
|
||||
<excludes>
|
||||
<exclude>target/**</exclude>
|
||||
</excludes>
|
||||
<useDefaultExcludes>true</useDefaultExcludes>
|
||||
<mapping>
|
||||
<java>JAVADOC_STYLE</java>
|
||||
</mapping>
|
||||
<strictCheck>true</strictCheck>
|
||||
<useDefaultMapping>true</useDefaultMapping>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.catalina.session.StandardSession;
|
||||
import org.redisson.api.RMap;
|
||||
|
||||
/**
|
||||
* Redisson Session object for Apache Tomcat
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public class RedissonSession extends StandardSession {
|
||||
|
||||
private final RedissonSessionManager redissonManager;
|
||||
private final Map<String, Object> attrs;
|
||||
private RMap<String, Object> map;
|
||||
|
||||
public RedissonSession(RedissonSessionManager manager) {
|
||||
super(manager);
|
||||
this.redissonManager = manager;
|
||||
|
||||
try {
|
||||
Field attr = StandardSession.class.getDeclaredField("attributes");
|
||||
attrs = (Map<String, Object>) attr.get(this);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = -2518607181636076487L;
|
||||
|
||||
@Override
|
||||
public void setId(String id, boolean notify) {
|
||||
super.setId(id, notify);
|
||||
map = redissonManager.getMap(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCreationTime(long time) {
|
||||
super.setCreationTime(time);
|
||||
|
||||
if (map != null) {
|
||||
Map<String, Object> newMap = new HashMap<String, Object>(3);
|
||||
newMap.put("session:creationTime", creationTime);
|
||||
newMap.put("session:lastAccessedTime", lastAccessedTime);
|
||||
newMap.put("session:thisAccessedTime", thisAccessedTime);
|
||||
map.putAll(newMap);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void access() {
|
||||
super.access();
|
||||
|
||||
if (map != null) {
|
||||
Map<String, Object> newMap = new HashMap<String, Object>(2);
|
||||
newMap.put("session:lastAccessedTime", lastAccessedTime);
|
||||
newMap.put("session:thisAccessedTime", thisAccessedTime);
|
||||
map.putAll(newMap);
|
||||
if (getMaxInactiveInterval() >= 0) {
|
||||
map.expire(getMaxInactiveInterval(), TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxInactiveInterval(int interval) {
|
||||
super.setMaxInactiveInterval(interval);
|
||||
|
||||
if (map != null) {
|
||||
map.fastPut("session:maxInactiveInterval", maxInactiveInterval);
|
||||
if (maxInactiveInterval >= 0) {
|
||||
map.expire(getMaxInactiveInterval(), TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValid(boolean isValid) {
|
||||
super.setValid(isValid);
|
||||
|
||||
if (map != null) {
|
||||
map.fastPut("session:isValid", isValid);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNew(boolean isNew) {
|
||||
super.setNew(isNew);
|
||||
|
||||
if (map != null) {
|
||||
map.fastPut("session:isNew", isNew);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAccess() {
|
||||
boolean oldValue = isNew;
|
||||
super.endAccess();
|
||||
|
||||
if (isNew != oldValue) {
|
||||
map.fastPut("session:isNew", isNew);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, Object value, boolean notify) {
|
||||
super.setAttribute(name, value, notify);
|
||||
|
||||
if (map != null && value != null) {
|
||||
map.fastPut(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeAttributeInternal(String name, boolean notify) {
|
||||
super.removeAttributeInternal(name, notify);
|
||||
|
||||
if (map != null) {
|
||||
map.fastRemove(name);
|
||||
}
|
||||
}
|
||||
|
||||
public void save() {
|
||||
Map<String, Object> newMap = new HashMap<String, Object>();
|
||||
newMap.put("session:creationTime", creationTime);
|
||||
newMap.put("session:lastAccessedTime", lastAccessedTime);
|
||||
newMap.put("session:thisAccessedTime", thisAccessedTime);
|
||||
newMap.put("session:maxInactiveInterval", maxInactiveInterval);
|
||||
newMap.put("session:isValid", isValid);
|
||||
newMap.put("session:isNew", isNew);
|
||||
|
||||
for (Entry<String, Object> entry : attrs.entrySet()) {
|
||||
newMap.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
map.putAll(newMap);
|
||||
|
||||
if (maxInactiveInterval >= 0) {
|
||||
map.expire(getMaxInactiveInterval(), TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
public void load() {
|
||||
Set<Entry<String, Object>> entrySet = map.readAllEntrySet();
|
||||
for (Entry<String, Object> entry : entrySet) {
|
||||
if ("session:creationTime".equals(entry.getKey())) {
|
||||
creationTime = (Long) entry.getValue();
|
||||
} else if ("session:lastAccessedTime".equals(entry.getKey())) {
|
||||
lastAccessedTime = (Long) entry.getValue();
|
||||
} else if ("session:thisAccessedTime".equals(entry.getKey())) {
|
||||
thisAccessedTime = (Long) entry.getValue();
|
||||
} else if ("session:maxInactiveInterval".equals(entry.getKey())) {
|
||||
maxInactiveInterval = (Integer) entry.getValue();
|
||||
} else if ("session:isValid".equals(entry.getKey())) {
|
||||
isValid = (Boolean) entry.getValue();
|
||||
} else if ("session:isNew".equals(entry.getKey())) {
|
||||
isNew = (Boolean) entry.getValue();
|
||||
} else {
|
||||
setAttribute(entry.getKey(), entry.getValue(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.Lifecycle;
|
||||
import org.apache.catalina.LifecycleException;
|
||||
import org.apache.catalina.LifecycleListener;
|
||||
import org.apache.catalina.LifecycleState;
|
||||
import org.apache.catalina.Session;
|
||||
import org.apache.catalina.session.ManagerBase;
|
||||
import org.apache.catalina.util.LifecycleSupport;
|
||||
import org.apache.juli.logging.Log;
|
||||
import org.apache.juli.logging.LogFactory;
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RMap;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.config.Config;
|
||||
|
||||
/**
|
||||
* Redisson Session Manager for Apache Tomcat
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public class RedissonSessionManager extends ManagerBase implements Lifecycle {
|
||||
|
||||
private final Log log = LogFactory.getLog(RedissonSessionManager.class);
|
||||
|
||||
protected LifecycleSupport lifecycle = new LifecycleSupport(this);
|
||||
|
||||
private RedissonClient redisson;
|
||||
private String configPath;
|
||||
|
||||
public void setConfigPath(String configPath) {
|
||||
this.configPath = configPath;
|
||||
}
|
||||
|
||||
public String getConfigPath() {
|
||||
return configPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return RedissonSessionManager.class.getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRejectedSessions() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() throws ClassNotFoundException, IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.addLifecycleListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifecycleListener[] findLifecycleListeners() {
|
||||
return lifecycle.findLifecycleListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.removeLifecycleListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session createSession(String sessionId) {
|
||||
RedissonSession session = (RedissonSession) createEmptySession();
|
||||
|
||||
session.setNew(true);
|
||||
session.setValid(true);
|
||||
session.setCreationTime(System.currentTimeMillis());
|
||||
session.setMaxInactiveInterval(((Context) getContainer()).getSessionTimeout() * 60);
|
||||
|
||||
if (sessionId == null) {
|
||||
sessionId = generateSessionId();
|
||||
}
|
||||
|
||||
session.setId(sessionId);
|
||||
session.save();
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
public RMap<String, Object> getMap(String sessionId) {
|
||||
return redisson.getMap("redisson_tomcat_session:" + sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session findSession(String id) throws IOException {
|
||||
Session result = super.findSession(id);
|
||||
if (result == null && id != null) {
|
||||
RedissonSession session = (RedissonSession) createEmptySession();
|
||||
session.setId(id);
|
||||
session.load();
|
||||
return session;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Session createEmptySession() {
|
||||
return new RedissonSession(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Session session) {
|
||||
super.remove(session);
|
||||
|
||||
getMap(session.getId()).delete();
|
||||
}
|
||||
|
||||
public RedissonClient getRedisson() {
|
||||
return redisson;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startInternal() throws LifecycleException {
|
||||
super.startInternal();
|
||||
Config config = null;
|
||||
try {
|
||||
config = Config.fromJSON(new File(configPath));
|
||||
} catch (IOException e) {
|
||||
// trying next format
|
||||
try {
|
||||
config = Config.fromYAML(new File(configPath));
|
||||
} catch (IOException e1) {
|
||||
log.error("Can't parse json config " + configPath, e);
|
||||
throw new LifecycleException("Can't parse yaml config " + configPath, e1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
redisson = Redisson.create(config);
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException(e);
|
||||
}
|
||||
|
||||
setState(LifecycleState.STARTING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopInternal() throws LifecycleException {
|
||||
super.stopInternal();
|
||||
|
||||
setState(LifecycleState.STOPPING);
|
||||
|
||||
try {
|
||||
if (redisson != null) {
|
||||
redisson.shutdown();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.fluent.Executor;
|
||||
import org.apache.http.client.fluent.Request;
|
||||
import org.apache.http.cookie.Cookie;
|
||||
import org.apache.http.impl.client.BasicCookieStore;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class RedissonSessionManagerTest {
|
||||
|
||||
@Test
|
||||
public void testSwitchServer() throws Exception {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
BasicCookieStore cookieStore = new BasicCookieStore();
|
||||
executor.use(cookieStore);
|
||||
|
||||
write(executor, "test", "1234");
|
||||
Cookie cookie = cookieStore.getCookies().get(0);
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
|
||||
server = new TomcatServer("myapp", 8080, "src/test/");
|
||||
server.start();
|
||||
|
||||
executor = Executor.newInstance();
|
||||
cookieStore = new BasicCookieStore();
|
||||
cookieStore.addCookie(cookie);
|
||||
executor.use(cookieStore);
|
||||
read(executor, "test", "1234");
|
||||
remove(executor, "test", "null");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testWriteReadRemove() throws Exception {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
|
||||
write(executor, "test", "1234");
|
||||
read(executor, "test", "1234");
|
||||
remove(executor, "test", "null");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecreate() throws Exception {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
|
||||
write(executor, "test", "1");
|
||||
recreate(executor, "test", "2");
|
||||
read(executor, "test", "2");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdate() throws Exception {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
|
||||
write(executor, "test", "1");
|
||||
read(executor, "test", "1");
|
||||
write(executor, "test", "2");
|
||||
read(executor, "test", "2");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testInvalidate() throws Exception {
|
||||
// start the server at http://localhost:8080/myapp
|
||||
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
|
||||
server.start();
|
||||
|
||||
Executor executor = Executor.newInstance();
|
||||
BasicCookieStore cookieStore = new BasicCookieStore();
|
||||
executor.use(cookieStore);
|
||||
|
||||
write(executor, "test", "1234");
|
||||
Cookie cookie = cookieStore.getCookies().get(0);
|
||||
invalidate(executor);
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
|
||||
executor = Executor.newInstance();
|
||||
cookieStore = new BasicCookieStore();
|
||||
cookieStore.addCookie(cookie);
|
||||
executor.use(cookieStore);
|
||||
read(executor, "test", "null");
|
||||
|
||||
Executor.closeIdleConnections();
|
||||
server.stop();
|
||||
}
|
||||
|
||||
private void write(Executor executor, String key, String value) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/write?key=" + key + "&value=" + value;
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals("OK", response);
|
||||
}
|
||||
|
||||
private void read(Executor executor, String key, String value) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/read?key=" + key;
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals(value, response);
|
||||
}
|
||||
|
||||
private void remove(Executor executor, String key, String value) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/remove?key=" + key;
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals(value, response);
|
||||
}
|
||||
|
||||
private void invalidate(Executor executor) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/invalidate";
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals("OK", response);
|
||||
}
|
||||
|
||||
private void recreate(Executor executor, String key, String value) throws IOException, ClientProtocolException {
|
||||
String url = "http://localhost:8080/myapp/recreate?key=" + key + "&value=" + value;
|
||||
String response = executor.execute(Request.Get(url)).returnContent().asString();
|
||||
Assert.assertEquals("OK", response);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
public class TestServlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = 1243830648280853203L;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
HttpSession session = req.getSession();
|
||||
|
||||
if (req.getPathInfo().equals("/write")) {
|
||||
String[] params = req.getQueryString().split("&");
|
||||
String key = null;
|
||||
String value = null;
|
||||
for (String param : params) {
|
||||
String[] paramLine = param.split("=");
|
||||
String keyParam = paramLine[0];
|
||||
String valueParam = paramLine[1];
|
||||
|
||||
if ("key".equals(keyParam)) {
|
||||
key = valueParam;
|
||||
}
|
||||
if ("value".equals(keyParam)) {
|
||||
value = valueParam;
|
||||
}
|
||||
}
|
||||
session.setAttribute(key, value);
|
||||
|
||||
resp.getWriter().print("OK");
|
||||
} else if (req.getPathInfo().equals("/read")) {
|
||||
String[] params = req.getQueryString().split("&");
|
||||
String key = null;
|
||||
for (String param : params) {
|
||||
String[] line = param.split("=");
|
||||
String keyParam = line[0];
|
||||
if ("key".equals(keyParam)) {
|
||||
key = line[1];
|
||||
}
|
||||
}
|
||||
|
||||
Object attr = session.getAttribute(key);
|
||||
resp.getWriter().print(attr);
|
||||
} else if (req.getPathInfo().equals("/remove")) {
|
||||
String[] params = req.getQueryString().split("&");
|
||||
String key = null;
|
||||
for (String param : params) {
|
||||
String[] line = param.split("=");
|
||||
String keyParam = line[0];
|
||||
if ("key".equals(keyParam)) {
|
||||
key = line[1];
|
||||
}
|
||||
}
|
||||
|
||||
session.removeAttribute(key);
|
||||
resp.getWriter().print(String.valueOf(session.getAttribute(key)));
|
||||
} else if (req.getPathInfo().equals("/invalidate")) {
|
||||
session.invalidate();
|
||||
|
||||
resp.getWriter().print("OK");
|
||||
} else if (req.getPathInfo().equals("/recreate")) {
|
||||
session.invalidate();
|
||||
|
||||
session = req.getSession();
|
||||
|
||||
String[] params = req.getQueryString().split("&");
|
||||
String key = null;
|
||||
String value = null;
|
||||
for (String param : params) {
|
||||
String[] paramLine = param.split("=");
|
||||
String keyParam = paramLine[0];
|
||||
String valueParam = paramLine[1];
|
||||
|
||||
if ("key".equals(keyParam)) {
|
||||
key = valueParam;
|
||||
}
|
||||
if ("value".equals(keyParam)) {
|
||||
value = valueParam;
|
||||
}
|
||||
}
|
||||
session.setAttribute(key, value);
|
||||
|
||||
resp.getWriter().print("OK");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package org.redisson.tomcat;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
import org.apache.catalina.LifecycleException;
|
||||
import org.apache.catalina.startup.Tomcat;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class TomcatServer {
|
||||
|
||||
private Tomcat tomcat = new Tomcat();
|
||||
private int port;
|
||||
private boolean isRunning;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TomcatServer.class);
|
||||
private static final boolean isInfo = LOG.isInfoEnabled();
|
||||
|
||||
public TomcatServer(String contextPath, int port, String appBase) throws MalformedURLException, ServletException {
|
||||
if(contextPath == null || appBase == null || appBase.length() == 0) {
|
||||
throw new IllegalArgumentException("Context path or appbase should not be null");
|
||||
}
|
||||
if(!contextPath.startsWith("/")) {
|
||||
contextPath = "/" + contextPath;
|
||||
}
|
||||
|
||||
tomcat.setBaseDir("."); // location where temp dir is created
|
||||
tomcat.setPort(port);
|
||||
tomcat.getHost().setAppBase(".");
|
||||
|
||||
tomcat.addWebapp(contextPath, appBase + "webapp");
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the tomcat embedded server
|
||||
*/
|
||||
public void start() throws LifecycleException {
|
||||
tomcat.start();
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the tomcat embedded server
|
||||
*/
|
||||
public void stop() throws LifecycleException {
|
||||
if(!isRunning) {
|
||||
LOG.warn("Tomcat server is not running @ port={}", port);
|
||||
return;
|
||||
}
|
||||
|
||||
if(isInfo) LOG.info("Stopping the Tomcat server");
|
||||
|
||||
tomcat.stop();
|
||||
tomcat.destroy();
|
||||
tomcat.getServer().await();
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Context>
|
||||
|
||||
<Manager className="org.redisson.tomcat.RedissonSessionManager"
|
||||
configPath="${catalina.base}/src/test/webapp/WEB-INF/redisson.yaml" />
|
||||
|
||||
</Context>
|
@ -0,0 +1,2 @@
|
||||
singleServerConfig:
|
||||
address: "redis://127.0.0.1:6379"
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
|
||||
version="2.5">
|
||||
|
||||
<servlet>
|
||||
<servlet-name>testServlet</servlet-name>
|
||||
<servlet-class>org.redisson.tomcat.TestServlet</servlet-class>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>testServlet</servlet-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<session-config>
|
||||
<session-timeout>30</session-timeout>
|
||||
</session-config>
|
||||
|
||||
</web-app>
|
@ -1,13 +0,0 @@
|
||||
Copyright 2016 Nikita Koksharov
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,246 +0,0 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.redisson.api.RFuture;
|
||||
import org.redisson.client.codec.LongCodec;
|
||||
import org.redisson.client.protocol.RedisCommands;
|
||||
import org.redisson.command.CommandAsyncExecutor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.util.concurrent.Future;
|
||||
import io.netty.util.concurrent.FutureListener;
|
||||
import io.netty.util.internal.PlatformDependent;
|
||||
|
||||
/**
|
||||
* Eviction scheduler for RMapCache object.
|
||||
* Deletes expired entries in time interval between 5 seconds to 2 hours.
|
||||
* It analyzes deleted amount of expired keys
|
||||
* and 'tune' next execution delay depending on it.
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public class EvictionScheduler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EvictionScheduler.class);
|
||||
|
||||
public class RedissonCacheTask implements Runnable {
|
||||
|
||||
final String name;
|
||||
final String timeoutSetName;
|
||||
final String maxIdleSetName;
|
||||
final boolean multimap;
|
||||
final Deque<Integer> sizeHistory = new LinkedList<Integer>();
|
||||
int delay = 10;
|
||||
|
||||
final int minDelay = 1;
|
||||
final int maxDelay = 2*60*60;
|
||||
final int keysLimit = 300;
|
||||
|
||||
public RedissonCacheTask(String name, String timeoutSetName, String maxIdleSetName, boolean multimap) {
|
||||
this.name = name;
|
||||
this.timeoutSetName = timeoutSetName;
|
||||
this.maxIdleSetName = maxIdleSetName;
|
||||
this.multimap = multimap;
|
||||
}
|
||||
|
||||
public void schedule() {
|
||||
executor.getConnectionManager().getGroup().schedule(this, delay, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
RFuture<Integer> future = cleanupExpiredEntires(name, timeoutSetName, maxIdleSetName, keysLimit, multimap);
|
||||
|
||||
future.addListener(new FutureListener<Integer>() {
|
||||
@Override
|
||||
public void operationComplete(Future<Integer> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
schedule();
|
||||
return;
|
||||
}
|
||||
|
||||
Integer size = future.getNow();
|
||||
|
||||
if (sizeHistory.size() == 2) {
|
||||
if (sizeHistory.peekFirst() > sizeHistory.peekLast()
|
||||
&& sizeHistory.peekLast() > size) {
|
||||
delay = Math.min(maxDelay, (int)(delay*1.5));
|
||||
}
|
||||
|
||||
// if (sizeHistory.peekFirst() < sizeHistory.peekLast()
|
||||
// && sizeHistory.peekLast() < size) {
|
||||
// prevDelay = Math.max(minDelay, prevDelay/2);
|
||||
// }
|
||||
|
||||
if (sizeHistory.peekFirst().intValue() == sizeHistory.peekLast()
|
||||
&& sizeHistory.peekLast().intValue() == size) {
|
||||
if (size == keysLimit) {
|
||||
delay = Math.max(minDelay, delay/4);
|
||||
}
|
||||
if (size == 0) {
|
||||
delay = Math.min(maxDelay, (int)(delay*1.5));
|
||||
}
|
||||
}
|
||||
|
||||
sizeHistory.pollFirst();
|
||||
}
|
||||
|
||||
sizeHistory.add(size);
|
||||
schedule();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final ConcurrentMap<String, RedissonCacheTask> tasks = PlatformDependent.newConcurrentHashMap();
|
||||
private final CommandAsyncExecutor executor;
|
||||
|
||||
private final ConcurrentMap<String, Long> lastExpiredTime = PlatformDependent.newConcurrentHashMap();
|
||||
private final int expireTaskExecutionDelay = 1000;
|
||||
private final int valuesAmountToClean = 500;
|
||||
|
||||
public EvictionScheduler(CommandAsyncExecutor executor) {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
public void scheduleCleanMultimap(String name, String timeoutSetName) {
|
||||
RedissonCacheTask task = new RedissonCacheTask(name, timeoutSetName, null, true);
|
||||
RedissonCacheTask prevTask = tasks.putIfAbsent(name, task);
|
||||
if (prevTask == null) {
|
||||
task.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
public void schedule(String name, String timeoutSetName) {
|
||||
RedissonCacheTask task = new RedissonCacheTask(name, timeoutSetName, null, false);
|
||||
RedissonCacheTask prevTask = tasks.putIfAbsent(name, task);
|
||||
if (prevTask == null) {
|
||||
task.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
public void schedule(String name) {
|
||||
schedule(name, null);
|
||||
}
|
||||
|
||||
public void schedule(String name, String timeoutSetName, String maxIdleSetName) {
|
||||
RedissonCacheTask task = new RedissonCacheTask(name, timeoutSetName, maxIdleSetName, false);
|
||||
RedissonCacheTask prevTask = tasks.putIfAbsent(name, task);
|
||||
if (prevTask == null) {
|
||||
task.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
public void runCleanTask(final String name, String timeoutSetName, long currentDate) {
|
||||
|
||||
final Long lastExpired = lastExpiredTime.get(name);
|
||||
long now = System.currentTimeMillis();
|
||||
if (lastExpired == null) {
|
||||
if (lastExpiredTime.putIfAbsent(name, now) != null) {
|
||||
return;
|
||||
}
|
||||
} else if (lastExpired + expireTaskExecutionDelay >= now) {
|
||||
if (!lastExpiredTime.replace(name, lastExpired, now)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
RFuture<Integer> future = cleanupExpiredEntires(name, timeoutSetName, null, valuesAmountToClean, false);
|
||||
|
||||
future.addListener(new FutureListener<Integer>() {
|
||||
@Override
|
||||
public void operationComplete(Future<Integer> future) throws Exception {
|
||||
executor.getConnectionManager().getGroup().schedule(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
lastExpiredTime.remove(name, lastExpired);
|
||||
}
|
||||
}, expireTaskExecutionDelay*3, TimeUnit.SECONDS);
|
||||
|
||||
if (!future.isSuccess()) {
|
||||
log.warn("Can't execute clean task for expired values. RSetCache name: " + name, future.cause());
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private RFuture<Integer> cleanupExpiredEntires(String name, String timeoutSetName, String maxIdleSetName, int keysLimit, boolean multimap) {
|
||||
if (multimap) {
|
||||
return executor.evalWriteAsync(name, LongCodec.INSTANCE, RedisCommands.EVAL_INTEGER,
|
||||
"local expiredKeys = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); "
|
||||
+ "if #expiredKeys > 0 then "
|
||||
+ "redis.call('zrem', KEYS[2], unpack(expiredKeys)); "
|
||||
|
||||
+ "local values = redis.call('hmget', KEYS[1], unpack(expiredKeys)); "
|
||||
+ "local keys = {}; "
|
||||
+ "for i, v in ipairs(values) do "
|
||||
+ "local name = '{' .. KEYS[1] .. '}:' .. v; "
|
||||
+ "table.insert(keys, name); "
|
||||
+ "end; "
|
||||
+ "redis.call('del', unpack(keys)); "
|
||||
|
||||
+ "redis.call('hdel', KEYS[1], unpack(expiredKeys)); "
|
||||
+ "end; "
|
||||
+ "return #expiredKeys;",
|
||||
Arrays.<Object>asList(name, timeoutSetName), System.currentTimeMillis(), keysLimit);
|
||||
}
|
||||
|
||||
if (maxIdleSetName != null) {
|
||||
return executor.evalWriteAsync(name, LongCodec.INSTANCE, RedisCommands.EVAL_INTEGER,
|
||||
"local expiredKeys1 = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); "
|
||||
+ "if #expiredKeys1 > 0 then "
|
||||
+ "redis.call('zrem', KEYS[3], unpack(expiredKeys1)); "
|
||||
+ "redis.call('zrem', KEYS[2], unpack(expiredKeys1)); "
|
||||
+ "redis.call('hdel', KEYS[1], unpack(expiredKeys1)); "
|
||||
+ "end; "
|
||||
+ "local expiredKeys2 = redis.call('zrangebyscore', KEYS[3], 0, ARGV[1], 'limit', 0, ARGV[2]); "
|
||||
+ "if #expiredKeys2 > 0 then "
|
||||
+ "redis.call('zrem', KEYS[3], unpack(expiredKeys2)); "
|
||||
+ "redis.call('zrem', KEYS[2], unpack(expiredKeys2)); "
|
||||
+ "redis.call('hdel', KEYS[1], unpack(expiredKeys2)); "
|
||||
+ "end; "
|
||||
+ "return #expiredKeys1 + #expiredKeys2;",
|
||||
Arrays.<Object>asList(name, timeoutSetName, maxIdleSetName), System.currentTimeMillis(), keysLimit);
|
||||
}
|
||||
|
||||
if (timeoutSetName == null) {
|
||||
return executor.writeAsync(name, LongCodec.INSTANCE, RedisCommands.ZREMRANGEBYSCORE, name, 0, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
return executor.evalWriteAsync(name, LongCodec.INSTANCE, RedisCommands.EVAL_INTEGER,
|
||||
"local expiredKeys = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); "
|
||||
+ "if #expiredKeys > 0 then "
|
||||
+ "redis.call('zrem', KEYS[2], unpack(expiredKeys)); "
|
||||
+ "redis.call('hdel', KEYS[1], unpack(expiredKeys)); "
|
||||
+ "end; "
|
||||
+ "return #expiredKeys;",
|
||||
Arrays.<Object>asList(name, timeoutSetName), System.currentTimeMillis(), keysLimit);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson;
|
||||
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import io.netty.util.internal.PlatformDependent;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public class QueueTransferService {
|
||||
|
||||
private final ConcurrentMap<String, QueueTransferTask> tasks = PlatformDependent.newConcurrentHashMap();
|
||||
|
||||
public synchronized void schedule(String name, QueueTransferTask task) {
|
||||
QueueTransferTask oldTask = tasks.putIfAbsent(name, task);
|
||||
if (oldTask == null) {
|
||||
task.start();
|
||||
} else {
|
||||
oldTask.incUsage();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void remove(String name) {
|
||||
QueueTransferTask task = tasks.get(name);
|
||||
if (task != null) {
|
||||
if (task.decUsage() == 0) {
|
||||
tasks.remove(name, task);
|
||||
task.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.redisson.api.RFuture;
|
||||
import org.redisson.api.RTopic;
|
||||
import org.redisson.api.listener.BaseStatusListener;
|
||||
import org.redisson.api.listener.MessageListener;
|
||||
import org.redisson.connection.ConnectionManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.util.Timeout;
|
||||
import io.netty.util.TimerTask;
|
||||
import io.netty.util.concurrent.FutureListener;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public abstract class QueueTransferTask {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(QueueTransferTask.class);
|
||||
|
||||
private int usage = 1;
|
||||
private final AtomicReference<Timeout> timeoutReference = new AtomicReference<Timeout>();
|
||||
private final ConnectionManager connectionManager;
|
||||
|
||||
public QueueTransferTask(ConnectionManager connectionManager) {
|
||||
super();
|
||||
this.connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
public void incUsage() {
|
||||
usage++;
|
||||
}
|
||||
|
||||
public int decUsage() {
|
||||
usage--;
|
||||
return usage;
|
||||
}
|
||||
|
||||
private int messageListenerId;
|
||||
private int statusListenerId;
|
||||
|
||||
public void start() {
|
||||
RTopic<Long> schedulerTopic = getTopic();
|
||||
statusListenerId = schedulerTopic.addListener(new BaseStatusListener() {
|
||||
@Override
|
||||
public void onSubscribe(String channel) {
|
||||
pushTask();
|
||||
}
|
||||
});
|
||||
|
||||
messageListenerId = schedulerTopic.addListener(new MessageListener<Long>() {
|
||||
@Override
|
||||
public void onMessage(String channel, Long startTime) {
|
||||
scheduleTask(startTime);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
RTopic<Long> schedulerTopic = getTopic();
|
||||
schedulerTopic.removeListener(messageListenerId);
|
||||
schedulerTopic.removeListener(statusListenerId);
|
||||
}
|
||||
|
||||
private void scheduleTask(final Long startTime) {
|
||||
if (startTime == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Timeout oldTimeout = timeoutReference.get();
|
||||
if (oldTimeout != null) {
|
||||
oldTimeout.cancel();
|
||||
timeoutReference.compareAndSet(oldTimeout, null);
|
||||
}
|
||||
|
||||
long delay = startTime - System.currentTimeMillis();
|
||||
if (delay > 10) {
|
||||
Timeout timeout = connectionManager.newTimeout(new TimerTask() {
|
||||
@Override
|
||||
public void run(Timeout timeout) throws Exception {
|
||||
pushTask();
|
||||
}
|
||||
}, delay, TimeUnit.MILLISECONDS);
|
||||
timeoutReference.set(timeout);
|
||||
} else {
|
||||
pushTask();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract RTopic<Long> getTopic();
|
||||
|
||||
protected abstract RFuture<Long> pushTaskAsync();
|
||||
|
||||
private void pushTask() {
|
||||
RFuture<Long> startTimeFuture = pushTaskAsync();
|
||||
addListener(startTimeFuture);
|
||||
}
|
||||
|
||||
private void addListener(RFuture<Long> startTimeFuture) {
|
||||
startTimeFuture.addListener(new FutureListener<Long>() {
|
||||
@Override
|
||||
public void operationComplete(io.netty.util.concurrent.Future<Long> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
if (future.cause() instanceof RedissonShutdownException) {
|
||||
return;
|
||||
}
|
||||
log.error(future.cause().getMessage(), future.cause());
|
||||
scheduleTask(System.currentTimeMillis() + 5 * 1000L);
|
||||
return;
|
||||
}
|
||||
|
||||
if (future.getNow() != null) {
|
||||
scheduleTask(future.getNow());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,303 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.redisson.api.RBinaryStream;
|
||||
import org.redisson.api.RFuture;
|
||||
import org.redisson.client.codec.ByteArrayCodec;
|
||||
import org.redisson.client.handler.State;
|
||||
import org.redisson.client.protocol.Decoder;
|
||||
import org.redisson.client.protocol.RedisCommand;
|
||||
import org.redisson.client.protocol.RedisCommands;
|
||||
import org.redisson.command.CommandAsyncExecutor;
|
||||
import org.redisson.misc.RPromise;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.util.concurrent.Future;
|
||||
import io.netty.util.concurrent.FutureListener;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public class RedissonBinaryStream extends RedissonBucket<byte[]> implements RBinaryStream {
|
||||
|
||||
class RedissonOutputStream extends OutputStream {
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
writeBytes(new byte[] {(byte)b});
|
||||
}
|
||||
|
||||
private void writeBytes(byte[] bytes) {
|
||||
get(writeAsync(bytes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
byte[] dest;
|
||||
if (b.length == len && off == 0) {
|
||||
dest = b;
|
||||
} else {
|
||||
dest = new byte[len];
|
||||
System.arraycopy(b, off, dest, 0, len);
|
||||
}
|
||||
writeBytes(dest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RedissonInputStream extends InputStream {
|
||||
|
||||
private int index;
|
||||
private int mark;
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long k = size() - index;
|
||||
if (n < k) {
|
||||
k = n;
|
||||
if (n < 0) {
|
||||
k = 0;
|
||||
}
|
||||
}
|
||||
|
||||
index += k;
|
||||
return k;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mark(int readlimit) {
|
||||
mark = index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
index = mark;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return (int)(size() - index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
byte[] b = new byte[1];
|
||||
int len = read(b);
|
||||
if (len == -1) {
|
||||
return -1;
|
||||
}
|
||||
return b[0] & 0xff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(final byte[] b, final int off, final int len) throws IOException {
|
||||
if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (b == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
if (off < 0 || len < 0 || len > b.length - off) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
|
||||
return (Integer)get(commandExecutor.evalReadAsync(getName(), codec, new RedisCommand<Integer>("EVAL", new Decoder<Integer>() {
|
||||
@Override
|
||||
public Integer decode(ByteBuf buf, State state) {
|
||||
if (buf.readableBytes() == 0) {
|
||||
return -1;
|
||||
}
|
||||
int readBytes = Math.min(buf.readableBytes(), len);
|
||||
buf.readBytes(b, off, readBytes);
|
||||
index += readBytes;
|
||||
return readBytes;
|
||||
}
|
||||
}),
|
||||
"local parts = redis.call('get', KEYS[2]); "
|
||||
+ "if parts ~= false then "
|
||||
+ "local startPart = math.floor(tonumber(ARGV[1])/536870912); "
|
||||
+ "local endPart = math.floor(tonumber(ARGV[2])/536870912); "
|
||||
+ "local startPartName = KEYS[1]; "
|
||||
+ "local endPartName = KEYS[1]; "
|
||||
|
||||
+ "if startPart > 0 then "
|
||||
+ "startPartName = KEYS[1] .. ':' .. startPart; "
|
||||
+ "end; "
|
||||
+ "if endPart > 0 then "
|
||||
+ "endPartName = KEYS[1] .. ':' .. endPart; "
|
||||
+ "end; "
|
||||
|
||||
+ "if startPartName ~= endPartName then "
|
||||
+ "local startIndex = tonumber(ARGV[1]) - startPart*536870912; "
|
||||
+ "local endIndex = tonumber(ARGV[2]) - endPart*536870912; "
|
||||
+ "local result = redis.call('getrange', startPartName, startIndex, 536870911); "
|
||||
+ "result = result .. redis.call('getrange', endPartName, 0, endIndex-1); "
|
||||
+ "return result; "
|
||||
+ "end; "
|
||||
|
||||
+ "local startIndex = tonumber(ARGV[1]) - startPart*536870912; "
|
||||
+ "local endIndex = tonumber(ARGV[2]) - endPart*536870912; "
|
||||
+ "return redis.call('getrange', startPartName, startIndex, endIndex);"
|
||||
+ "end;"
|
||||
+ "return redis.call('getrange', KEYS[1], ARGV[1], ARGV[2]);",
|
||||
Arrays.<Object>asList(getName(), getPartsName()), index, index + len - 1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected RedissonBinaryStream(CommandAsyncExecutor connectionManager, String name) {
|
||||
super(ByteArrayCodec.INSTANCE, connectionManager, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Long> sizeAsync() {
|
||||
return commandExecutor.evalReadAsync(getName(), codec, RedisCommands.EVAL_LONG,
|
||||
"local parts = redis.call('get', KEYS[2]); "
|
||||
+ "local lastPartName = KEYS[1];"
|
||||
+ "if parts ~= false then "
|
||||
+ "lastPartName = KEYS[1] .. ':' .. (tonumber(parts)-1);"
|
||||
+ "local lastPartSize = redis.call('strlen', lastPartName);"
|
||||
+ "return ((tonumber(parts)-1) * 536870912) + lastPartSize;"
|
||||
+ "end;"
|
||||
+ "return redis.call('strlen', lastPartName);",
|
||||
Arrays.<Object>asList(getName(), getPartsName()));
|
||||
}
|
||||
|
||||
private RFuture<Void> writeAsync(byte[] bytes) {
|
||||
return commandExecutor.evalWriteAsync(getName(), codec, RedisCommands.EVAL_VOID,
|
||||
"local parts = redis.call('get', KEYS[2]); "
|
||||
+ "local lastPartName = KEYS[1];"
|
||||
+ "if parts ~= false then "
|
||||
+ "lastPartName = KEYS[1] .. ':' .. (tonumber(parts)-1);"
|
||||
+ "end;"
|
||||
+ "local lastPartSize = redis.call('strlen', lastPartName);"
|
||||
+ "if lastPartSize == 0 then "
|
||||
+ "redis.call('append', lastPartName, ARGV[1]); "
|
||||
+ "return; "
|
||||
+ "end;"
|
||||
|
||||
+ "local chunkSize = 536870912 - lastPartSize; "
|
||||
+ "local arraySize = string.len(ARGV[1]); "
|
||||
+ "if chunkSize > 0 then "
|
||||
+ "if chunkSize >= arraySize then "
|
||||
+ "redis.call('append', lastPartName, ARGV[1]); "
|
||||
+ "return; "
|
||||
+ "else "
|
||||
+ "local chunk = string.sub(ARGV[1], 1, chunkSize);"
|
||||
+ "redis.call('append', lastPartName, chunk); "
|
||||
|
||||
+ "if parts == false then "
|
||||
+ "parts = 1;"
|
||||
+ "redis.call('incrby', KEYS[2], 2); "
|
||||
+ "else "
|
||||
+ "redis.call('incrby', KEYS[2], 1); "
|
||||
+ "end; "
|
||||
|
||||
+ "local newPartName = KEYS[1] .. ':' .. parts; "
|
||||
+ "chunk = string.sub(ARGV[1], -(arraySize - chunkSize));"
|
||||
+ "redis.call('append', newPartName, chunk); "
|
||||
+ "end; "
|
||||
+ "else "
|
||||
+ "if parts == false then "
|
||||
+ "parts = 1;"
|
||||
+ "redis.call('incrby', KEYS[2], 2); "
|
||||
+ "else "
|
||||
+ "redis.call('incrby', KEYS[2], 1); "
|
||||
+ "end; "
|
||||
|
||||
+ "local newPartName = KEYS[1] .. ':' .. parts; "
|
||||
+ "local chunk = string.sub(ARGV[1], -(arraySize - chunkSize));"
|
||||
+ "redis.call('append', newPartName, ARGV[1]); "
|
||||
+ "end; ",
|
||||
Arrays.<Object>asList(getName(), getPartsName()), bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return new RedissonInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
return new RedissonOutputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Void> setAsync(byte[] value) {
|
||||
if (value.length > 512*1024*1024) {
|
||||
RPromise<Void> result = newPromise();
|
||||
int chunkSize = 10*1024*1024;
|
||||
write(value, result, chunkSize, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
return super.setAsync(value);
|
||||
}
|
||||
|
||||
private void write(final byte[] value, final RPromise<Void> result, final int chunkSize, final int i) {
|
||||
final int len = Math.min(value.length - i*chunkSize, chunkSize);
|
||||
byte[] bytes = Arrays.copyOfRange(value, i*chunkSize, i*chunkSize + len);
|
||||
writeAsync(bytes).addListener(new FutureListener<Void>() {
|
||||
@Override
|
||||
public void operationComplete(Future<Void> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
result.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
int j = i + 1;
|
||||
if (j*chunkSize > value.length) {
|
||||
result.trySuccess(null);
|
||||
} else {
|
||||
write(value, result, chunkSize, j);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getPartsName() {
|
||||
return getName() + ":parts";
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Boolean> deleteAsync() {
|
||||
return commandExecutor.evalWriteAsync(getName(), codec, RedisCommands.EVAL_BOOLEAN_AMOUNT,
|
||||
"local parts = redis.call('get', KEYS[2]); "
|
||||
+ "local names = {KEYS[1], KEYS[2]};"
|
||||
+ "if parts ~= false then "
|
||||
+ "for i = 1, tonumber(parts)-1, 1 do "
|
||||
+ "table.insert(names, KEYS[1] .. ':' .. i); "
|
||||
+ "end; "
|
||||
+ "end;"
|
||||
+ "return redis.call('del', unpack(names));",
|
||||
Arrays.<Object>asList(getName(), getPartsName()));
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,781 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.redisson.api.RBlockingFairQueue;
|
||||
import org.redisson.api.RFuture;
|
||||
import org.redisson.client.codec.Codec;
|
||||
import org.redisson.client.codec.LongCodec;
|
||||
import org.redisson.client.codec.StringCodec;
|
||||
import org.redisson.client.protocol.RedisCommands;
|
||||
import org.redisson.command.CommandExecutor;
|
||||
import org.redisson.misc.RPromise;
|
||||
import org.redisson.pubsub.SemaphorePubSub;
|
||||
|
||||
import io.netty.util.Timeout;
|
||||
import io.netty.util.TimerTask;
|
||||
import io.netty.util.concurrent.Future;
|
||||
import io.netty.util.concurrent.FutureListener;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public class RedissonBlockingFairQueue<V> extends RedissonBlockingQueue<V> implements RBlockingFairQueue<V> {
|
||||
|
||||
public static final long TIMEOUT_SECONDS = 30;
|
||||
|
||||
private final UUID id;
|
||||
private final AtomicInteger instances = new AtomicInteger();
|
||||
private final SemaphorePubSub semaphorePubSub;
|
||||
|
||||
protected RedissonBlockingFairQueue(CommandExecutor commandExecutor, String name, SemaphorePubSub semaphorePubSub, UUID id) {
|
||||
super(commandExecutor, name);
|
||||
this.semaphorePubSub = semaphorePubSub;
|
||||
this.id = id;
|
||||
instances.incrementAndGet();
|
||||
}
|
||||
|
||||
protected RedissonBlockingFairQueue(Codec codec, CommandExecutor commandExecutor, String name, SemaphorePubSub semaphorePubSub, UUID id) {
|
||||
super(codec, commandExecutor, name);
|
||||
this.semaphorePubSub = semaphorePubSub;
|
||||
this.id = id;
|
||||
instances.incrementAndGet();
|
||||
}
|
||||
|
||||
private String getIdsListName() {
|
||||
return suffixName(getName(), "list");
|
||||
}
|
||||
|
||||
private String getTimeoutName() {
|
||||
return suffixName(getName(), "timeout");
|
||||
}
|
||||
|
||||
private String getChannelName() {
|
||||
return suffixName(getName(), getCurrentId() + ":channel");
|
||||
}
|
||||
|
||||
private RedissonLockEntry getEntry() {
|
||||
return semaphorePubSub.getEntry(getName());
|
||||
}
|
||||
|
||||
private RFuture<RedissonLockEntry> subscribe() {
|
||||
return semaphorePubSub.subscribe(getName(), getChannelName(), commandExecutor.getConnectionManager());
|
||||
}
|
||||
|
||||
private void unsubscribe(RFuture<RedissonLockEntry> future) {
|
||||
semaphorePubSub.unsubscribe(future.getNow(), getName(), getChannelName(), commandExecutor.getConnectionManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Boolean> deleteAsync() {
|
||||
return commandExecutor.writeAsync(getName(), RedisCommands.DEL_OBJECTS, getName(), getIdsListName(), getTimeoutName());
|
||||
}
|
||||
|
||||
private Long tryAcquire() {
|
||||
return get(tryAcquireAsync());
|
||||
}
|
||||
|
||||
private RFuture<Long> tryAcquireAsync() {
|
||||
long timeout = System.currentTimeMillis() + TIMEOUT_SECONDS*1000;
|
||||
|
||||
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_LONG,
|
||||
|
||||
"local timeout = redis.call('get', KEYS[3]);"
|
||||
+ "if timeout ~= false and tonumber(timeout) <= tonumber(ARGV[3]) then "
|
||||
+ "redis.call('lpop', KEYS[2]); "
|
||||
+ "local nextValue = redis.call('lindex', KEYS[2], 0); "
|
||||
+ "if nextValue ~= false and nextValue ~= ARGV[1] then "
|
||||
+ "redis.call('set', KEYS[3], ARGV[2]);"
|
||||
+ "redis.call('publish', '{' .. KEYS[1] .. '}:' .. nextValue .. ':channel', 1);"
|
||||
+ "end; "
|
||||
+ "end; "
|
||||
|
||||
+ "local items = redis.call('lrange', KEYS[2], 0, -1) "
|
||||
+ "local found = false; "
|
||||
+ "for i=1,#items do "
|
||||
+ "if items[i] == ARGV[1] then "
|
||||
+ "found = true; "
|
||||
+ "break;"
|
||||
+ "end; "
|
||||
+ "end; "
|
||||
|
||||
+ "if found == false then "
|
||||
+ "redis.call('lpush', KEYS[2], ARGV[1]); "
|
||||
+ "end; "
|
||||
|
||||
+ "local value = redis.call('lindex', KEYS[2], 0); "
|
||||
+ "if value == ARGV[1] then "
|
||||
+ "redis.call('set', KEYS[3], ARGV[2]);"
|
||||
+ "local size = redis.call('llen', KEYS[2]); "
|
||||
+ "if size > 1 then "
|
||||
+ "redis.call('lpop', KEYS[2]);"
|
||||
+ "redis.call('rpush', KEYS[2], value);"
|
||||
+ "local nextValue = redis.call('lindex', KEYS[2], 0); "
|
||||
+ "redis.call('publish', '{' .. KEYS[1] .. '}:' .. nextValue .. ':channel', 1);"
|
||||
+ "end; "
|
||||
+ "return nil;"
|
||||
+ "end;"
|
||||
+ "return tonumber(timeout) - tonumber(ARGV[3]);",
|
||||
Arrays.<Object>asList(getName(), getIdsListName(), getTimeoutName()), getCurrentId(), timeout, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private String getCurrentId() {
|
||||
return id.toString();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public V take() throws InterruptedException {
|
||||
Long currentTimeout = tryAcquire();
|
||||
if (currentTimeout == null) {
|
||||
return super.take();
|
||||
}
|
||||
|
||||
RFuture<RedissonLockEntry> future = subscribe();
|
||||
commandExecutor.syncSubscription(future);
|
||||
try {
|
||||
while (true) {
|
||||
currentTimeout = tryAcquire();
|
||||
if (currentTimeout == null) {
|
||||
return super.take();
|
||||
}
|
||||
|
||||
getEntry().getLatch().tryAcquire(currentTimeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
} finally {
|
||||
unsubscribe(future);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
if (instances.decrementAndGet() == 0) {
|
||||
get(commandExecutor.evalWriteAsync(getName(), StringCodec.INSTANCE, RedisCommands.EVAL_VOID_WITH_VALUES,
|
||||
"for i = 1, #ARGV, 1 do "
|
||||
+ "redis.call('lrem', KEYS[1], 0, ARGV[i]);"
|
||||
+"end; ",
|
||||
Collections.<Object>singletonList(getIdsListName()), getCurrentId()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<V> takeAsync() {
|
||||
final RPromise<V> promise = newPromise();
|
||||
|
||||
RFuture<Long> tryAcquireFuture = tryAcquireAsync();
|
||||
tryAcquireFuture.addListener(new FutureListener<Long>() {
|
||||
@Override
|
||||
public void operationComplete(Future<Long> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
final Long currentTimeout = future.getNow();
|
||||
if (currentTimeout == null) {
|
||||
final RFuture<V> pollFuture = RedissonBlockingFairQueue.super.takeAsync();
|
||||
pollFuture.addListener(new FutureListener<V>() {
|
||||
@Override
|
||||
public void operationComplete(Future<V> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
promise.trySuccess(future.getNow());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
final RFuture<RedissonLockEntry> subscribeFuture = subscribe();
|
||||
final AtomicReference<Timeout> futureRef = new AtomicReference<Timeout>();
|
||||
subscribeFuture.addListener(new FutureListener<RedissonLockEntry>() {
|
||||
@Override
|
||||
public void operationComplete(Future<RedissonLockEntry> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
if (futureRef.get() != null) {
|
||||
futureRef.get().cancel();
|
||||
}
|
||||
|
||||
tryTakeAsync(subscribeFuture, promise);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V poll() {
|
||||
Long currentTimeout = tryAcquire();
|
||||
if (currentTimeout == null) {
|
||||
return super.poll();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<V> pollAsync() {
|
||||
final RPromise<V> promise = newPromise();
|
||||
|
||||
RFuture<Long> tryAcquireFuture = tryAcquireAsync();
|
||||
tryAcquireFuture.addListener(new FutureListener<Long>() {
|
||||
@Override
|
||||
public void operationComplete(Future<Long> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
final Long currentTimeout = future.getNow();
|
||||
if (currentTimeout == null) {
|
||||
final RFuture<V> pollFuture = RedissonBlockingFairQueue.super.pollAsync();
|
||||
pollFuture.addListener(new FutureListener<V>() {
|
||||
@Override
|
||||
public void operationComplete(Future<V> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
promise.trySuccess(future.getNow());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
promise.trySuccess(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V poll(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
long startTime = System.currentTimeMillis();
|
||||
Long currentTimeout = tryAcquire();
|
||||
if (currentTimeout == null) {
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
if (remainTime > 0) {
|
||||
return super.poll(remainTime, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
RFuture<RedissonLockEntry> future = subscribe();
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
if (!future.awaitUninterruptibly(remainTime, TimeUnit.MILLISECONDS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
currentTimeout = tryAcquire();
|
||||
if (currentTimeout == null) {
|
||||
spentTime = System.currentTimeMillis() - startTime;
|
||||
remainTime = unit.toMillis(timeout) - spentTime;
|
||||
if (remainTime > 0) {
|
||||
return super.poll(remainTime, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
spentTime = System.currentTimeMillis() - startTime;
|
||||
remainTime = unit.toMillis(timeout) - spentTime;
|
||||
remainTime = Math.min(remainTime, currentTimeout);
|
||||
if (remainTime <= 0 || !getEntry().getLatch().tryAcquire(remainTime, TimeUnit.MILLISECONDS)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
unsubscribe(future);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<V> pollAsync(final long timeout, final TimeUnit unit) {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
final RPromise<V> promise = newPromise();
|
||||
|
||||
RFuture<Long> tryAcquireFuture = tryAcquireAsync();
|
||||
tryAcquireFuture.addListener(new FutureListener<Long>() {
|
||||
@Override
|
||||
public void operationComplete(Future<Long> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
Long currentTimeout = future.getNow();
|
||||
if (currentTimeout == null) {
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
if (remainTime > 0) {
|
||||
final RFuture<V> pollFuture = RedissonBlockingFairQueue.super.pollAsync(remainTime, TimeUnit.MILLISECONDS);
|
||||
pollFuture.addListener(new FutureListener<V>() {
|
||||
@Override
|
||||
public void operationComplete(Future<V> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
promise.trySuccess(future.getNow());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
promise.trySuccess(null);
|
||||
}
|
||||
} else {
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
remainTime = Math.min(remainTime, currentTimeout);
|
||||
if (remainTime <= 0) {
|
||||
promise.trySuccess(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final RFuture<RedissonLockEntry> subscribeFuture = subscribe();
|
||||
final AtomicReference<Timeout> futureRef = new AtomicReference<Timeout>();
|
||||
subscribeFuture.addListener(new FutureListener<RedissonLockEntry>() {
|
||||
@Override
|
||||
public void operationComplete(Future<RedissonLockEntry> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
if (futureRef.get() != null) {
|
||||
futureRef.get().cancel();
|
||||
}
|
||||
|
||||
tryPollAsync(startTime, timeout, unit, subscribeFuture, promise);
|
||||
}
|
||||
});
|
||||
if (!subscribeFuture.isDone()) {
|
||||
Timeout scheduledFuture = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
|
||||
@Override
|
||||
public void run(Timeout timeout) throws Exception {
|
||||
if (!subscribeFuture.isDone()) {
|
||||
subscribeFuture.cancel(false);
|
||||
promise.trySuccess(null);
|
||||
}
|
||||
}
|
||||
}, remainTime, TimeUnit.MILLISECONDS);
|
||||
futureRef.set(scheduledFuture);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
private void tryTakeAsync(final RFuture<RedissonLockEntry> subscribeFuture, final RPromise<V> promise) {
|
||||
if (promise.isDone()) {
|
||||
unsubscribe(subscribeFuture);
|
||||
return;
|
||||
}
|
||||
|
||||
RFuture<Long> tryAcquireFuture = tryAcquireAsync();
|
||||
tryAcquireFuture.addListener(new FutureListener<Long>() {
|
||||
@Override
|
||||
public void operationComplete(Future<Long> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
unsubscribe(subscribeFuture);
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
Long currentTimeout = future.getNow();
|
||||
if (currentTimeout == null) {
|
||||
final RFuture<V> pollFuture = RedissonBlockingFairQueue.super.takeAsync();
|
||||
pollFuture.addListener(new FutureListener<V>() {
|
||||
@Override
|
||||
public void operationComplete(Future<V> future) throws Exception {
|
||||
unsubscribe(subscribeFuture);
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
promise.trySuccess(future.getNow());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
final RedissonLockEntry entry = getEntry();
|
||||
synchronized (entry) {
|
||||
if (entry.getLatch().tryAcquire()) {
|
||||
tryTakeAsync(subscribeFuture, promise);
|
||||
} else {
|
||||
final AtomicBoolean executed = new AtomicBoolean();
|
||||
final AtomicReference<Timeout> futureRef = new AtomicReference<Timeout>();
|
||||
final Runnable listener = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
executed.set(true);
|
||||
if (futureRef.get() != null) {
|
||||
futureRef.get().cancel();
|
||||
}
|
||||
|
||||
tryTakeAsync(subscribeFuture, promise);
|
||||
}
|
||||
};
|
||||
entry.addListener(listener);
|
||||
|
||||
if (!executed.get()) {
|
||||
Timeout scheduledFuture = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
|
||||
@Override
|
||||
public void run(Timeout t) throws Exception {
|
||||
synchronized (entry) {
|
||||
if (entry.removeListener(listener)) {
|
||||
tryTakeAsync(subscribeFuture, promise);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, currentTimeout, TimeUnit.MILLISECONDS);
|
||||
futureRef.set(scheduledFuture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private void tryPollAsync(final long startTime, final long timeout, final TimeUnit unit,
|
||||
final RFuture<RedissonLockEntry> subscribeFuture, final RPromise<V> promise) {
|
||||
if (promise.isDone()) {
|
||||
unsubscribe(subscribeFuture);
|
||||
return;
|
||||
}
|
||||
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
if (remainTime <= 0) {
|
||||
unsubscribe(subscribeFuture);
|
||||
promise.trySuccess(null);
|
||||
return;
|
||||
}
|
||||
|
||||
RFuture<Long> tryAcquireFuture = tryAcquireAsync();
|
||||
tryAcquireFuture.addListener(new FutureListener<Long>() {
|
||||
@Override
|
||||
public void operationComplete(Future<Long> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
unsubscribe(subscribeFuture);
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
Long currentTimeout = future.getNow();
|
||||
if (currentTimeout == null) {
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
if (remainTime > 0) {
|
||||
final RFuture<V> pollFuture = RedissonBlockingFairQueue.super.pollAsync(remainTime, TimeUnit.MILLISECONDS);
|
||||
pollFuture.addListener(new FutureListener<V>() {
|
||||
@Override
|
||||
public void operationComplete(Future<V> future) throws Exception {
|
||||
unsubscribe(subscribeFuture);
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
promise.trySuccess(future.getNow());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
unsubscribe(subscribeFuture);
|
||||
promise.trySuccess(null);
|
||||
}
|
||||
} else {
|
||||
final RedissonLockEntry entry = getEntry();
|
||||
synchronized (entry) {
|
||||
if (entry.getLatch().tryAcquire()) {
|
||||
tryPollAsync(startTime, timeout, unit, subscribeFuture, promise);
|
||||
} else {
|
||||
final AtomicBoolean executed = new AtomicBoolean();
|
||||
final AtomicReference<Timeout> futureRef = new AtomicReference<Timeout>();
|
||||
final Runnable listener = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
executed.set(true);
|
||||
if (futureRef.get() != null) {
|
||||
futureRef.get().cancel();
|
||||
}
|
||||
|
||||
tryPollAsync(startTime, timeout, unit, subscribeFuture, promise);
|
||||
}
|
||||
};
|
||||
entry.addListener(listener);
|
||||
|
||||
if (!executed.get()) {
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
Timeout scheduledFuture = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
|
||||
@Override
|
||||
public void run(Timeout t) throws Exception {
|
||||
synchronized (entry) {
|
||||
if (entry.removeListener(listener)) {
|
||||
tryPollAsync(startTime, timeout, unit, subscribeFuture, promise);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, remainTime, TimeUnit.MILLISECONDS);
|
||||
futureRef.set(scheduledFuture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public V pollLastAndOfferFirstTo(String queueName, long timeout, TimeUnit unit) throws InterruptedException {
|
||||
long startTime = System.currentTimeMillis();
|
||||
Long currentTimeout = tryAcquire();
|
||||
if (currentTimeout == null) {
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
if (remainTime > 0) {
|
||||
return super.pollLastAndOfferFirstTo(queueName, remainTime, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
RFuture<RedissonLockEntry> future = subscribe();
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
if (!future.awaitUninterruptibly(remainTime, TimeUnit.MILLISECONDS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
currentTimeout = tryAcquire();
|
||||
if (currentTimeout == null) {
|
||||
spentTime = System.currentTimeMillis() - startTime;
|
||||
remainTime = unit.toMillis(timeout) - spentTime;
|
||||
if (remainTime > 0) {
|
||||
return super.pollLastAndOfferFirstTo(queueName, remainTime, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
spentTime = System.currentTimeMillis() - startTime;
|
||||
remainTime = unit.toMillis(timeout) - spentTime;
|
||||
remainTime = Math.min(remainTime, currentTimeout);
|
||||
if (remainTime <= 0 || !getEntry().getLatch().tryAcquire(remainTime, TimeUnit.MILLISECONDS)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
unsubscribe(future);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<V> pollLastAndOfferFirstToAsync(final String queueName, final long timeout, final TimeUnit unit) {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
final RPromise<V> promise = newPromise();
|
||||
|
||||
RFuture<Long> tryAcquireFuture = tryAcquireAsync();
|
||||
tryAcquireFuture.addListener(new FutureListener<Long>() {
|
||||
@Override
|
||||
public void operationComplete(Future<Long> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
Long currentTimeout = future.getNow();
|
||||
if (currentTimeout == null) {
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
if (remainTime > 0) {
|
||||
final RFuture<V> pollFuture = RedissonBlockingFairQueue.super.pollLastAndOfferFirstToAsync(queueName, remainTime, TimeUnit.MILLISECONDS);
|
||||
pollFuture.addListener(new FutureListener<V>() {
|
||||
@Override
|
||||
public void operationComplete(Future<V> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
promise.trySuccess(future.getNow());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
promise.trySuccess(null);
|
||||
}
|
||||
} else {
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
remainTime = Math.min(remainTime, currentTimeout);
|
||||
if (remainTime <= 0) {
|
||||
promise.trySuccess(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final RFuture<RedissonLockEntry> subscribeFuture = subscribe();
|
||||
final AtomicReference<Timeout> futureRef = new AtomicReference<Timeout>();
|
||||
subscribeFuture.addListener(new FutureListener<RedissonLockEntry>() {
|
||||
@Override
|
||||
public void operationComplete(Future<RedissonLockEntry> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
if (futureRef.get() != null) {
|
||||
futureRef.get().cancel();
|
||||
}
|
||||
|
||||
tryPollLastAndOfferFirstToAsync(startTime, timeout, unit, subscribeFuture, promise, queueName);
|
||||
}
|
||||
});
|
||||
if (!subscribeFuture.isDone()) {
|
||||
Timeout scheduledFuture = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
|
||||
@Override
|
||||
public void run(Timeout timeout) throws Exception {
|
||||
if (!subscribeFuture.isDone()) {
|
||||
subscribeFuture.cancel(false);
|
||||
promise.trySuccess(null);
|
||||
}
|
||||
}
|
||||
}, remainTime, TimeUnit.MILLISECONDS);
|
||||
futureRef.set(scheduledFuture);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
private void tryPollLastAndOfferFirstToAsync(final long startTime, final long timeout, final TimeUnit unit,
|
||||
final RFuture<RedissonLockEntry> subscribeFuture, final RPromise<V> promise, final String queueName) {
|
||||
if (promise.isDone()) {
|
||||
unsubscribe(subscribeFuture);
|
||||
return;
|
||||
}
|
||||
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
if (remainTime <= 0) {
|
||||
unsubscribe(subscribeFuture);
|
||||
promise.trySuccess(null);
|
||||
return;
|
||||
}
|
||||
|
||||
RFuture<Long> tryAcquireFuture = tryAcquireAsync();
|
||||
tryAcquireFuture.addListener(new FutureListener<Long>() {
|
||||
@Override
|
||||
public void operationComplete(Future<Long> future) throws Exception {
|
||||
if (!future.isSuccess()) {
|
||||
unsubscribe(subscribeFuture);
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
Long currentTimeout = future.getNow();
|
||||
if (currentTimeout == null) {
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
if (remainTime > 0) {
|
||||
final RFuture<V> pollFuture = RedissonBlockingFairQueue.super.pollLastAndOfferFirstToAsync(queueName, remainTime, TimeUnit.MILLISECONDS);
|
||||
pollFuture.addListener(new FutureListener<V>() {
|
||||
@Override
|
||||
public void operationComplete(Future<V> future) throws Exception {
|
||||
unsubscribe(subscribeFuture);
|
||||
if (!future.isSuccess()) {
|
||||
promise.tryFailure(future.cause());
|
||||
return;
|
||||
}
|
||||
|
||||
promise.trySuccess(future.getNow());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
unsubscribe(subscribeFuture);
|
||||
promise.trySuccess(null);
|
||||
}
|
||||
} else {
|
||||
final RedissonLockEntry entry = getEntry();
|
||||
synchronized (entry) {
|
||||
if (entry.getLatch().tryAcquire()) {
|
||||
tryPollAsync(startTime, timeout, unit, subscribeFuture, promise);
|
||||
} else {
|
||||
final AtomicBoolean executed = new AtomicBoolean();
|
||||
final AtomicReference<Timeout> futureRef = new AtomicReference<Timeout>();
|
||||
final Runnable listener = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
executed.set(true);
|
||||
if (futureRef.get() != null) {
|
||||
futureRef.get().cancel();
|
||||
}
|
||||
|
||||
tryPollLastAndOfferFirstToAsync(startTime, timeout, unit, subscribeFuture, promise, queueName);
|
||||
}
|
||||
};
|
||||
entry.addListener(listener);
|
||||
|
||||
if (!executed.get()) {
|
||||
long spentTime = System.currentTimeMillis() - startTime;
|
||||
long remainTime = unit.toMillis(timeout) - spentTime;
|
||||
Timeout scheduledFuture = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
|
||||
@Override
|
||||
public void run(Timeout t) throws Exception {
|
||||
synchronized (entry) {
|
||||
if (entry.removeListener(listener)) {
|
||||
tryPollLastAndOfferFirstToAsync(startTime, timeout, unit, subscribeFuture, promise, queueName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, remainTime, TimeUnit.MILLISECONDS);
|
||||
futureRef.set(scheduledFuture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,502 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.redisson.api.RDelayedQueue;
|
||||
import org.redisson.api.RFuture;
|
||||
import org.redisson.api.RQueue;
|
||||
import org.redisson.api.RTopic;
|
||||
import org.redisson.client.codec.Codec;
|
||||
import org.redisson.client.codec.LongCodec;
|
||||
import org.redisson.client.protocol.RedisCommand;
|
||||
import org.redisson.client.protocol.RedisCommands;
|
||||
import org.redisson.client.protocol.convertor.BooleanReplayConvertor;
|
||||
import org.redisson.client.protocol.convertor.VoidReplayConvertor;
|
||||
import org.redisson.command.CommandAsyncExecutor;
|
||||
|
||||
import io.netty.util.internal.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
* @param <V> value type
|
||||
*/
|
||||
public class RedissonDelayedQueue<V> extends RedissonExpirable implements RDelayedQueue<V> {
|
||||
|
||||
private static final RedisCommand<Void> EVAL_OFFER = new RedisCommand<Void>("EVAL", new VoidReplayConvertor(), 9);
|
||||
|
||||
private final QueueTransferService queueTransferService;
|
||||
|
||||
protected RedissonDelayedQueue(QueueTransferService queueTransferService, Codec codec, final CommandAsyncExecutor commandExecutor, String name) {
|
||||
super(codec, commandExecutor, name);
|
||||
|
||||
QueueTransferTask task = new QueueTransferTask(commandExecutor.getConnectionManager()) {
|
||||
|
||||
@Override
|
||||
protected RFuture<Long> pushTaskAsync() {
|
||||
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_LONG,
|
||||
"local expiredValues = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); "
|
||||
+ "if #expiredValues > 0 then "
|
||||
+ "for i, v in ipairs(expiredValues) do "
|
||||
+ "local randomId, value = struct.unpack('dLc0', v);"
|
||||
+ "redis.call('rpush', KEYS[1], value);"
|
||||
+ "redis.call('lrem', KEYS[3], 1, v);"
|
||||
+ "end; "
|
||||
+ "redis.call('zrem', KEYS[2], unpack(expiredValues));"
|
||||
+ "end; "
|
||||
// get startTime from scheduler queue head task
|
||||
+ "local v = redis.call('zrange', KEYS[2], 0, 0, 'WITHSCORES'); "
|
||||
+ "if v[1] ~= nil then "
|
||||
+ "return v[2]; "
|
||||
+ "end "
|
||||
+ "return nil;",
|
||||
Arrays.<Object>asList(getName(), getTimeoutSetName(), getQueueName()),
|
||||
System.currentTimeMillis(), 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RTopic<Long> getTopic() {
|
||||
return new RedissonTopic<Long>(LongCodec.INSTANCE, commandExecutor, getChannelName());
|
||||
}
|
||||
};
|
||||
|
||||
queueTransferService.schedule(getQueueName(), task);
|
||||
|
||||
this.queueTransferService = queueTransferService;
|
||||
}
|
||||
|
||||
private String getChannelName() {
|
||||
return prefixName("redisson_delay_queue_channel", getName());
|
||||
}
|
||||
|
||||
private String getQueueName() {
|
||||
return prefixName("redisson_delay_queue", getName());
|
||||
}
|
||||
|
||||
private String getTimeoutSetName() {
|
||||
return prefixName("redisson_delay_queue_timeout", getName());
|
||||
}
|
||||
|
||||
public void offer(V e, long delay, TimeUnit timeUnit) {
|
||||
get(offerAsync(e, delay, timeUnit));
|
||||
}
|
||||
|
||||
public RFuture<Void> offerAsync(V e, long delay, TimeUnit timeUnit) {
|
||||
long delayInMs = timeUnit.toMillis(delay);
|
||||
long timeout = System.currentTimeMillis() + delayInMs;
|
||||
|
||||
long randomId = ThreadLocalRandom.current().nextLong();
|
||||
return commandExecutor.evalWriteAsync(getName(), codec, EVAL_OFFER,
|
||||
"local value = struct.pack('dLc0', tonumber(ARGV[2]), string.len(ARGV[3]), ARGV[3]);"
|
||||
+ "redis.call('zadd', KEYS[2], ARGV[1], value);"
|
||||
+ "redis.call('rpush', KEYS[3], value);"
|
||||
// if new object added to queue head when publish its startTime
|
||||
// to all scheduler workers
|
||||
+ "local v = redis.call('zrange', KEYS[2], 0, 0); "
|
||||
+ "if v[1] == value then "
|
||||
+ "redis.call('publish', KEYS[4], ARGV[1]); "
|
||||
+ "end;"
|
||||
,
|
||||
Arrays.<Object>asList(getName(), getTimeoutSetName(), getQueueName(), getChannelName()),
|
||||
timeout, randomId, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(V e) {
|
||||
throw new UnsupportedOperationException("Use 'offer' method with timeout param");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean offer(V e) {
|
||||
throw new UnsupportedOperationException("Use 'offer' method with timeout param");
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove() {
|
||||
V value = poll();
|
||||
if (value == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V poll() {
|
||||
return get(pollAsync());
|
||||
}
|
||||
|
||||
@Override
|
||||
public V element() {
|
||||
V value = peek();
|
||||
if (value == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V peek() {
|
||||
return get(peekAsync());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return get(sizeAsync());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return size() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o) {
|
||||
return get(containsAsync(o));
|
||||
}
|
||||
|
||||
V getValue(int index) {
|
||||
return (V)get(commandExecutor.evalReadAsync(getName(), codec, RedisCommands.EVAL_OBJECT,
|
||||
"local v = redis.call('lindex', KEYS[1], ARGV[1]); "
|
||||
+ "if v ~= false then "
|
||||
+ "local randomId, value = struct.unpack('dLc0', v);"
|
||||
+ "return value; "
|
||||
+ "end "
|
||||
+ "return nil;",
|
||||
Arrays.<Object>asList(getQueueName()), index));
|
||||
}
|
||||
|
||||
void remove(int index) {
|
||||
get(commandExecutor.evalWriteAsync(getName(), null, RedisCommands.EVAL_VOID,
|
||||
"local v = redis.call('lindex', KEYS[1], ARGV[1]);" +
|
||||
"if v ~= false then " +
|
||||
"local randomId, value = struct.unpack('dLc0', v);" +
|
||||
"redis.call('lrem', KEYS[1], 1, v);" +
|
||||
"redis.call('zrem', KEYS[2], v);" +
|
||||
"end; ",
|
||||
Arrays.<Object>asList(getQueueName(), getTimeoutSetName()), index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<V> iterator() {
|
||||
return new Iterator<V>() {
|
||||
|
||||
private V nextCurrentValue;
|
||||
private V currentValueHasRead;
|
||||
private int currentIndex = -1;
|
||||
private boolean hasBeenModified = true;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
V val = RedissonDelayedQueue.this.getValue(currentIndex+1);
|
||||
if (val != null) {
|
||||
nextCurrentValue = val;
|
||||
}
|
||||
return val != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V next() {
|
||||
if (nextCurrentValue == null && !hasNext()) {
|
||||
throw new NoSuchElementException("No such element at index " + currentIndex);
|
||||
}
|
||||
currentIndex++;
|
||||
currentValueHasRead = nextCurrentValue;
|
||||
nextCurrentValue = null;
|
||||
hasBeenModified = false;
|
||||
return currentValueHasRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (currentValueHasRead == null) {
|
||||
throw new IllegalStateException("Neither next nor previous have been called");
|
||||
}
|
||||
if (hasBeenModified) {
|
||||
throw new IllegalStateException("Element been already deleted");
|
||||
}
|
||||
RedissonDelayedQueue.this.remove(currentIndex);
|
||||
currentIndex--;
|
||||
hasBeenModified = true;
|
||||
currentValueHasRead = null;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray() {
|
||||
List<V> list = readAll();
|
||||
return list.toArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T[] toArray(T[] a) {
|
||||
List<V> list = readAll();
|
||||
return list.toArray(a);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<V> readAll() {
|
||||
return get(readAllAsync());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<List<V>> readAllAsync() {
|
||||
return commandExecutor.evalReadAsync(getName(), codec, RedisCommands.EVAL_LIST,
|
||||
"local result = {}; " +
|
||||
"local items = redis.call('lrange', KEYS[1], 0, -1); "
|
||||
+ "for i, v in ipairs(items) do "
|
||||
+ "local randomId, value = struct.unpack('dLc0', v); "
|
||||
+ "table.insert(result, value);"
|
||||
+ "end; "
|
||||
+ "return result; ",
|
||||
Collections.<Object>singletonList(getQueueName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object o) {
|
||||
return get(removeAsync(o));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Boolean> removeAsync(Object o) {
|
||||
return removeAsync(o, 1);
|
||||
}
|
||||
|
||||
protected RFuture<Boolean> removeAsync(Object o, int count) {
|
||||
return commandExecutor.evalWriteAsync(getName(), codec, new RedisCommand<Boolean>("EVAL", new BooleanReplayConvertor(), 4),
|
||||
"local s = redis.call('llen', KEYS[1]);" +
|
||||
"for i = 0, s-1, 1 do "
|
||||
+ "local v = redis.call('lindex', KEYS[1], i);"
|
||||
+ "local randomId, value = struct.unpack('dLc0', v);"
|
||||
+ "if ARGV[1] == value then "
|
||||
+ "redis.call('lrem', KEYS[1], 1, v);"
|
||||
+ "return 1;"
|
||||
+ "end; "
|
||||
+ "end;" +
|
||||
"return 0;",
|
||||
Collections.<Object>singletonList(getQueueName()), o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Boolean> containsAllAsync(Collection<?> c) {
|
||||
if (c.isEmpty()) {
|
||||
return newSucceededFuture(true);
|
||||
}
|
||||
|
||||
return commandExecutor.evalReadAsync(getName(), codec, RedisCommands.EVAL_BOOLEAN_WITH_VALUES,
|
||||
"local s = redis.call('llen', KEYS[1]);" +
|
||||
"for i = 0, s-1, 1 do "
|
||||
+ "local v = redis.call('lindex', KEYS[1], i);"
|
||||
+ "local randomId, value = struct.unpack('dLc0', v);"
|
||||
|
||||
+ "for j = 1, #ARGV, 1 do "
|
||||
+ "if value == ARGV[j] then "
|
||||
+ "table.remove(ARGV, j) "
|
||||
+ "end; "
|
||||
+ "end; "
|
||||
+ "end;" +
|
||||
"return #ARGV == 0 and 1 or 0;",
|
||||
Collections.<Object>singletonList(getQueueName()), c.toArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAll(Collection<?> c) {
|
||||
return get(containsAllAsync(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(Collection<? extends V> c) {
|
||||
throw new UnsupportedOperationException("Use 'offer' method with timeout param");
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Boolean> removeAllAsync(Collection<?> c) {
|
||||
if (c.isEmpty()) {
|
||||
return newSucceededFuture(false);
|
||||
}
|
||||
|
||||
return commandExecutor.evalWriteAsync(getName(), codec, RedisCommands.EVAL_BOOLEAN_WITH_VALUES,
|
||||
"local result = 0;" +
|
||||
"local s = redis.call('llen', KEYS[1]);" +
|
||||
"local i = 0;" +
|
||||
"while i < s do "
|
||||
+ "local v = redis.call('lindex', KEYS[1], i);"
|
||||
+ "local randomId, value = struct.unpack('dLc0', v);"
|
||||
|
||||
+ "for j = 1, #ARGV, 1 do "
|
||||
+ "if value == ARGV[j] then "
|
||||
+ "result = 1; "
|
||||
+ "i = i - 1; "
|
||||
+ "s = s - 1; "
|
||||
+ "redis.call('lrem', KEYS[1], 0, v); "
|
||||
+ "break; "
|
||||
+ "end; "
|
||||
+ "end; "
|
||||
+ "i = i + 1;"
|
||||
+ "end; "
|
||||
+ "return result;",
|
||||
Collections.<Object>singletonList(getQueueName()), c.toArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll(Collection<?> c) {
|
||||
return get(removeAllAsync(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainAll(Collection<?> c) {
|
||||
return get(retainAllAsync(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Boolean> retainAllAsync(Collection<?> c) {
|
||||
if (c.isEmpty()) {
|
||||
return deleteAsync();
|
||||
}
|
||||
|
||||
return commandExecutor.evalWriteAsync(getName(), codec, RedisCommands.EVAL_BOOLEAN_WITH_VALUES,
|
||||
"local changed = 0; " +
|
||||
"local items = redis.call('lrange', KEYS[1], 0, -1); "
|
||||
+ "local i = 1; "
|
||||
+ "while i <= #items do "
|
||||
+ "local randomId, element = struct.unpack('dLc0', items[i]); "
|
||||
+ "local isInAgrs = false; "
|
||||
+ "for j = 1, #ARGV, 1 do "
|
||||
+ "if ARGV[j] == element then "
|
||||
+ "isInAgrs = true; "
|
||||
+ "break; "
|
||||
+ "end; "
|
||||
+ "end; "
|
||||
+ "if isInAgrs == false then "
|
||||
+ "redis.call('LREM', KEYS[1], 0, items[i]) "
|
||||
+ "changed = 1; "
|
||||
+ "end; "
|
||||
+ "i = i + 1; "
|
||||
+ "end; "
|
||||
+ "return changed; ",
|
||||
Collections.<Object>singletonList(getQueueName()), c.toArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Boolean> deleteAsync() {
|
||||
return commandExecutor.writeAsync(getName(), RedisCommands.DEL_OBJECTS, getQueueName(), getTimeoutSetName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<V> peekAsync() {
|
||||
return commandExecutor.evalReadAsync(getName(), codec, RedisCommands.EVAL_OBJECT,
|
||||
"local v = redis.call('lindex', KEYS[1], 0); "
|
||||
+ "if v ~= nil then "
|
||||
+ "local randomId, value = struct.unpack('dLc0', v);"
|
||||
+ "return value; "
|
||||
+ "end "
|
||||
+ "return nil;",
|
||||
Arrays.<Object>asList(getQueueName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<V> pollAsync() {
|
||||
return commandExecutor.evalWriteAsync(getName(), codec, RedisCommands.EVAL_OBJECT,
|
||||
"local v = redis.call('lpop', KEYS[1]); "
|
||||
+ "if v ~= nil then "
|
||||
+ "redis.call('zrem', KEYS[2], v); "
|
||||
+ "local randomId, value = struct.unpack('dLc0', v);"
|
||||
+ "return value; "
|
||||
+ "end "
|
||||
+ "return nil;",
|
||||
Arrays.<Object>asList(getQueueName(), getTimeoutSetName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Boolean> offerAsync(V e) {
|
||||
throw new UnsupportedOperationException("Use 'offer' method with timeout param");
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<V> pollLastAndOfferFirstToAsync(String queueName) {
|
||||
return commandExecutor.evalWriteAsync(getName(), codec, RedisCommands.EVAL_OBJECT,
|
||||
"local v = redis.call('rpop', KEYS[1]); "
|
||||
+ "if v ~= nil then "
|
||||
+ "redis.call('zrem', KEYS[2], v); "
|
||||
+ "local randomId, value = struct.unpack('dLc0', v);"
|
||||
+ "redis.call('lpush', KEYS[3], value); "
|
||||
+ "return value; "
|
||||
+ "end "
|
||||
+ "return nil;",
|
||||
Arrays.<Object>asList(getQueueName(), getTimeoutSetName(), queueName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Boolean> containsAsync(Object o) {
|
||||
return commandExecutor.evalReadAsync(getName(), codec, new RedisCommand<Boolean>("EVAL", new BooleanReplayConvertor(), 4),
|
||||
"local s = redis.call('llen', KEYS[1]);" +
|
||||
"for i = 0, s-1, 1 do "
|
||||
+ "local v = redis.call('lindex', KEYS[1], i);"
|
||||
+ "local randomId, value = struct.unpack('dLc0', v);"
|
||||
+ "if ARGV[1] == value then "
|
||||
+ "return 1;"
|
||||
+ "end; "
|
||||
+ "end;" +
|
||||
"return 0;",
|
||||
Collections.<Object>singletonList(getQueueName()), o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Integer> sizeAsync() {
|
||||
return commandExecutor.readAsync(getName(), codec, RedisCommands.LLEN_INT, getQueueName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Boolean> addAsync(V e) {
|
||||
throw new UnsupportedOperationException("Use 'offer' method with timeout param");
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<Boolean> addAllAsync(Collection<? extends V> c) {
|
||||
throw new UnsupportedOperationException("Use 'offer' method with timeout param");
|
||||
}
|
||||
|
||||
@Override
|
||||
public V pollLastAndOfferFirstTo(String dequeName) {
|
||||
return get(pollLastAndOfferFirstToAsync(dequeName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public V pollLastAndOfferFirstTo(RQueue<V> deque) {
|
||||
return get(pollLastAndOfferFirstToAsync(deque.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
queueTransferService.remove(getQueueName());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.redisson.api.RFuture;
|
||||
import org.redisson.api.RPriorityDeque;
|
||||
import org.redisson.client.codec.Codec;
|
||||
import org.redisson.client.protocol.RedisCommand;
|
||||
import org.redisson.client.protocol.RedisCommand.ValueType;
|
||||
import org.redisson.client.protocol.RedisCommands;
|
||||
import org.redisson.client.protocol.convertor.VoidReplayConvertor;
|
||||
import org.redisson.client.protocol.decoder.ListFirstObjectDecoder;
|
||||
import org.redisson.command.CommandExecutor;
|
||||
|
||||
/**
|
||||
* Distributed and concurrent implementation of {@link java.util.Queue}
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
* @param <V> the type of elements held in this collection
|
||||
*/
|
||||
public class RedissonPriorityDeque<V> extends RedissonPriorityQueue<V> implements RPriorityDeque<V> {
|
||||
|
||||
private static final RedisCommand<Void> RPUSH_VOID = new RedisCommand<Void>("RPUSH", new VoidReplayConvertor(), 2, ValueType.OBJECTS);
|
||||
private static final RedisCommand<Object> LRANGE_SINGLE = new RedisCommand<Object>("LRANGE", new ListFirstObjectDecoder());
|
||||
|
||||
|
||||
protected RedissonPriorityDeque(CommandExecutor commandExecutor, String name, Redisson redisson) {
|
||||
super(commandExecutor, name, redisson);
|
||||
}
|
||||
|
||||
public RedissonPriorityDeque(Codec codec, CommandExecutor commandExecutor, String name, Redisson redisson) {
|
||||
super(codec, commandExecutor, name, redisson);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFirst(V e) {
|
||||
get(addFirstAsync(e));
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<Void> addFirstAsync(V e) {
|
||||
return commandExecutor.writeAsync(getName(), codec, RedisCommands.LPUSH_VOID, getName(), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLast(V e) {
|
||||
get(addLastAsync(e));
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<Void> addLastAsync(V e) {
|
||||
return commandExecutor.writeAsync(getName(), codec, RPUSH_VOID, getName(), e);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Iterator<V> descendingIterator() {
|
||||
return new Iterator<V>() {
|
||||
|
||||
private int currentIndex = size();
|
||||
private boolean removeExecuted;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
int size = size();
|
||||
return currentIndex > 0 && size > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V next() {
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException("No such element at index " + currentIndex);
|
||||
}
|
||||
currentIndex--;
|
||||
removeExecuted = false;
|
||||
return RedissonPriorityDeque.this.get(currentIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (removeExecuted) {
|
||||
throw new IllegalStateException("Element been already deleted");
|
||||
}
|
||||
RedissonPriorityDeque.this.remove(currentIndex);
|
||||
currentIndex++;
|
||||
removeExecuted = true;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<V> getLastAsync() {
|
||||
return commandExecutor.readAsync(getName(), codec, LRANGE_SINGLE, getName(), -1, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V getLast() {
|
||||
V result = get(getLastAsync());
|
||||
if (result == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean offerFirst(V e) {
|
||||
return get(offerFirstAsync(e));
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<Boolean> offerFirstAsync(V e) {
|
||||
return commandExecutor.writeAsync(getName(), codec, RedisCommands.LPUSH_BOOLEAN, getName(), e);
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<Boolean> offerLastAsync(V e) {
|
||||
return offerAsync(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean offerLast(V e) {
|
||||
return get(offerLastAsync(e));
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<V> peekFirstAsync() {
|
||||
return getAsync(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V peekFirst() {
|
||||
return get(peekFirstAsync());
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<V> peekLastAsync() {
|
||||
return getLastAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public V peekLast() {
|
||||
return get(getLastAsync());
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<V> pollFirstAsync() {
|
||||
return pollAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public V pollFirst() {
|
||||
return poll();
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<V> pollLastAsync() {
|
||||
return commandExecutor.writeAsync(getName(), codec, RedisCommands.RPOP, getName());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public V pollLast() {
|
||||
return get(pollLastAsync());
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<V> popAsync() {
|
||||
return pollAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public V pop() {
|
||||
return removeFirst();
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<Void> pushAsync(V e) {
|
||||
return addFirstAsync(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void push(V e) {
|
||||
addFirst(e);
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<Boolean> removeFirstOccurrenceAsync(Object o) {
|
||||
return removeAsync(o, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeFirstOccurrence(Object o) {
|
||||
return remove(o, 1);
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<V> removeFirstAsync() {
|
||||
return pollAsync();
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<V> removeLastAsync() {
|
||||
return commandExecutor.writeAsync(getName(), codec, RedisCommands.RPOP, getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public V removeLast() {
|
||||
V value = get(removeLastAsync());
|
||||
if (value == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<Boolean> removeLastOccurrenceAsync(Object o) {
|
||||
return removeAsync(o, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeLastOccurrence(Object o) {
|
||||
return remove(o, -1);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,414 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.redisson.api.RBucket;
|
||||
import org.redisson.api.RFuture;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RPriorityQueue;
|
||||
import org.redisson.client.codec.Codec;
|
||||
import org.redisson.client.codec.StringCodec;
|
||||
import org.redisson.client.protocol.RedisCommands;
|
||||
import org.redisson.command.CommandExecutor;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
* @param <V> value type
|
||||
*/
|
||||
public class RedissonPriorityQueue<V> extends RedissonList<V> implements RPriorityQueue<V> {
|
||||
|
||||
private static class NaturalComparator<V> implements Comparator<V>, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 7207038068494060240L;
|
||||
|
||||
static final NaturalComparator NATURAL_ORDER = new NaturalComparator();
|
||||
|
||||
public int compare(V c1, V c2) {
|
||||
Comparable<Object> c1co = (Comparable<Object>) c1;
|
||||
Comparable<Object> c2co = (Comparable<Object>) c2;
|
||||
return c1co.compareTo(c2co);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class BinarySearchResult<V> {
|
||||
|
||||
private V value;
|
||||
private Integer index;
|
||||
|
||||
public BinarySearchResult(V value) {
|
||||
super();
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public BinarySearchResult() {
|
||||
}
|
||||
|
||||
public void setIndex(Integer index) {
|
||||
this.index = index;
|
||||
}
|
||||
public Integer getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public V getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private Comparator<? super V> comparator = NaturalComparator.NATURAL_ORDER;
|
||||
|
||||
CommandExecutor commandExecutor;
|
||||
|
||||
private RLock lock;
|
||||
private RBucket<String> comparatorHolder;
|
||||
|
||||
protected RedissonPriorityQueue(CommandExecutor commandExecutor, String name, Redisson redisson) {
|
||||
super(commandExecutor, name);
|
||||
this.commandExecutor = commandExecutor;
|
||||
|
||||
comparatorHolder = redisson.getBucket(getComparatorKeyName(), StringCodec.INSTANCE);
|
||||
lock = redisson.getLock("redisson_sortedset_lock:{" + getName() + "}");
|
||||
|
||||
loadComparator();
|
||||
}
|
||||
|
||||
public RedissonPriorityQueue(Codec codec, CommandExecutor commandExecutor, String name, Redisson redisson) {
|
||||
super(codec, commandExecutor, name);
|
||||
this.commandExecutor = commandExecutor;
|
||||
|
||||
comparatorHolder = redisson.getBucket(getComparatorKeyName(), StringCodec.INSTANCE);
|
||||
lock = redisson.getLock("redisson_sortedset_lock:{" + getName() + "}");
|
||||
|
||||
loadComparator();
|
||||
}
|
||||
|
||||
private void loadComparator() {
|
||||
try {
|
||||
String comparatorSign = comparatorHolder.get();
|
||||
if (comparatorSign != null) {
|
||||
String[] parts = comparatorSign.split(":");
|
||||
String className = parts[0];
|
||||
String sign = parts[1];
|
||||
|
||||
String result = calcClassSign(className);
|
||||
if (!result.equals(sign)) {
|
||||
throw new IllegalStateException("Local class signature of " + className + " differs from used by this SortedSet!");
|
||||
}
|
||||
|
||||
Class<?> clazz = Class.forName(className);
|
||||
comparator = (Comparator<V>) clazz.newInstance();
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO cache result
|
||||
private static String calcClassSign(String name) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(name);
|
||||
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
ObjectOutputStream outputStream = new ObjectOutputStream(result);
|
||||
outputStream.writeObject(clazz);
|
||||
outputStream.close();
|
||||
|
||||
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
|
||||
crypt.reset();
|
||||
crypt.update(result.toByteArray());
|
||||
|
||||
return new BigInteger(1, crypt.digest()).toString(16);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Can't calculate sign of " + name, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<V> readAll() {
|
||||
return get(readAllAsync());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RFuture<List<V>> readAllAsync() {
|
||||
return commandExecutor.readAsync(getName(), codec, RedisCommands.LRANGE, getName(), 0, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean offer(V e) {
|
||||
return add(e);
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<Boolean> offerAsync(V e) {
|
||||
return addAsync(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(final Object o) {
|
||||
return binarySearch((V)o, codec).getIndex() >= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(V value) {
|
||||
lock.lock();
|
||||
|
||||
try {
|
||||
checkComparator();
|
||||
|
||||
BinarySearchResult<V> res = binarySearch(value, codec);
|
||||
int index = 0;
|
||||
if (res.getIndex() < 0) {
|
||||
index = -(res.getIndex() + 1);
|
||||
} else {
|
||||
index = res.getIndex() + 1;
|
||||
}
|
||||
|
||||
byte[] encodedValue = encode(value);
|
||||
|
||||
commandExecutor.evalWrite(getName(), RedisCommands.EVAL_VOID,
|
||||
"local len = redis.call('llen', KEYS[1]);"
|
||||
+ "if tonumber(ARGV[1]) < len then "
|
||||
+ "local pivot = redis.call('lindex', KEYS[1], ARGV[1]);"
|
||||
+ "redis.call('linsert', KEYS[1], 'before', pivot, ARGV[2]);"
|
||||
+ "return;"
|
||||
+ "end;"
|
||||
+ "redis.call('rpush', KEYS[1], ARGV[2]);", Arrays.<Object>asList(getName()), index, encodedValue);
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkComparator() {
|
||||
String comparatorSign = comparatorHolder.get();
|
||||
if (comparatorSign != null) {
|
||||
String[] vals = comparatorSign.split(":");
|
||||
String className = vals[0];
|
||||
if (!comparator.getClass().getName().equals(className)) {
|
||||
loadComparator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object value) {
|
||||
lock.lock();
|
||||
|
||||
try {
|
||||
checkComparator();
|
||||
|
||||
BinarySearchResult<V> res = binarySearch((V) value, codec);
|
||||
if (res.getIndex() < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
remove((int)res.getIndex());
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAll(Collection<?> c) {
|
||||
for (Object object : c) {
|
||||
if (!contains(object)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(Collection<? extends V> c) {
|
||||
boolean changed = false;
|
||||
for (V v : c) {
|
||||
if (add(v)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainAll(Collection<?> c) {
|
||||
boolean changed = false;
|
||||
for (Iterator<?> iterator = iterator(); iterator.hasNext();) {
|
||||
Object object = (Object) iterator.next();
|
||||
if (!c.contains(object)) {
|
||||
iterator.remove();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll(Collection<?> c) {
|
||||
boolean changed = false;
|
||||
for (Object obj : c) {
|
||||
if (remove(obj)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<? super V> comparator() {
|
||||
return comparator;
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<V> pollAsync() {
|
||||
return commandExecutor.writeAsync(getName(), codec, RedisCommands.LPOP, getName());
|
||||
}
|
||||
|
||||
public V getFirst() {
|
||||
V value = getValue(0);
|
||||
if (value == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V poll() {
|
||||
return get(pollAsync());
|
||||
}
|
||||
|
||||
@Override
|
||||
public V element() {
|
||||
return getFirst();
|
||||
}
|
||||
|
||||
// @Override
|
||||
public RFuture<V> peekAsync() {
|
||||
return getAsync(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V peek() {
|
||||
return getValue(0);
|
||||
}
|
||||
|
||||
private String getComparatorKeyName() {
|
||||
return "redisson_sortedset_comparator:{" + getName() + "}";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean trySetComparator(Comparator<? super V> comparator) {
|
||||
String className = comparator.getClass().getName();
|
||||
final String comparatorSign = className + ":" + calcClassSign(className);
|
||||
|
||||
Boolean res = commandExecutor.evalWrite(getName(), StringCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
|
||||
"if redis.call('llen', KEYS[1]) == 0 then "
|
||||
+ "redis.call('set', KEYS[2], ARGV[1]); "
|
||||
+ "return 1; "
|
||||
+ "else "
|
||||
+ "return 0; "
|
||||
+ "end",
|
||||
Arrays.<Object>asList(getName(), getComparatorKeyName()), comparatorSign);
|
||||
if (res) {
|
||||
this.comparator = comparator;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove() {
|
||||
return removeFirst();
|
||||
}
|
||||
|
||||
public V removeFirst() {
|
||||
V value = poll();
|
||||
if (value == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// TODO optimize: get three values each time instead of single
|
||||
public BinarySearchResult<V> binarySearch(V value, Codec codec) {
|
||||
int size = size();
|
||||
int upperIndex = size - 1;
|
||||
int lowerIndex = 0;
|
||||
while (lowerIndex <= upperIndex) {
|
||||
int index = lowerIndex + (upperIndex - lowerIndex) / 2;
|
||||
|
||||
V res = getValue(index);
|
||||
if (res == null) {
|
||||
return new BinarySearchResult<V>();
|
||||
}
|
||||
int cmp = comparator.compare(value, res);
|
||||
|
||||
if (cmp == 0) {
|
||||
BinarySearchResult<V> indexRes = new BinarySearchResult<V>();
|
||||
indexRes.setIndex(index);
|
||||
return indexRes;
|
||||
} else if (cmp < 0) {
|
||||
upperIndex = index - 1;
|
||||
} else {
|
||||
lowerIndex = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
BinarySearchResult<V> indexRes = new BinarySearchResult<V>();
|
||||
indexRes.setIndex(-(lowerIndex + 1));
|
||||
return indexRes;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
Iterator<V> it = iterator();
|
||||
if (! it.hasNext())
|
||||
return "[]";
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append('[');
|
||||
for (;;) {
|
||||
V e = it.next();
|
||||
sb.append(e == this ? "(this Collection)" : e);
|
||||
if (! it.hasNext())
|
||||
return sb.append(']').toString();
|
||||
sb.append(',').append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson.api;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.redisson.api.Node.InfoSection;
|
||||
|
||||
/**
|
||||
* Redis node interface
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public interface NodeAsync {
|
||||
|
||||
RFuture<Map<String, String>> infoAsync(InfoSection section);
|
||||
|
||||
RFuture<Long> timeAsync();
|
||||
|
||||
RFuture<Boolean> pingAsync();
|
||||
|
||||
RFuture<Map<String, String>> clusterInfoAsync();
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson.api;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Binary stream holder. Maximum size of stream is limited by available memory of Redis master node.
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
*/
|
||||
public interface RBinaryStream extends RBucket<byte[]> {
|
||||
|
||||
/**
|
||||
* Returns inputStream which reads binary stream.
|
||||
* This stream isn't thread-safe.
|
||||
*
|
||||
* @return stream
|
||||
*/
|
||||
InputStream getInputStream();
|
||||
|
||||
/**
|
||||
* Returns outputStream which writes binary stream.
|
||||
* This stream isn't thread-safe.
|
||||
*
|
||||
* @return stream
|
||||
*/
|
||||
OutputStream getOutputStream();
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright 2016 Nikita Koksharov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.redisson.api;
|
||||
|
||||
/**
|
||||
* Blocking queue with fair polling and
|
||||
* guarantees access order for poll and take methods.
|
||||
*
|
||||
* @author Nikita Koksharov
|
||||
*
|
||||
* @param <V> value
|
||||
*/
|
||||
public interface RBlockingFairQueue<V> extends RBlockingQueue<V>, RDestroyable {
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue