Skip to content
This repository has been archived by the owner on Jul 25, 2022. It is now read-only.

Commit

Permalink
Integrate with SCG
Browse files Browse the repository at this point in the history
  • Loading branch information
alek-sys committed Jun 19, 2019
1 parent 7e76836 commit 1f277bd
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 48 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Summary

This is a PoC of using Hazelcast peer-to-peer cluster to implement Spring Cloud Gateway Rate Limiter for horizontally scaled CF app.

# Build and deploy

```
$ ./gradlew assemble
$ cf push --var cf-apps-domain=<your cf env domain>
$ ./configure-network.sh
```
# Test

```
$ curl -vv -k https://peer.<your cf env domain>/google
# should return 403 - no header
$ curl -vv -k https://peer.<your cf env domain>/google -H "X-API-Key: foo"
# response from Google (404) + X-Remaining-Limit header
```
13 changes: 11 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@ plugins {
apply plugin: 'io.spring.dependency-management'

group = 'caching'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
mavenCentral()
}

ext {
set('springCloudVersion', "Greenwich.SR1")
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'com.hazelcast:hazelcast:3.12'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
3 changes: 3 additions & 0 deletions configure-network.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

cf add-network-policy peer --destination-app=peer --protocol=tcp --port=5701
10 changes: 10 additions & 0 deletions manifest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
applications:
- name: peer
path: build/libs/scg-rate-limiter.jar
instances: 2
routes:
- route: peer.apps.internal
- route: peer.((cf-apps-domain))
buildpacks:
- java_buildpack_offline
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ pluginManagement {
gradlePluginPortal()
}
}
rootProject.name = 'hz-demo'
rootProject.name = 'scg-rate-limiter'
43 changes: 0 additions & 43 deletions src/main/java/caching/hzdemo/HzDemoApplication.java

This file was deleted.

114 changes: 114 additions & 0 deletions src/main/java/scg/ratelimiter/DemoApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package scg.ratelimiter;

import com.hazelcast.config.Config;
import com.hazelcast.config.JoinConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.filter.ratelimit.AbstractRateLimiter;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.validation.Validator;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;

class HazelcastRateLimiterConfig {

private int limit;

public int getLimit() {
return limit;
}

public void setLimit(int limit) {
this.limit = limit;
}
}

@Component
class HazelcastRateLimiter extends AbstractRateLimiter<HazelcastRateLimiterConfig> {

private final Logger logger = LoggerFactory.getLogger(HazelcastRateLimiter.class);

private IMap<String, Integer> map;
private AtomicBoolean initialized = new AtomicBoolean(false);

protected HazelcastRateLimiter(Validator defaultValidator) {
super(HazelcastRateLimiterConfig.class, "hazelcast-rate-limiter", defaultValidator);
}

// super hacky - but DNS records X.peer.apps.internal will only be available shortly after application started
// so InitializingBean or any other option to start cluster during app startup won't work
@Scheduled(initialDelay = 3000, fixedDelay = Integer.MAX_VALUE)
public void run() {
String groupName = "my-test-group";

Config cfg = new Config();
cfg.getGroupConfig().setName(groupName);

JoinConfig joinConfig = cfg.getNetworkConfig().getJoin();
joinConfig.getMulticastConfig().setEnabled(false);
joinConfig.getTcpIpConfig().setEnabled(true)
.addMember("0.peer.apps.internal")
.addMember("1.peer.apps.internal");

HazelcastInstance instance = Hazelcast.newHazelcastInstance(cfg);
map = instance.getMap("rate-limit");
initialized.set(true);
}


@Override
public Mono<Response> isAllowed(String routeId, String id) {
final Response notAllowed = new Response(false, Collections.emptyMap());

if (!initialized.get()) {
logger.info("Cluster is not yet initialized");
return Mono.just(notAllowed);
}

Integer noRequests = map.getOrDefault(id, 0) + 1;
HazelcastRateLimiterConfig config = getConfig().getOrDefault(routeId, new HazelcastRateLimiterConfig());

logger.info("Current limit is {}, total requests to date {}", config.getLimit(), noRequests);

if (noRequests >= config.getLimit()) {
logger.info("Exceeded number of allowed requests");
return Mono.just(notAllowed);
}

map.put(id, noRequests);
final int remainingRequests = config.getLimit() - noRequests;

return Mono.just(new Response(true, Collections.singletonMap("X-Remaining-Limit", String.valueOf(remainingRequests))));
}
}

@Component
class HeaderKeyResolver implements KeyResolver {

@Override
public Mono<String> resolve(ServerWebExchange exchange) {
String apiKey = exchange.getRequest().getHeaders().getFirst("X-API-Key");
return apiKey == null ? Mono.empty() : Mono.just(apiKey);
}
}

@SpringBootApplication
@EnableScheduling
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}
1 change: 0 additions & 1 deletion src/main/resources/application.properties

This file was deleted.

14 changes: 14 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
spring:
cloud:
gateway:
routes:
- id: github
uri: https://google.com
predicates:
- Path=/google
filters:
- name: RequestRateLimiter
args:
rate-limiter: "#{@hazelcastRateLimiter}"
key-resolver: "#{@headerKeyResolver}"
hazelcast-rate-limiter.limit: 3
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

@RunWith(SpringRunner.class)
@SpringBootTest
public class HzDemoApplicationTests {
public class DemoApplicationTests {

@Test
public void contextLoads() {
Expand Down

0 comments on commit 1f277bd

Please sign in to comment.