Skip to content

Commit 434b2d0

Browse files
committed
varnish-frontend invalidation logic
1 parent f77f2be commit 434b2d0

File tree

7 files changed

+79
-35
lines changed

7 files changed

+79
-35
lines changed

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ configs:
285285
286286
acl local {
287287
"localhost";
288+
"linkeddatahub";
288289
}
289290
290291
acl remote {

http-tests/document-hierarchy/GET-children.sh

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ add-agent-to-group.sh \
1515
--agent "$AGENT_URI" \
1616
"${ADMIN_BASE_URL}acl/groups/writers/"
1717

18+
# execute SPARQL query to retrieve children of the end-user base URL to prime the Varnish cache
19+
20+
query="DESCRIBE * WHERE { SELECT DISTINCT ?child ?thing WHERE { GRAPH ?childGraph { { ?child <http://rdfs.org/sioc/ns#has_parent> <${END_USER_BASE_URL}>. } UNION { ?child <http://rdfs.org/sioc/ns#has_container> <${END_USER_BASE_URL}>. } ?child <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ?Type. OPTIONAL { ?child <http://purl.org/dc/terms/title> ?title. } OPTIONAL { ?child <http://xmlns.com/foaf/0.1/primaryTopic> ?thing. } } } ORDER BY (?title) LIMIT 20 }"
21+
22+
# URL-encode query with uppercase hex digits (matching Java's UriComponent.encode())
23+
# Note: We must construct the URL manually instead of using curl's -G --data-urlencode because curl normalizes percent-encoding to lowercase,
24+
# which won't match the uppercase percent-encoding that Java produces in cache invalidation BAN requests
25+
encoded_query=$(python -c "import urllib.parse; print(urllib.parse.quote('''$query''', safe=''))")
26+
27+
curl -k -f -s \
28+
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
29+
-H "Accept: application/n-triples" \
30+
"${END_USER_BASE_URL}sparql?query=$encoded_query" \
31+
> /dev/null
32+
1833
# create container
1934

2035
slug="test-children-query"
@@ -27,14 +42,10 @@ container=$(create-container.sh \
2742
--slug "$slug" \
2843
--parent "$END_USER_BASE_URL")
2944

30-
# execute SPARQL query to retrieve children of the end-user base URL
31-
32-
query="DESCRIBE * WHERE { SELECT DISTINCT ?child ?thing WHERE { GRAPH ?childGraph { { ?child <http://rdfs.org/sioc/ns#has_parent> <${END_USER_BASE_URL}>. } UNION { ?child <http://rdfs.org/sioc/ns#has_container> <${END_USER_BASE_URL}>. } ?child <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ?Type. OPTIONAL { ?child <http://purl.org/dc/terms/title> ?title. } OPTIONAL { ?child <http://xmlns.com/foaf/0.1/primaryTopic> ?thing. } } } ORDER BY (?title) LIMIT 20 }"
45+
# execute SPARQL query again - the new container should appear (verifies cache invalidation)
3346

3447
curl -k -f -s \
35-
-G \
3648
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
37-
-H 'Accept: application/n-triples' \
38-
--data-urlencode "query=$query" \
39-
"${END_USER_BASE_URL}sparql" \
49+
-H "Accept: application/n-triples" \
50+
"${END_USER_BASE_URL}sparql?query=$encoded_query" \
4051
| grep -q "<${container}>"

src/main/java/com/atomgraph/linkeddatahub/Application.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
import com.atomgraph.linkeddatahub.server.filter.request.auth.ProxiedWebIDFilter;
107107
import com.atomgraph.linkeddatahub.server.filter.response.CORSFilter;
108108
import com.atomgraph.linkeddatahub.server.filter.response.ResponseHeadersFilter;
109-
import com.atomgraph.linkeddatahub.server.filter.response.BackendInvalidationFilter;
109+
import com.atomgraph.linkeddatahub.server.filter.response.CacheInvalidationFilter;
110110
import com.atomgraph.linkeddatahub.server.filter.response.XsltExecutableFilter;
111111
import com.atomgraph.linkeddatahub.server.interceptor.RDFPostMediaTypeInterceptor;
112112
import com.atomgraph.linkeddatahub.server.mapper.auth.oauth2.TokenExpiredExceptionMapper;
@@ -1033,7 +1033,7 @@ protected void registerContainerResponseFilters()
10331033
register(new CORSFilter());
10341034
register(new ResponseHeadersFilter());
10351035
register(new XsltExecutableFilter());
1036-
if (isInvalidateCache()) register(new BackendInvalidationFilter());
1036+
if (isInvalidateCache()) register(new CacheInvalidationFilter());
10371037
// register(new ProvenanceFilter());
10381038
}
10391039

src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import com.atomgraph.linkeddatahub.client.LinkedDataClient;
2222
import com.atomgraph.linkeddatahub.imports.QueryLoader;
2323
import com.atomgraph.linkeddatahub.model.Service;
24-
import com.atomgraph.linkeddatahub.server.filter.response.BackendInvalidationFilter;
24+
import com.atomgraph.linkeddatahub.server.filter.response.CacheInvalidationFilter;
2525
import com.atomgraph.linkeddatahub.server.model.impl.GraphStoreImpl;
2626
import com.atomgraph.linkeddatahub.server.security.AgentContext;
2727
import com.atomgraph.linkeddatahub.server.util.Skolemizer;
@@ -237,7 +237,7 @@ public Response ban(Resource proxy, String url)
237237
if (url == null) throw new IllegalArgumentException("Resource cannot be null");
238238

239239
return getSystem().getClient().target(proxy.getURI()).request().
240-
header(BackendInvalidationFilter.HEADER_NAME, UriComponent.encode(url, UriComponent.Type.UNRESERVED)). // the value has to be URL-encoded in order to match request URLs in Varnish
240+
header(CacheInvalidationFilter.HEADER_NAME, UriComponent.encode(url, UriComponent.Type.UNRESERVED)). // the value has to be URL-encoded in order to match request URLs in Varnish
241241
method("BAN", Response.class);
242242
}
243243

src/main/java/com/atomgraph/linkeddatahub/resource/admin/Clear.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import com.atomgraph.linkeddatahub.apps.model.AdminApplication;
2020
import com.atomgraph.linkeddatahub.apps.model.EndUserApplication;
2121
import static com.atomgraph.linkeddatahub.server.filter.request.OntologyFilter.addDocumentModel;
22-
import com.atomgraph.linkeddatahub.server.filter.response.BackendInvalidationFilter;
22+
import com.atomgraph.linkeddatahub.server.filter.response.CacheInvalidationFilter;
2323
import com.atomgraph.linkeddatahub.server.util.OntologyModelGetter;
2424
import java.net.URI;
2525
import jakarta.inject.Inject;
@@ -131,7 +131,7 @@ public Response ban(Resource proxy, String url)
131131
if (url == null) throw new IllegalArgumentException("Resource cannot be null");
132132

133133
return getSystem().getClient().target(proxy.getURI()).request().
134-
header(BackendInvalidationFilter.HEADER_NAME, UriComponent.encode(url, UriComponent.Type.UNRESERVED)). // the value has to be URL-encoded in order to match request URLs in Varnish
134+
header(CacheInvalidationFilter.HEADER_NAME, UriComponent.encode(url, UriComponent.Type.UNRESERVED)). // the value has to be URL-encoded in order to match request URLs in Varnish
135135
method("BAN", Response.class);
136136
}
137137

src/main/java/com/atomgraph/linkeddatahub/resource/oauth2/google/Login.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import static com.atomgraph.linkeddatahub.resource.admin.SignUp.AGENT_PATH;
2626
import static com.atomgraph.linkeddatahub.resource.admin.SignUp.AUTHORIZATION_PATH;
2727
import com.atomgraph.linkeddatahub.server.filter.request.auth.IDTokenFilter;
28-
import com.atomgraph.linkeddatahub.server.filter.response.BackendInvalidationFilter;
28+
import com.atomgraph.linkeddatahub.server.filter.response.CacheInvalidationFilter;
2929
import com.atomgraph.linkeddatahub.server.util.MessageBuilder;
3030
import com.atomgraph.linkeddatahub.server.util.Skolemizer;
3131
import com.atomgraph.linkeddatahub.vocabulary.ACL;
@@ -477,7 +477,7 @@ public Response ban(Resource proxy, String url)
477477
if (url == null) throw new IllegalArgumentException("Resource cannot be null");
478478

479479
return getSystem().getClient().target(proxy.getURI()).request().
480-
header(BackendInvalidationFilter.HEADER_NAME, UriComponent.encode(url, UriComponent.Type.UNRESERVED)). // the value has to be URL-encoded in order to match request URLs in Varnish
480+
header(CacheInvalidationFilter.HEADER_NAME, UriComponent.encode(url, UriComponent.Type.UNRESERVED)). // the value has to be URL-encoded in order to match request URLs in Varnish
481481
method("BAN", Response.class);
482482
}
483483

src/main/java/com/atomgraph/linkeddatahub/server/filter/response/BackendInvalidationFilter.java renamed to src/main/java/com/atomgraph/linkeddatahub/server/filter/response/CacheInvalidationFilter.java

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import jakarta.ws.rs.core.HttpHeaders;
3333
import jakarta.ws.rs.core.Response;
3434
import java.util.Optional;
35+
import java.util.Set;
3536
import org.apache.jena.rdf.model.Resource;
3637
import org.glassfish.jersey.uri.UriComponent;
3738

@@ -42,7 +43,7 @@
4243
* @author Martynas Jusevičius {@literal <[email protected]>}
4344
*/
4445
@Priority(Priorities.USER + 400)
45-
public class BackendInvalidationFilter implements ContainerResponseFilter
46+
public class CacheInvalidationFilter implements ContainerResponseFilter
4647
{
4748

4849
/**
@@ -59,58 +60,89 @@ public void filter(ContainerRequestContext req, ContainerResponseContext resp) t
5960
// If no application was matched (e.g., non-existent dataspace), skip cache invalidation
6061
if (!getApplication().isPresent()) return;
6162

62-
if (getAdminApplication().getService().getBackendProxy() == null) return;
63-
6463
if (req.getMethod().equals(HttpMethod.POST) && resp.getHeaderString(HttpHeaders.LOCATION) != null)
6564
{
6665
URI location = (URI)resp.getHeaders().get(HttpHeaders.LOCATION).get(0);
6766
URI parentURI = location.resolve("..").normalize();
67+
URI relativeParentURI = getApplication().get().getBaseURI().relativize(parentURI);
6868

69-
ban(getApplication().get().getService().getBackendProxy(), location.toString()).close();
69+
banIfNotNull(getApplication().get().getFrontendProxy(), location.toString());
70+
banIfNotNull(getApplication().get().getService().getBackendProxy(), location.toString());
7071
// ban URI from authorization query results
71-
ban(getAdminApplication().getService().getBackendProxy(), location.toString()).close();
72+
banIfNotNull(getAdminApplication().getService().getBackendProxy(), location.toString());
73+
7274
// ban parent resource URI in order to avoid stale children data in containers
73-
ban(getApplication().get().getService().getBackendProxy(), parentURI.toString()).close();
74-
ban(getApplication().get().getService().getBackendProxy(), getApplication().get().getBaseURI().relativize(parentURI).toString()).close(); // URIs can be relative in queries
75+
banIfNotNull(getApplication().get().getFrontendProxy(), parentURI.toString());
76+
banIfNotNull(getApplication().get().getService().getBackendProxy(), parentURI.toString());
77+
78+
if (!relativeParentURI.toString().isEmpty()) // URIs can be relative in queries
79+
{
80+
banIfNotNull(getApplication().get().getFrontendProxy(), relativeParentURI.toString());
81+
banIfNotNull(getApplication().get().getService().getBackendProxy(), relativeParentURI.toString());
82+
}
83+
7584
// ban all results of queries that use forClass type
7685
if (req.getUriInfo().getQueryParameters().containsKey(AC.forClass.getLocalName()))
7786
{
7887
String forClass = req.getUriInfo().getQueryParameters().getFirst(AC.forClass.getLocalName());
79-
ban(getApplication().get().getService().getBackendProxy(), forClass).close();
88+
banIfNotNull(getApplication().get().getFrontendProxy(), forClass);
89+
banIfNotNull(getApplication().get().getService().getBackendProxy(), forClass);
8090
}
8191
}
8292

83-
if (req.getMethod().equals(HttpMethod.POST) || req.getMethod().equals(HttpMethod.PUT) || req.getMethod().equals(HttpMethod.DELETE) || req.getMethod().equals(HttpMethod.PATCH))
93+
if (Set.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.PATCH).contains(req.getMethod()))
8494
{
85-
// ban all admin/ entries when the admin dataset is changed - not perfect, but works
95+
// ban all admin. entries when the admin dataset is changed - not perfect, but works
8696
if (!getAdminApplication().getBaseURI().relativize(req.getUriInfo().getAbsolutePath()).isAbsolute()) // URL is relative to the admin app's base URI
8797
{
88-
ban(getAdminApplication().getService().getBackendProxy(), getAdminApplication().getBaseURI().toString()).close();
89-
ban(getAdminApplication().getService().getBackendProxy(), "foaf:Agent").close(); // queries use prefixed names instead of absolute URIs
90-
ban(getAdminApplication().getService().getBackendProxy(), "acl:AuthenticatedAgent").close();
98+
banIfNotNull(getAdminApplication().getService().getBackendProxy(), getAdminApplication().getBaseURI().toString());
99+
banIfNotNull(getAdminApplication().getService().getBackendProxy(), "foaf:Agent"); // queries use prefixed names instead of absolute URIs
100+
banIfNotNull(getAdminApplication().getService().getBackendProxy(), "acl:AuthenticatedAgent");
91101
}
92-
102+
93103
if (req.getUriInfo().getAbsolutePath().toString().endsWith("/"))
94104
{
95-
ban(getApplication().get().getService().getBackendProxy(), req.getUriInfo().getAbsolutePath().toString()).close();
105+
banIfNotNull(getApplication().get().getFrontendProxy(), req.getUriInfo().getAbsolutePath().toString());
106+
banIfNotNull(getApplication().get().getService().getBackendProxy(), req.getUriInfo().getAbsolutePath().toString());
96107
// ban URI from authorization query results
97-
ban(getAdminApplication().getService().getBackendProxy(), req.getUriInfo().getAbsolutePath().toString()).close();
108+
banIfNotNull(getAdminApplication().getService().getBackendProxy(), req.getUriInfo().getAbsolutePath().toString());
98109

99110
// ban parent document URIs (those that have a trailing slash) in order to avoid stale children data in containers
100111
if (!req.getUriInfo().getAbsolutePath().equals(getApplication().get().getBaseURI()))
101112
{
102113
URI parentURI = req.getUriInfo().getAbsolutePath().resolve("..").normalize();
114+
URI relativeParentURI = getApplication().get().getBaseURI().relativize(parentURI);
115+
116+
// ban parent resource URI in order to avoid stale children data in containers
117+
banIfNotNull(getApplication().get().getFrontendProxy(), parentURI.toString());
118+
banIfNotNull(getApplication().get().getService().getBackendProxy(), parentURI.toString());
103119

104-
ban(getApplication().get().getService().getBackendProxy(), parentURI.toString()).close();
105-
ban(getApplication().get().getService().getBackendProxy(), getApplication().get().getBaseURI().relativize(parentURI).toString()).close(); // URIs can be relative in queries
120+
if (!relativeParentURI.toString().isEmpty()) // URIs can be relative in queries
121+
{
122+
banIfNotNull(getApplication().get().getFrontendProxy(), relativeParentURI.toString());
123+
banIfNotNull(getApplication().get().getService().getBackendProxy(), relativeParentURI.toString());
124+
}
106125
}
107126
}
108127
}
109128
}
110129

130+
/**
131+
* Bans URL from proxy cache if proxy is not null.
132+
* Null-safe wrapper that handles the common pattern of banning and closing the response.
133+
*
134+
* @param proxy proxy resource (can be null)
135+
* @param url URL to be banned
136+
*/
137+
public void banIfNotNull(Resource proxy, String url)
138+
{
139+
if (proxy != null)
140+
ban(proxy, url).close();
141+
}
142+
111143
/**
112144
* Bans URL from proxy cache.
113-
*
145+
*
114146
* @param proxy proxy resource
115147
* @param url URL to be banned
116148
* @return response from proxy
@@ -119,7 +151,7 @@ public Response ban(Resource proxy, String url)
119151
{
120152
if (proxy == null) throw new IllegalArgumentException("Proxy resource cannot be null");
121153
if (url == null) throw new IllegalArgumentException("Resource cannot be null");
122-
154+
123155
return getClient().target(proxy.getURI()).request().
124156
header(HEADER_NAME, UriComponent.encode(url, UriComponent.Type.UNRESERVED)). // the value has to be URL-encoded in order to match request URLs in Varnish
125157
method("BAN", Response.class);

0 commit comments

Comments
 (0)