Skip to content

Commit

Permalink
Merge branch 'master' into add-e2e-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
munishchouhan authored Jan 21, 2025
2 parents ecf97cc + cc79afe commit ce8c995
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 22 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.16.7
1.16.8
7 changes: 7 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# Wave changelog
1.16.8 - 20 Jab 2025
- Add TraceContextFilter logging context propagation [396c10c2]
- Improve Proxy cache configuration [163e605f]
- Improve logging pattern [0ab87164]
- Improve request caching logic [a95153be]
- Bump MN 4.7.4 [4ce2a139]

1.16.7 - 10 Jan 2025
- Improve logging on pairing websocket error [21861dbe]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Wave, containers provisioning service
* Copyright (c) 2023-2024, Seqera Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.configuration

import java.time.Duration

import groovy.transform.CompileStatic
import groovy.transform.ToString
import io.micronaut.context.annotation.Value
import jakarta.inject.Singleton

/**
* Model {@link io.seqera.wave.proxy.ProxyCache} configuration settings
*
* @author Paolo Di Tommaso <[email protected]>
*/
@Singleton
@CompileStatic
@ToString(includeNames = true, includePackage = false)
class ProxyCacheConfig {

@Value('${wave.proxy-cache.duration:4m}')
private Duration duration

@Value('${wave.proxy-cache.max-size:10000}')
private int maxSize

@Value('${wave.proxy-cache.enabled:true}')
private boolean enabled

Duration getDuration() {
return duration
}

int getMaxSize() {
return maxSize
}

boolean getEnabled() {
return enabled
}
}
35 changes: 26 additions & 9 deletions src/main/groovy/io/seqera/wave/core/RegistryProxyService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ class RegistryProxyService {
'Accept-Encoding',
'Authorization',
'Cache-Control',
'Connection',
'Content-Type',
'Content-Length',
'Content-Range',
Expand All @@ -173,9 +174,12 @@ class RegistryProxyService {
'If-None-Match',
'If-None-Match',
'Etag',
'Host',
'Location',
'Last-Modified',
'User-Agent',
'Range',
'X-Registry-Auth'
]

static protected boolean isCacheableHeader(String key) {
Expand All @@ -196,8 +200,13 @@ class RegistryProxyService {
if( !headers )
headers = Map.of()
for( Map.Entry<String,List<String>> entry : headers ) {
if( !isCacheableHeader(entry.key) )
if( "Cache-Control".equalsIgnoreCase(entry.key) ) {
// ignore caching when cache-control header is provided
return null
}
if( !isCacheableHeader(entry.key) ) {
continue
}
hasher.putUnencodedChars(entry.key)
for( String it : entry.value ) {
if( it )
Expand All @@ -218,14 +227,22 @@ class RegistryProxyService {
}

DelegateResponse handleRequest(RoutePath route, Map<String,List<String>> headers) {
return cache.getOrCompute(
requestKey(route, headers),
(String k)-> {
final resp = handleRequest0(route, headers)
// when the response is not cacheable, return null as TTL
final ttl = route.isDigest() && resp.isCacheable() ? cache.duration : null
return new Tuple2<DelegateResponse, Duration>(resp, ttl)
})
if( !cache.enabled ) {
return handleRequest0(route, headers)
}
final key = requestKey(route, headers)
if( !key ) {
log.debug "Bypass cache for requrst route=${route}; headers=${headers}"
return handleRequest0(route, headers)
}
else {
return cache.getOrCompute(key,(String k)-> {
final resp = handleRequest0(route, headers)
// when the response is not cacheable, return null as TTL
final ttl = route.isDigest() && resp.isCacheable() ? cache.duration : null
return new Tuple2<DelegateResponse, Duration>(resp, ttl)
})
}
}

@TraceElapsedTime(thresholdMillis = '${wave.trace.proxy-service.threshold:1000}')
Expand Down
63 changes: 63 additions & 0 deletions src/main/groovy/io/seqera/wave/filter/TraceContextFilter.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Wave, containers provisioning service
* Copyright (c) 2023-2024, Seqera Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.filter

import java.util.regex.Pattern

import groovy.transform.CompileStatic
import io.micronaut.context.propagation.slf4j.MdcPropagationContext
import io.micronaut.core.propagation.MutablePropagatedContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.annotation.RequestFilter
import io.micronaut.http.annotation.ServerFilter
import org.slf4j.MDC
import static io.micronaut.http.annotation.ServerFilter.MATCH_ALL_PATTERN

/**
* HTTP filter to trace and propagate request metadata in the MDC logging context
*
* @author Paolo Di Tommaso <[email protected]>
*/
@CompileStatic
@ServerFilter(MATCH_ALL_PATTERN)
class TraceContextFilter {

static final Pattern REGEX = ~/^\/v2\/wt\/([a-z0-9]+).*/

@RequestFilter
void requestFilter(HttpRequest<?> request, MutablePropagatedContext mutablePropagatedContext) {
try {
final requestId = getRequestId(request.path)
MDC.put("requestId", requestId)
MDC.put("requestPath", request.path)
MDC.put("requestMethod", request.methodName)
mutablePropagatedContext.add(new MdcPropagationContext())
} finally {
MDC.remove("requestId")
MDC.remove("requestPath")
MDC.remove("requestMethod")
}
}

static String getRequestId(String path) {
final m = REGEX.matcher(path)
return m.matches() ? m.group(1) : null
}

}
22 changes: 13 additions & 9 deletions src/main/groovy/io/seqera/wave/proxy/ProxyCache.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import java.time.Duration

import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.Value
import groovy.util.logging.Slf4j
import io.micronaut.core.annotation.Nullable
import io.seqera.wave.configuration.ProxyCacheConfig
import io.seqera.wave.encoder.MoshiEncodeStrategy
import io.seqera.wave.encoder.MoshiExchange
import io.seqera.wave.store.cache.AbstractTieredCache
Expand All @@ -34,18 +35,17 @@ import jakarta.inject.Singleton
*
* @author Paolo Di Tommaso <[email protected]>
*/
@Slf4j
@Singleton
@CompileStatic
class ProxyCache extends AbstractTieredCache<DelegateResponse> {

@Value('${wave.proxy-cache.duration:30m}')
private Duration duration
private ProxyCacheConfig config

@Value('${wave.proxy-cache.max-size:10000}')
private int maxSize

ProxyCache(@Nullable L2TieredCache l2) {
ProxyCache(@Nullable L2TieredCache l2, ProxyCacheConfig config) {
super(l2, encoder())
this.config = config
log.info "+ Creating Proxy-cache - config=${config}"
}

static MoshiEncodeStrategy encoder() {
Expand All @@ -69,11 +69,15 @@ class ProxyCache extends AbstractTieredCache<DelegateResponse> {

@Override
int getMaxSize() {
return maxSize
return config.maxSize
}

Duration getDuration() {
return duration
return config.duration
}

boolean getEnabled() {
return config.enabled
}

}
4 changes: 2 additions & 2 deletions src/main/resources/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<!-- ansi escape mess-up with non-ansi terminal and loggers -->
<withJansi>false</withJansi>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg >> wt=%X{requestId}%n</pattern>
</encoder>
<!-- <filter class="io.seqera.wave.util.LoggerLevelFilter">-->
<!-- <level>${WAVE_LOG_LEVEL:-INFO}</level>-->
Expand All @@ -43,7 +43,7 @@
<totalSizeCap>200MB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{MMM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</pattern>
<pattern>%d{MMM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n >> wt=%X{requestId}%n</pattern>
</encoder>
</appender>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ class RegistryProxyServiceTest extends Specification {
def k5 = RegistryProxyService.requestKey(p1, ['Content-Type': ['text/1', 'text/2'], 'X-Foo': ['bar']])
then:
k5 == 'f9fe81aed4d72cba' // <-- the header 'X-Foo' does not alter the cache key

when:
def k6 = RegistryProxyService.requestKey(p1, ["cache-control":["none"]])
then:
k6 == null
}

def 'check is cacheable header' () {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Wave, containers provisioning service
* Copyright (c) 2023-2024, Seqera Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.filter

import spock.lang.Specification

/**
*
* @author Paolo Di Tommaso <[email protected]>
*/
class TraceContextFilterTest extends Specification {

def 'should validate request id regex' () {
expect:
TraceContextFilter.getRequestId(PATH) == EXPECTED
where:
PATH | EXPECTED
'/foo/bar' | null
'/v2/wt/1234' | '1234'
'/v2/wt/12ab/x/y/z' | '12ab'
}

}
3 changes: 2 additions & 1 deletion src/test/groovy/io/seqera/wave/proxy/ProxyCacheTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import spock.lang.Specification
import java.time.Duration

import io.micronaut.context.ApplicationContext
import io.seqera.wave.configuration.ProxyCacheConfig
import io.seqera.wave.store.cache.RedisL2TieredCache
import io.seqera.wave.test.RedisTestContainer
/**
Expand Down Expand Up @@ -51,7 +52,7 @@ class ProxyCacheTest extends Specification implements RedisTestContainer {
given:
def TTL = Duration.ofMillis(150)
def store = applicationContext.getBean(RedisL2TieredCache)
def cache = new ProxyCache(store)
def cache = new ProxyCache(store, Mock(ProxyCacheConfig))
and:
def k = UUID.randomUUID().toString()
def resp = new DelegateResponse(
Expand Down

0 comments on commit ce8c995

Please sign in to comment.