2 Commits

Author SHA1 Message Date
kimscott
00513acf31 재고량 수적 2020-04-09 11:16:54 +09:00
kimscott
4c7c3d3345 add saga rollback 2020-02-10 13:34:44 +09:00
10 changed files with 146 additions and 128 deletions

View File

@@ -37,7 +37,7 @@ stages:
pool:
vmImage: $(vmImageName)
steps:
- task: CacheBeta@1
- task: Cache@2
inputs:
key: 'maven | "$(Agent.OS)" | **/pom.xml'
restoreKeys: |
@@ -48,11 +48,24 @@ stages:
- task: Maven@3
inputs:
mavenPomFile: 'pom.xml'
options: '-Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)'
options: '-Dmaven.test.skip=true -Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)'
javaHomeOption: 'JDKVersion'
jdkVersionOption: '1.8'
jdkArchitectureOption: 'x64'
goals: 'package'
- task: CopyFiles@2
inputs:
SourceFolder: '$(system.defaultworkingdirectory)'
Contents: |
**/*.jar
Dockerfile
kubernetes/*
TargetFolder: '$(build.artifactstagingdirectory)'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'artifact'
publishLocation: 'Container'
- task: Docker@2
inputs:
containerRegistry: $(containerRegistryDockerConnection)

View File

@@ -1,99 +0,0 @@
version: 0.2
env:
variables:
_PROJECT_NAME: "products"
phases:
install:
runtime-versions:
java: openjdk8
docker: 18
commands:
- echo install kubectl
- curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
- chmod +x ./kubectl
- mv ./kubectl /usr/local/bin/kubectl
pre_build:
commands:
- echo Logging in to Amazon ECR...
- echo $_PROJECT_NAME
- echo $AWS_ACCOUNT_ID
- echo $AWS_DEFAULT_REGION
- echo $CODEBUILD_RESOLVED_SOURCE_VERSION
- echo start command
- $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- mvn package -B
- docker build -t $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$_PROJECT_NAME:$CODEBUILD_RESOLVED_SOURCE_VERSION .
post_build:
commands:
- echo Pushing the Docker image...
- docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$_PROJECT_NAME:$CODEBUILD_RESOLVED_SOURCE_VERSION
- echo connect kubectl
- kubectl config set-cluster k8s --server="$KUBE_URL" --insecure-skip-tls-verify=true
- kubectl config set-credentials admin --token="$KUBE_TOKEN"
- kubectl config set-context default --cluster=k8s --user=admin
- kubectl config use-context default
- |
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: $_PROJECT_NAME
labels:
app: $_PROJECT_NAME
spec:
ports:
- port: 8080
targetPort: 8080
selector:
app: $_PROJECT_NAME
EOF
- |
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: $_PROJECT_NAME
labels:
app: $_PROJECT_NAME
spec:
replicas: 1
selector:
matchLabels:
app: $_PROJECT_NAME
template:
metadata:
labels:
app: $_PROJECT_NAME
spec:
containers:
- name: $_PROJECT_NAME
image: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$_PROJECT_NAME:$CODEBUILD_RESOLVED_SOURCE_VERSION
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 10
timeoutSeconds: 2
periodSeconds: 5
failureThreshold: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 120
timeoutSeconds: 2
periodSeconds: 5
failureThreshold: 5
EOF
cache:
paths:
- '/root/.m2/**/*'

View File

@@ -11,8 +11,8 @@ steps:
name: 'gcr.io/cloud-builders/mvn'
args: [
'clean',
'package',
'-Dmaven.test.skip=true'
'package'
# '-Dmaven.test.skip=true'
]
# waitFor: ['test']
### docker Build
@@ -102,4 +102,4 @@ options:
env:
# # location/name of GKE cluster (used by all kubectl commands)
- CLOUDSDK_COMPUTE_ZONE=asia-northeast1-a
- CLOUDSDK_CONTAINER_CLUSTER=cluster-1
- CLOUDSDK_CONTAINER_CLUSTER=standard-cluster-1

View File

@@ -1,13 +1,9 @@
apiVersion: apps/v1
apiVersion : apps/v1beta1
kind: Deployment
metadata:
name: product
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: product
template:
metadata:
labels:
@@ -15,6 +11,6 @@ spec:
spec:
containers:
- name: product
image: 979050235289.dkr.ecr.ap-southeast-2.amazonaws.com/12st-product:${BUILD_NUMBER}
image: myeventstormingregistry.azurecr.io/product
ports:
- containerPort: 8080
- containerPort: 8080

View File

@@ -2,7 +2,6 @@ apiVersion: v1
kind: Service
metadata:
name: product
namespace: default
labels:
app: product
spec:
@@ -10,4 +9,4 @@ spec:
- port: 8080
targetPort: 8080
selector:
app: product
app: product

11
pom.xml
View File

@@ -79,6 +79,17 @@
<scope>test</scope>
</dependency>
<!-- hystrix circuit breaker -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<!-- Add Stackdriver Trace Starter -->
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->

View File

@@ -3,11 +3,15 @@ package com.example.template;
import com.example.template.config.kafka.KafkaProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
@EnableBinding(KafkaProcessor.class)
@EnableCircuitBreaker
@EnableHystrixDashboard
public class Application {
protected static ApplicationContext applicationContext;
@@ -24,7 +28,7 @@ public class Application {
product.setImageUrl("https://github.githubassets.com/images/modules/profile/profile-joined-github.png");
product.setName(p);
product.setPrice(i*10000);
product.setStock(i*10);
product.setStock(i*1);
product.setImageUrl("/goods/img/"+p+".jpg");
// 상품 디테일 추가 - 양방향 관계

View File

@@ -1,26 +1,42 @@
package com.example.template;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
public class ProductController {
private static final String RESPONSE_STRING_FORMAT = "product change from '%s': %d";
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
ProductService productService;
private int count = 0;
private static final String HOSTNAME = parseContainerIdFromHostname(
System.getenv().getOrDefault("HOSTNAME", "products"));
static String parseContainerIdFromHostname(String hostname) {
return hostname.replaceAll("products-\\d+-", "");
}
@GetMapping("/product/{productId}")
Product productStockCheck(@PathVariable(value = "productId") Long productId) {
// System.out.println("productStockCheck call");
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println("productStockCheck call");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.productService.getProductById(productId);
}
@@ -31,4 +47,35 @@ public class ProductController {
return this.productService.save(data);
}
@PatchMapping("/product/{productId}")
@HystrixCommand(
fallbackMethod = "certifyFallBack",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000"), // 기본 타임아웃
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"), // 서킷브레이커가 작하게될 에러 퍼센트
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"), // 서킷브레이커가 열리기 위한 최소 요청조 (10초간 19번이 실패해도 서킷은 오픈 안됨)
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000") } // 서킷브래이커가 열렸을때 얼마나 유지할지
)
ResponseEntity<String> fakeProductPatch(@PathVariable(value = "productId") Long productId, @RequestBody String data) {
count++;
logger.info(String.format("product start from %s: %d", HOSTNAME, count));
// try {
// Thread.sleep(900);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
return ResponseEntity.ok(String.format(RESPONSE_STRING_FORMAT, HOSTNAME, count));
}
ResponseEntity<String> certifyFallBack(Long productId, String data){
System.out.println("certifyFallBack");
System.out.println(data);
return ResponseEntity.ok("시스템이 혼잡합니다!! 잠시후 다시 호출해주세요");
}
}

View File

@@ -0,0 +1,36 @@
package com.example.template;
public class ProductOutOfStock extends AbstractEvent {
private String stateMessage = "재고량 바닥";
private Long productId;
private Long orderId;
public ProductOutOfStock(){
super();
}
public String getStateMessage() {
return stateMessage;
}
public void setStateMessage(String stateMessage) {
this.stateMessage = stateMessage;
}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public Long getOrderId() {
return orderId;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
}

View File

@@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Service;
@@ -16,6 +15,9 @@ import java.util.Optional;
@Service
public class ProductService {
@Autowired
ProductRepository productRepository;
@StreamListener(KafkaProcessor.INPUT)
public void onOrderPlaced(@Payload String message) {
System.out.println("##### listener : " + message);
@@ -35,7 +37,17 @@ public class ProductService {
Product product = productOptional.get();
product.setStock(product.getStock() - orderPlaced.getQuantity());
productRepository.save(product);
if( product.getStock() < 0 ){
System.out.println("productOutOfStock 이벤트 발생");
ProductOutOfStock productOutOfStock = new ProductOutOfStock();
productOutOfStock.setProductId(orderPlaced.getProductId());
productOutOfStock.setOrderId(orderPlaced.getOrderId());
productOutOfStock.publish();
}else{
productRepository.save(product);
}
}
@@ -45,9 +57,11 @@ public class ProductService {
if( orderPlaced.getEventType().equals(OrderCancelled.class.getSimpleName())){
Optional<Product> productOptional = productRepository.findById(orderPlaced.getProductId());
Product product = productOptional.get();
product.setStock(product.getStock() + orderPlaced.getQuantity());
productRepository.save(product);
// productOutOfStock 상황이 아닐때만 재고량을 늘린다.
if( product.getStock() - orderPlaced.getQuantity() > 0 ){
product.setStock(product.getStock() + orderPlaced.getQuantity());
productRepository.save(product);
}
}
}catch (Exception e){
@@ -55,9 +69,6 @@ public class ProductService {
}
}
@Autowired
ProductRepository productRepository;
/**
* 상품 조회
*/