cache the "tree"

This commit is contained in:
Athou
2013-07-02 18:07:08 +02:00
parent 18016c952e
commit f0f1a7f87b
15 changed files with 216 additions and 76 deletions

View File

@@ -6,6 +6,8 @@ import org.apache.commons.codec.digest.DigestUtils;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.Category;
public abstract class CacheService {
@@ -13,9 +15,15 @@ public abstract class CacheService {
public abstract void setLastEntries(Feed feed, List<String> entries);
public String buildKey(Feed feed, FeedEntry entry) {
public String buildUniqueEntryKey(Feed feed, FeedEntry entry) {
return DigestUtils.sha1Hex(entry.getGuid() +
entry.getUrl());
}
public abstract Category getRootCategory(User user);
public abstract void setRootCategory(User user, Category category);
public abstract void invalidateRootCategory(User... users);
}

View File

@@ -1,46 +0,0 @@
package com.commafeed.backend.cache;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative;
import javax.inject.Inject;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
@Alternative
@ApplicationScoped
public class InMemoryCacheService extends CacheService {
@Inject
ApplicationSettingsService applicationSettingsService;
private Cache<Long, List<String>> entryCache;
@PostConstruct
private void init() {
int capacity = applicationSettingsService.get().isHeavyLoad() ? 1000000 : 100;
entryCache = CacheBuilder.newBuilder()
.maximumSize(capacity).expireAfterWrite(24, TimeUnit.HOURS).build();
}
@Override
public List<String> getLastEntries(Feed feed) {
List<String> list = entryCache.getIfPresent(feed.getId());
if (list == null) {
list = Collections.emptyList();
}
return list;
}
@Override
public void setLastEntries(Feed feed, List<String> entries) {
entryCache.put(feed.getId(), entries);
}
}

View File

@@ -0,0 +1,40 @@
package com.commafeed.backend.cache;
import java.util.Collections;
import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.Category;
@Alternative
@ApplicationScoped
public class NoopCacheService extends CacheService {
@Override
public List<String> getLastEntries(Feed feed) {
return Collections.emptyList();
}
@Override
public void setLastEntries(Feed feed, List<String> entries) {
}
@Override
public Category getRootCategory(User user) {
return null;
}
@Override
public void setRootCategory(User user, Category category) {
}
@Override
public void invalidateRootCategory(User... users) {
}
}

View File

@@ -7,26 +7,37 @@ import java.util.concurrent.TimeUnit;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Pipeline;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.User;
import com.commafeed.frontend.model.Category;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.api.client.util.Lists;
@Alternative
@ApplicationScoped
public class RedisCacheService extends CacheService {
private static final Logger log = LoggerFactory.getLogger(RedisCacheService.class);
private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
private ObjectMapper mapper = new ObjectMapper();
@Override
public List<String> getLastEntries(Feed feed) {
List<String> list = Lists.newArrayList();
Jedis jedis = pool.getResource();
try {
String key = buildKey(feed);
String key = buildRedisEntryKey(feed);
Set<String> members = jedis.smembers(key);
for (String member : members) {
list.add(member);
@@ -41,7 +52,7 @@ public class RedisCacheService extends CacheService {
public void setLastEntries(Feed feed, List<String> entries) {
Jedis jedis = pool.getResource();
try {
String key = buildKey(feed);
String key = buildRedisEntryKey(feed);
Pipeline pipe = jedis.pipelined();
pipe.del(key);
@@ -55,7 +66,64 @@ public class RedisCacheService extends CacheService {
}
}
private String buildKey(Feed feed) {
@Override
public Category getRootCategory(User user) {
Category cat = null;
Jedis jedis = pool.getResource();
try {
String key = buildRedisRootCategoryKey(user);
String json = jedis.get(key);
if (json != null) {
cat = mapper.readValue(json, Category.class);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
pool.returnResource(jedis);
}
return cat;
}
@Override
public void setRootCategory(User user, Category category) {
Jedis jedis = pool.getResource();
try {
String key = buildRedisRootCategoryKey(user);
Pipeline pipe = jedis.pipelined();
pipe.del(key);
pipe.set(key, mapper.writeValueAsString(category));
pipe.expire(key, (int) TimeUnit.MINUTES.toSeconds(30));
pipe.sync();
} catch (JsonProcessingException e) {
log.error(e.getMessage(), e);
} finally {
pool.returnResource(jedis);
}
}
@Override
public void invalidateRootCategory(User... users) {
Jedis jedis = pool.getResource();
try {
Pipeline pipe = jedis.pipelined();
if (users != null) {
for (User user : users) {
String key = buildRedisRootCategoryKey(user);
pipe.del(key);
}
}
pipe.sync();
} finally {
pool.returnResource(jedis);
}
}
private String buildRedisRootCategoryKey(User user) {
return "root_cat:" + Models.getId(user);
}
private String buildRedisEntryKey(Feed feed) {
return "feed:" + feed.getId();
}

View File

@@ -20,7 +20,6 @@ import javax.persistence.criteria.SetJoin;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.hibernate.Hibernate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,6 +34,7 @@ import com.commafeed.backend.model.FeedEntry_;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.FeedSubscription_;
import com.commafeed.backend.model.Feed_;
import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.backend.services.ApplicationSettingsService;
@@ -507,8 +507,8 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
List<FeedEntryStatus> results) {
if (includeContent) {
for (FeedEntryStatus status : results) {
Hibernate.initialize(status.getSubscription().getFeed());
Hibernate.initialize(status.getEntry().getContent());
Models.initialize(status.getSubscription().getFeed());
Models.initialize(status.getEntry().getContent());
}
}
return results;

View File

@@ -8,14 +8,13 @@ import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.hibernate.Hibernate;
import com.commafeed.backend.model.Feed;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedCategory_;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.FeedSubscription_;
import com.commafeed.backend.model.Feed_;
import com.commafeed.backend.model.Models;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.User_;
import com.google.common.collect.Iterables;
@@ -127,8 +126,8 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
private void initRelations(FeedSubscription sub) {
if (sub != null) {
Hibernate.initialize(sub.getFeed());
Hibernate.initialize(sub.getCategory());
Models.initialize(sub.getFeed());
Models.initialize(sub.getCategory());
}
}
}

View File

@@ -106,7 +106,7 @@ public class FeedRefreshUpdater {
List<FeedSubscription> subscriptions = null;
for (FeedEntry entry : entries) {
String cacheKey = cache.buildKey(feed, entry);
String cacheKey = cache.buildUniqueEntryKey(feed, entry);
if (!lastEntries.contains(cacheKey)) {
log.debug("cache miss for {}", entry.getUrl());
if (subscriptions == null) {

View File

@@ -13,6 +13,7 @@ import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.User;
@@ -34,6 +35,9 @@ public class OPMLImporter {
@Inject
FeedCategoryDAO feedCategoryDAO;
@Inject
CacheService cache;
@SuppressWarnings("unchecked")
@Asynchronous
public void importOpml(User user, String xml) {
@@ -77,16 +81,17 @@ public class OPMLImporter {
if (StringUtils.isBlank(title)) {
title = "Unnamed subscription";
}
// make sure we continue with the import process even a feed failed
try {
feedSubscriptionService.subscribe(user, outline.getXmlUrl(), title,
parent);
} catch (FeedSubscriptionException e) {
throw e;
} catch (Exception e) {
log.error("error while importing {}: {}", outline.getXmlUrl(), e.getMessage());
}
}
cache.invalidateRootCategory(user);
}
}

View File

@@ -0,0 +1,29 @@
package com.commafeed.backend.model;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
public class Models {
/**
* initialize a proxy
*/
public static void initialize(Object proxy) throws HibernateException {
Hibernate.initialize(proxy);
}
/**
* extract the id from the proxy without initializing it
*/
public static Long getId(AbstractModel model) {
if (model instanceof HibernateProxy) {
LazyInitializer lazyInitializer = ((HibernateProxy) model).getHibernateLazyInitializer();
if (lazyInitializer.isUninitialized()) {
return (Long) lazyInitializer.getIdentifier();
}
}
return model.getId();
}
}

View File

@@ -7,6 +7,7 @@ import javax.ejb.Stateless;
import javax.inject.Inject;
import com.commafeed.backend.MetricsBean;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedEntryDAO;
import com.commafeed.backend.dao.FeedEntryDAO.EntryWithFeed;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
@@ -17,6 +18,7 @@ import com.commafeed.backend.model.FeedEntry;
import com.commafeed.backend.model.FeedEntryContent;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.google.common.collect.Lists;
@Stateless
@@ -34,6 +36,9 @@ public class FeedUpdateService {
@Inject
MetricsBean metricsBean;
@Inject
CacheService cache;
public void updateEntry(Feed feed, FeedEntry entry,
List<FeedSubscription> subscriptions) {
@@ -61,12 +66,16 @@ public class FeedUpdateService {
if (update != null) {
List<FeedEntryStatus> statusUpdateList = Lists.newArrayList();
List<User> users = Lists.newArrayList();
for (FeedSubscription sub : subscriptions) {
FeedEntryStatus status = new FeedEntryStatus();
status.setEntry(update);
status.setSubscription(sub);
statusUpdateList.add(status);
users.add(sub.getUser());
}
cache.invalidateRootCategory(users.toArray(new User[0]));
feedEntryDAO.saveOrUpdate(update);
feedEntryStatusDAO.saveOrUpdate(statusUpdateList);
metricsBean.entryUpdated(statusUpdateList.size());

View File

@@ -23,12 +23,14 @@ import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.frontend.SecurityCheck;
@@ -71,6 +73,9 @@ public class CategoryREST extends AbstractResourceREST {
@Inject
FeedSubscriptionDAO feedSubscriptionDAO;
@Inject
CacheService cache;
@Path("/entries")
@GET
@ApiOperation(value = "Get category entries", notes = "Get a list of category entries", responseClass = "com.commafeed.frontend.model.Entries")
@@ -226,7 +231,7 @@ public class CategoryREST extends AbstractResourceREST {
feedEntryStatusDAO.markCategoryEntries(getUser(), categories,
olderThan);
}
cache.invalidateRootCategory(getUser());
return Response.ok(Status.OK).build();
}
@@ -249,6 +254,7 @@ public class CategoryREST extends AbstractResourceREST {
cat.setParent(parent);
}
feedCategoryDAO.saveOrUpdate(cat);
cache.invalidateRootCategory(getUser());
return Response.ok().build();
}
@@ -279,6 +285,7 @@ public class CategoryREST extends AbstractResourceREST {
feedCategoryDAO.saveOrUpdate(categories);
feedCategoryDAO.delete(cat);
cache.invalidateRootCategory(getUser());
return Response.ok().build();
} else {
return Response.status(Status.NOT_FOUND).build();
@@ -343,7 +350,7 @@ public class CategoryREST extends AbstractResourceREST {
}
feedCategoryDAO.saveOrUpdate(category);
cache.invalidateRootCategory(getUser());
return Response.ok(Status.OK).build();
}
@@ -361,7 +368,7 @@ public class CategoryREST extends AbstractResourceREST {
}
category.setCollapsed(req.isCollapse());
feedCategoryDAO.saveOrUpdate(category);
cache.invalidateRootCategory(getUser());
return Response.ok(Status.OK).build();
}
@@ -382,18 +389,23 @@ public class CategoryREST extends AbstractResourceREST {
@Path("/get")
@ApiOperation(value = "Get feed categories", notes = "Get all categories and subscriptions of the user", responseClass = "com.commafeed.frontend.model.Category")
public Response getSubscriptions() {
User user = getUser();
List<FeedCategory> categories = feedCategoryDAO.findAll(getUser());
List<FeedSubscription> subscriptions = feedSubscriptionDAO
.findAll(getUser());
Map<Long, Long> unreadCount = feedEntryStatusDAO
.getUnreadCount(getUser());
Category root = buildCategory(null, categories, subscriptions,
unreadCount);
root.setId("all");
root.setName("All");
Category root = cache.getRootCategory(user);
if (root == null) {
log.debug("root category cache miss for {}", user.getName());
List<FeedCategory> categories = feedCategoryDAO.findAll(user);
List<FeedSubscription> subscriptions = feedSubscriptionDAO
.findAll(getUser());
Map<Long, Long> unreadCount = feedEntryStatusDAO
.getUnreadCount(getUser());
root = buildCategory(null, categories, subscriptions,
unreadCount);
root.setId("all");
root.setName("All");
cache.setRootCategory(user, root);
}
return Response.ok(root).build();
}

View File

@@ -13,6 +13,7 @@ import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.services.FeedEntryService;
@@ -36,6 +37,9 @@ public class EntryREST extends AbstractResourceREST {
@Inject
FeedEntryStatusDAO feedEntryStatusDAO;
@Inject
CacheService cache;
@Path("/mark")
@POST
@ApiOperation(value = "Mark a feed entry", notes = "Mark a feed entry as read/unread")
@@ -47,7 +51,7 @@ public class EntryREST extends AbstractResourceREST {
feedEntryService.markEntry(getUser(), Long.valueOf(req.getId()),
req.getFeedId(), req.isRead());
cache.invalidateRootCategory(getUser());
return Response.ok(Status.OK).build();
}

View File

@@ -37,6 +37,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
@@ -112,6 +113,9 @@ public class FeedREST extends AbstractResourceREST {
@Inject
OPMLExporter opmlExporter;
@Inject
CacheService cache;
@Context
private HttpServletRequest request;
@@ -288,7 +292,7 @@ public class FeedREST extends AbstractResourceREST {
if (subscription != null) {
feedEntryStatusDAO.markSubscriptionEntries(subscription, olderThan);
}
cache.invalidateRootCategory(getUser());
return Response.ok(Status.OK).build();
}
@@ -375,6 +379,7 @@ public class FeedREST extends AbstractResourceREST {
.entity("Failed to subscribe to URL " + url + ": "
+ e.getMessage()).build();
}
cache.invalidateRootCategory(getUser());
return Response.ok(Status.OK).build();
}
@@ -419,6 +424,7 @@ public class FeedREST extends AbstractResourceREST {
req.getId());
if (sub != null) {
feedSubscriptionDAO.delete(sub);
cache.invalidateRootCategory(getUser());
return Response.ok(Status.OK).build();
} else {
return Response.status(Status.NOT_FOUND).build();
@@ -478,7 +484,7 @@ public class FeedREST extends AbstractResourceREST {
} else {
feedSubscriptionDAO.saveOrUpdate(subscription);
}
cache.invalidateRootCategory(getUser());
return Response.ok(Status.OK).build();
}
@@ -517,6 +523,7 @@ public class FeedREST extends AbstractResourceREST {
.status(Status.INTERNAL_SERVER_ERROR)
.entity(e.getMessage()).build());
}
cache.invalidateRootCategory(getUser());
return Response.temporaryRedirect(
URI.create(applicationSettingsService.get().getPublicUrl()))
.build();

View File

@@ -12,6 +12,7 @@ import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.StartupBean;
import com.commafeed.backend.cache.CacheService;
import com.commafeed.backend.dao.UserDAO;
import com.commafeed.backend.dao.UserRoleDAO;
import com.commafeed.backend.dao.UserSettingsDAO;
@@ -56,6 +57,9 @@ public class UserREST extends AbstractResourceREST {
@Inject
PasswordEncryptionService encryptionService;
@Inject
CacheService cache;
@Path("/settings")
@GET
@ApiOperation(value = "Retrieve user settings", notes = "Retrieve user settings", responseClass = "com.commafeed.frontend.model.Settings")
@@ -194,6 +198,7 @@ public class UserREST extends AbstractResourceREST {
return Response.status(Status.FORBIDDEN).build();
}
userService.unregister(getUser());
cache.invalidateRootCategory(getUser());
return Response.ok().build();
}
}