Web Script Framework enhancements: ReadOnly transactions, Buffered Responses

- additional 'allow' attribute on <transaction> element in web script descriptor
   - values: readonly, readwrite (default)
   - readonly means that the whole web script executes in read transaction
   - readonly lighter weight; no flushing, no cache checks/updates
- transaction aware web script response buffers
   - only commits to response when trx is committed
   - fixes ALFCOM-2497 - CMIS: createFolder & immediately add document can fail
   - also means errors half-way thru response result in clean response with error contents only
   - readonly transactions are not buffered
- WebScript RepoStore now uses ReadOnly transaction for gets
- CMIS getter Web Scripts set to ReadOnly transaction
- Fix up Web Script pattern that checks for WebScriptServletResponse using instanceof
   - no longer the case, as it may be wrapped in BufferedResponse
   - use getRuntime() instanceof WebScriptServletRuntime and/or
   - WebScriptServletRuntime.getHttpServletResponse/Request(WebScriptReponse r) - returns null, if none

Tests:
- Run CMIS Tests
- Run CMIS BulkCreateSystemTest (now working)
- Run Share

Suggestion:
- Update your 'read' web script descriptors to include <transaction allow="readonly">. This will improve repo performance significantly.

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@14670 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
David Caruana
2009-06-11 18:25:59 +00:00
parent 63bc21f5c5
commit 565c57d893
23 changed files with 291 additions and 53 deletions

View File

@@ -243,7 +243,7 @@ public class RepoStore implements Store, TenantDeployer
"Web Script Store " + repoStore.toString() + repoPath + " must exist; it was not found");
}
}
});
}, true, false);
}
}, AuthenticationUtil.getSystemUserName());
@@ -364,7 +364,7 @@ public class RepoStore implements Store, TenantDeployer
return documentPaths != null ? documentPaths.toArray(new String[documentPaths.size()]) : new String[0];
}
});
}, true, false);
}
}, AuthenticationUtil.getSystemUserName());
}
@@ -423,7 +423,7 @@ public class RepoStore implements Store, TenantDeployer
}
return documentPaths.toArray(new String[documentPaths.size()]);
}
});
}, true, false);
}
}, AuthenticationUtil.getSystemUserName());
}
@@ -499,7 +499,7 @@ public class RepoStore implements Store, TenantDeployer
return documentPaths.toArray(new String[documentPaths.size()]);
}
});
}, true, false);
}
}, AuthenticationUtil.getSystemUserName());
}
@@ -521,7 +521,7 @@ public class RepoStore implements Store, TenantDeployer
findNodeRef(documentPath), ContentModel.PROP_CONTENT);
return reader.getLastModified();
}
});
}, true, false);
}
}, AuthenticationUtil.getSystemUserName());
}
@@ -542,7 +542,7 @@ public class RepoStore implements Store, TenantDeployer
NodeRef nodeRef = findNodeRef(documentPath);
return (nodeRef != null);
}
});
}, true, false);
}
}, AuthenticationUtil.getSystemUserName());
}
@@ -573,7 +573,7 @@ public class RepoStore implements Store, TenantDeployer
}
return reader.getContentInputStream();
}
});
}, true, false);
}
}, AuthenticationUtil.getSystemUserName());
}
@@ -867,9 +867,9 @@ public class RepoStore implements Store, TenantDeployer
}
return location;
}
});
}, true, false);
}
}, AuthenticationUtil.getSystemUserName());
}, AuthenticationUtil.getSystemUserName());
}
}

View File

@@ -24,7 +24,12 @@
*/
package org.alfresco.repo.web.scripts;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
@@ -32,6 +37,7 @@ import javax.servlet.http.HttpServletResponse;
import javax.transaction.Status;
import javax.transaction.UserTransaction;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.model.Repository;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
@@ -40,6 +46,7 @@ import org.alfresco.repo.tenant.TenantAdminService;
import org.alfresco.repo.tenant.TenantDeployer;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.TransactionListener;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.TemplateService;
@@ -47,15 +54,19 @@ import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.descriptor.DescriptorService;
import org.alfresco.web.scripts.AbstractRuntimeContainer;
import org.alfresco.web.scripts.Authenticator;
import org.alfresco.web.scripts.Cache;
import org.alfresco.web.scripts.Description;
import org.alfresco.web.scripts.Registry;
import org.alfresco.web.scripts.Runtime;
import org.alfresco.web.scripts.ServerModel;
import org.alfresco.web.scripts.WebScript;
import org.alfresco.web.scripts.WebScriptException;
import org.alfresco.web.scripts.WebScriptRequest;
import org.alfresco.web.scripts.WebScriptResponse;
import org.alfresco.web.scripts.WrappingWebScriptResponse;
import org.alfresco.web.scripts.Description.RequiredAuthentication;
import org.alfresco.web.scripts.Description.RequiredTransaction;
import org.alfresco.web.scripts.Description.TransactionCapability;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectFactory;
@@ -71,6 +82,9 @@ public class RepositoryContainer extends AbstractRuntimeContainer implements Ten
// Logger
protected static final Log logger = LogFactory.getLog(RepositoryContainer.class);
// Transaction key for buffered response
private static String BUFFERED_RESPONSE_KEY = RepositoryContainer.class.getName() + ".bufferedresponse";
/** Component Dependencies */
private Repository repository;
private RepositoryImageResolver imageResolver;
@@ -92,7 +106,8 @@ public class RepositoryContainer extends AbstractRuntimeContainer implements Ten
/**
* @param registryFactory
*/
public void setRegistryFactory(ObjectFactory registryFactory) {
public void setRegistryFactory(ObjectFactory registryFactory)
{
this.registryFactory = registryFactory;
}
@@ -314,9 +329,22 @@ public class RepositoryContainer extends AbstractRuntimeContainer implements Ten
try
{
if (logger.isDebugEnabled())
logger.debug("Begin retry transaction block: " + description.getRequiredTransaction());
logger.debug("Begin retry transaction block: " + description.getRequiredTransaction() + "," + description.getTransactionCapability());
WebScriptResponse redirectedRes = scriptRes;
if (description.getTransactionCapability() == TransactionCapability.readwrite)
{
if (logger.isDebugEnabled())
logger.debug("Creating Transactional Response for ReadWrite transaction");
// create buffered response that's sensitive transaction boundary
BufferedResponse bufferedRes = new BufferedResponse(scriptRes);
AlfrescoTransactionSupport.bindResource(BUFFERED_RESPONSE_KEY, bufferedRes);
AlfrescoTransactionSupport.bindListener(bufferedRes);
redirectedRes = bufferedRes;
}
script.execute(scriptReq, scriptRes);
script.execute(scriptReq, redirectedRes);
}
catch(Exception e)
{
@@ -356,21 +384,16 @@ public class RepositoryContainer extends AbstractRuntimeContainer implements Ten
finally
{
if (logger.isDebugEnabled())
logger.debug("End retry transaction block: " + description.getRequiredTransaction());
logger.debug("End retry transaction block: " + description.getRequiredTransaction() + "," + description.getTransactionCapability());
}
return null;
}
};
if (description.getRequiredTransaction() == RequiredTransaction.required)
{
retryingTransactionHelper.doInTransaction(work);
}
else
{
retryingTransactionHelper.doInTransaction(work, false, true);
}
boolean readonly = description.getTransactionCapability() == TransactionCapability.readonly;
boolean requiresNew = description.getRequiredTransaction() == RequiredTransaction.requiresnew;
retryingTransactionHelper.doInTransaction(work, readonly, requiresNew);
}
}
@@ -465,4 +488,213 @@ public class RepositoryContainer extends AbstractRuntimeContainer implements Ten
{
webScriptsRegistryCache.remove(tenantAdminService.getCurrentUserDomain());
}
/**
* Transactional Buffered Response
*/
private static class BufferedResponse implements TransactionListener, WrappingWebScriptResponse
{
private WebScriptResponse res;
private ByteArrayOutputStream outputStream;
private OutputStreamWriter outputWriter;
/**
* Construct
*
* @param res
*/
public BufferedResponse(WebScriptResponse res)
{
this.res = res;
this.outputStream = new ByteArrayOutputStream(4096);
try
{
this.outputWriter = new OutputStreamWriter(outputStream, "UTF-8");
}
catch (UnsupportedEncodingException e)
{
throw new AlfrescoRuntimeException("Failed to create buffered response", e);
};
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WrappingWebScriptResponse#getNext()
*/
public WebScriptResponse getNext()
{
return res;
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WebScriptResponse#addHeader(java.lang.String, java.lang.String)
*/
public void addHeader(String name, String value)
{
res.addHeader(name, value);
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WebScriptResponse#encodeScriptUrl(java.lang.String)
*/
public String encodeScriptUrl(String url)
{
return res.encodeScriptUrl(url);
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WebScriptResponse#getEncodeScriptUrlFunction(java.lang.String)
*/
public String getEncodeScriptUrlFunction(String name)
{
return res.getEncodeScriptUrlFunction(name);
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WebScriptResponse#getOutputStream()
*/
public OutputStream getOutputStream() throws IOException
{
return outputStream;
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WebScriptResponse#getRuntime()
*/
public Runtime getRuntime()
{
return res.getRuntime();
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WebScriptResponse#getWriter()
*/
public Writer getWriter() throws IOException
{
return outputWriter;
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WebScriptResponse#reset()
*/
public void reset()
{
outputStream.reset();
res.reset();
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WebScriptResponse#setCache(org.alfresco.web.scripts.Cache)
*/
public void setCache(Cache cache)
{
res.setCache(cache);
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WebScriptResponse#setContentType(java.lang.String)
*/
public void setContentType(String contentType)
{
res.setContentType(contentType);
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WebScriptResponse#setContentEncoding(java.lang.String)
*/
public void setContentEncoding(String contentEncoding)
{
res.setContentEncoding(contentEncoding);
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WebScriptResponse#setHeader(java.lang.String, java.lang.String)
*/
public void setHeader(String name, String value)
{
res.setHeader(name, value);
}
/*
* (non-Javadoc)
* @see org.alfresco.web.scripts.WebScriptResponse#setStatus(int)
*/
public void setStatus(int status)
{
res.setStatus(status);
}
/*
* (non-Javadoc)
* @see org.alfresco.repo.transaction.TransactionListener#afterCommit()
*/
public void afterCommit()
{
writeResponse();
}
/*
* (non-Javadoc)
* @see org.alfresco.repo.transaction.TransactionListener#afterRollback()
*/
public void afterRollback()
{
writeResponse();
}
/*
* (non-Javadoc)
* @see org.alfresco.repo.transaction.TransactionListener#beforeCommit(boolean)
*/
public void beforeCommit(boolean readOnly)
{
}
/*
* (non-Javadoc)
* @see org.alfresco.repo.transaction.TransactionListener#beforeCompletion()
*/
public void beforeCompletion()
{
}
/*
* (non-Javadoc)
* @see org.alfresco.repo.transaction.TransactionListener#flush()
*/
public void flush()
{
}
/**
* Write buffered response to underlying response
*/
private void writeResponse()
{
try
{
if (logger.isDebugEnabled())
logger.debug("Writing Transactional response: size=" + outputStream.size());
outputStream.flush();
outputStream.writeTo(res.getOutputStream());
}
catch (IOException e)
{
throw new AlfrescoRuntimeException("Failed to commit buffered response", e);
}
}
}
}

View File

@@ -46,7 +46,6 @@ import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.web.scripts.Status;
import org.alfresco.web.scripts.WebScriptException;
import org.alfresco.web.scripts.WebScriptResponse;
import org.alfresco.web.scripts.servlet.WebScriptServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -167,11 +166,10 @@ public class AVMRemoteStore extends BaseRemoteStore
}
// set mimetype for the content and the character encoding + length for the stream
WebScriptServletResponse httpRes = (WebScriptServletResponse)res;
httpRes.setContentType(mimetype);
httpRes.getHttpServletResponse().setCharacterEncoding(reader.getEncoding());
httpRes.getHttpServletResponse().setDateHeader("Last-Modified", desc.getModDate());
httpRes.setHeader("Content-Length", Long.toString(reader.getSize()));
res.setContentType(mimetype);
res.setContentEncoding(reader.getEncoding());
res.setHeader("Last-Modified", Long.toString(desc.getModDate()));
res.setHeader("Content-Length", Long.toString(reader.getSize()));
if (logger.isDebugEnabled())
logger.debug("AVMRemoteStore.getDocument() " + mimetype + " of size: " + reader.getSize());

View File

@@ -47,7 +47,7 @@ import org.alfresco.web.scripts.WebScriptException;
import org.alfresco.web.scripts.WebScriptRequest;
import org.alfresco.web.scripts.WebScriptResponse;
import org.alfresco.web.scripts.servlet.HTTPProxy;
import org.alfresco.web.scripts.servlet.WebScriptServletResponse;
import org.alfresco.web.scripts.servlet.WebScriptServletRuntime;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Attribute;
@@ -166,11 +166,12 @@ public class SearchProxy extends AbstractWebScript implements InitializingBean
// issue request against search engine
// NOTE: This web script must be executed in a HTTP servlet environment
if (!(res instanceof WebScriptServletResponse))
if (!(res.getRuntime() instanceof WebScriptServletRuntime))
{
throw new WebScriptException("Search Proxy must be executed in HTTP Servlet environment");
}
HttpServletResponse servletRes = ((WebScriptServletResponse)res).getHttpServletResponse();
HttpServletResponse servletRes = WebScriptServletRuntime.getHttpServletResponse(res);
SearchEngineHttpProxy proxy = new SearchEngineHttpProxy(req.getServicePath() + "/" + req.getContextPath(), engine, engineUrl, servletRes);
proxy.service();
}

View File

@@ -35,7 +35,6 @@ import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.alfresco.model.ContentModel;
@@ -58,8 +57,6 @@ import org.alfresco.web.scripts.WebScriptException;
import org.alfresco.web.scripts.WebScriptRequest;
import org.alfresco.web.scripts.WebScriptResponse;
import org.alfresco.web.scripts.WebScriptStatus;
import org.alfresco.web.scripts.servlet.WebScriptServletRequest;
import org.alfresco.web.scripts.servlet.WebScriptServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.FileCopyUtils;
@@ -298,14 +295,6 @@ public class StreamContent extends AbstractWebScript
protected void streamContent(WebScriptRequest req, WebScriptResponse res, NodeRef nodeRef, QName propertyQName, boolean attach)
throws IOException
{
// NOTE: This web script must be executed in a HTTP Servlet environment
if (!(req instanceof WebScriptServletRequest))
{
throw new WebScriptException("Content retrieval must be executed in HTTP Servlet environment");
}
HttpServletRequest httpReq = ((WebScriptServletRequest)req).getHttpServletRequest();
HttpServletResponse httpRes = ((WebScriptServletResponse)res).getHttpServletResponse();
if (logger.isDebugEnabled())
logger.debug("Retrieving content from node ref " + nodeRef.toString() + " (property: " + propertyQName.toString() + ") (attach: " + attach + ")");
@@ -317,14 +306,19 @@ public class StreamContent extends AbstractWebScript
// check If-Modified-Since header and set Last-Modified header as appropriate
Date modified = (Date)nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED);
long modifiedSince = httpReq.getDateHeader("If-Modified-Since");
String modifiedSinceStr = req.getHeader("If-Modified-Since");
if (modifiedSinceStr == null)
{
modifiedSinceStr = "-1";
}
long modifiedSince = new Long(modifiedSinceStr);
if (modifiedSince > 0L)
{
// round the date to the ignore millisecond value which is not supplied by header
long modDate = (modified.getTime() / 1000L) * 1000L;
if (modDate <= modifiedSince)
{
httpRes.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
}
@@ -337,7 +331,7 @@ public class StreamContent extends AbstractWebScript
}
// Stream the cotent
streamContentImpl(req, res, reader, attach, modified, String.valueOf(modifiedSince));
streamContentImpl(req, res, reader, attach, modified, String.valueOf(modifiedSince));
}
/**
@@ -398,15 +392,12 @@ public class StreamContent extends AbstractWebScript
protected void streamContentImpl(WebScriptRequest req, WebScriptResponse res, ContentReader reader, boolean attach, Date modified, String eTag)
throws IOException
{
HttpServletRequest httpReq = ((WebScriptServletRequest)req).getHttpServletRequest();
HttpServletResponse httpRes = ((WebScriptServletResponse)res).getHttpServletResponse();
// handle attachment
if (attach == true)
{
// set header based on filename - will force a Save As from the browse if it doesn't recognize it
// this is better than the default response of the browser trying to display the contents
httpRes.setHeader("Content-Disposition", "attachment");
res.setHeader("Content-Disposition", "attachment");
}
// establish mimetype
@@ -428,9 +419,9 @@ public class StreamContent extends AbstractWebScript
}
// set mimetype for the content and the character encoding + length for the stream
httpRes.setContentType(mimetype);
httpRes.setCharacterEncoding(reader.getEncoding());
httpRes.setHeader("Content-Length", Long.toString(reader.getSize()));
res.setContentType(mimetype);
res.setContentEncoding(reader.getEncoding());
res.setHeader("Content-Length", Long.toString(reader.getSize()));
// set caching
Cache cache = new Cache();