├── frontend ├── .gitignore ├── src │ └── main │ │ ├── fabric8 │ │ └── deployment.yaml │ │ ├── java │ │ └── io │ │ │ └── openshift │ │ │ └── booster │ │ │ └── messaging │ │ │ ├── Request.java │ │ │ ├── Response.java │ │ │ ├── Data.java │ │ │ ├── WorkerUpdate.java │ │ │ └── Frontend.java │ │ └── resources │ │ └── webroot │ │ ├── index.html │ │ ├── app.js │ │ ├── app.css │ │ └── gesso.js ├── pom.xml └── .openshiftio │ ├── application.yaml │ └── service.yaml ├── images └── CopyLogin.png ├── worker ├── .gitignore ├── integration-tests │ ├── src │ │ └── test │ │ │ ├── resources │ │ │ ├── arquillian.xml │ │ │ └── amq.yaml │ │ │ └── java │ │ │ └── io │ │ │ └── openshift │ │ │ └── booster │ │ │ └── OpenShiftIT.java │ └── pom.xml ├── src │ └── main │ │ ├── fabric8 │ │ └── deployment.yaml │ │ └── java │ │ └── io │ │ └── openshift │ │ └── booster │ │ └── messaging │ │ └── Worker.java ├── pom.xml ├── .openshiftio │ └── application.yaml ├── service.yaml └── LICENSE ├── scripts ├── exportEnvVars.sh ├── setup-gcp-all ├── setup-azure-all ├── setup-aws-ic-only ├── setup-gcp-ic-only ├── setup-azure-ic-only ├── setup-onprem-ic-only ├── workload.yaml ├── setup-aws-all ├── setup-onprem-all ├── qdrouterd.conf.public_cloud ├── ic.aws.yaml ├── ic.gcp.yaml ├── ic.azure.yaml ├── ic.onprem.yaml ├── generate-certs └── qdrouterd.conf.onprem ├── pom.xml ├── controller ├── package.json ├── package-lock.json └── index.html ├── client ├── Dockerfile └── client.py ├── server ├── Dockerfile └── service.py ├── messaging-load ├── Dockerfile ├── load.py └── local_load.py ├── .gitignore ├── README.md └── LICENSE /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vertx 3 | -------------------------------------------------------------------------------- /images/CopyLogin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burrsutter/demo2-amq/HEAD/images/CopyLogin.png -------------------------------------------------------------------------------- /worker/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vertx 3 | /bin 4 | .settings 5 | .project 6 | .classpath 7 | .DS_Store -------------------------------------------------------------------------------- /scripts/exportEnvVars.sh: -------------------------------------------------------------------------------- 1 | export ONPREM_SUFFIX=apps.gcp.burrsutter.dev 2 | export AWS_SUFFIX=apps.aws.burrsutter.org 3 | #export GCP_SUFFIX=apps.gcp.burrsutter.dev 4 | export AZURE_SUFFIX=apps.azr.burrsutter.net 5 | echo $ONPREM_SUFFIX 6 | echo $AWS_SUFFIX 7 | # echo $GCP_SUFFIX 8 | echo $AZURE_SUFFIX 9 | -------------------------------------------------------------------------------- /scripts/setup-gcp-all: -------------------------------------------------------------------------------- 1 | ./setup-gcp-ic-only 2 | oc apply -f workload.yaml 3 | oc apply -f ../worker/.openshiftio/application.yaml 4 | oc new-app --template=vertx-messaging-worker \ 5 | -p SOURCE_REPOSITORY_URL=https://github.com/burrsutter/demo2-amq \ 6 | -p SOURCE_REPOSITORY_REF=master \ 7 | -p SOURCE_REPOSITORY_DIR=worker 8 | 9 | -------------------------------------------------------------------------------- /scripts/setup-azure-all: -------------------------------------------------------------------------------- 1 | ./setup-azure-ic-only 2 | oc apply -f workload.yaml 3 | oc apply -f ../worker/.openshiftio/application.yaml 4 | oc new-app --template=vertx-messaging-worker \ 5 | -p SOURCE_REPOSITORY_URL=https://github.com/burrsutter/demo2-amq \ 6 | -p SOURCE_REPOSITORY_REF=master \ 7 | -p SOURCE_REPOSITORY_DIR=worker 8 | 9 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.example 5 | mylib 6 | 1.0 7 | 8 | pom 9 | 10 | 11 | frontend 12 | worker 13 | 14 | 15 | -------------------------------------------------------------------------------- /controller/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "controller", 3 | "version": "1.0.0", 4 | "description": "Controller for the AMQ workload in Demo-2", 5 | "main": "index.html", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "rhea": "^0.2.11" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/setup-aws-ic-only: -------------------------------------------------------------------------------- 1 | oc new-project demo2-amq 2 | oc create secret generic qdr-internal-cert --from-file=tls.crt=tls-certs/tls.aws.crt --from-file=tls.key=tls-certs/tls.aws.key --from-file=ca.crt=tls-certs/ca.crt --from-file=tls.pw=tls-certs/tls.aws.pw 3 | oc create configmap qdr-config --from-file=qdrouterd.conf.template=qdrouterd.conf.public_cloud --dry-run -o yaml | oc apply -f - 4 | cat ic.aws.yaml | envsubst | oc apply -f - 5 | -------------------------------------------------------------------------------- /scripts/setup-gcp-ic-only: -------------------------------------------------------------------------------- 1 | oc new-project demo2-amq 2 | oc create secret generic qdr-internal-cert --from-file=tls.crt=tls-certs/tls.gcp.crt --from-file=tls.key=tls-certs/tls.gcp.key --from-file=ca.crt=tls-certs/ca.crt --from-file=tls.pw=tls-certs/tls.gcp.pw 3 | oc create configmap qdr-config --from-file=qdrouterd.conf.template=qdrouterd.conf.public_cloud --dry-run -o yaml | oc apply -f - 4 | cat ic.gcp.yaml | envsubst | oc apply -f - 5 | -------------------------------------------------------------------------------- /scripts/setup-azure-ic-only: -------------------------------------------------------------------------------- 1 | oc new-project demo2-amq 2 | oc create secret generic qdr-internal-cert --from-file=tls.crt=tls-certs/tls.azure.crt --from-file=tls.key=tls-certs/tls.azure.key --from-file=ca.crt=tls-certs/ca.crt --from-file=tls.pw=tls-certs/tls.azure.pw 3 | oc create configmap qdr-config --from-file=qdrouterd.conf.template=qdrouterd.conf.public_cloud --dry-run -o yaml | oc apply -f - 4 | cat ic.azure.yaml | envsubst | oc apply -f - 5 | -------------------------------------------------------------------------------- /scripts/setup-onprem-ic-only: -------------------------------------------------------------------------------- 1 | oc new-project demo2-amq 2 | oc create secret generic qdr-internal-cert --from-file=tls.crt=tls-certs/tls.onprem.crt --from-file=tls.key=tls-certs/tls.onprem.key --from-file=ca.crt=tls-certs/ca.crt --from-file=tls.pw=tls-certs/tls.onprem.pw 3 | oc create configmap qdr-config --from-file=qdrouterd.conf.template=qdrouterd.conf.onprem --dry-run -o yaml | oc apply -f - 4 | cat ic.onprem.yaml | envsubst | oc apply -f - 5 | -------------------------------------------------------------------------------- /scripts/workload.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: workload-generator 5 | spec: 6 | replicas: 0 7 | template: 8 | metadata: 9 | labels: 10 | app: workload-generator 11 | version: v1 12 | type: server 13 | spec: 14 | containers: 15 | - name: client-load 16 | image: tedross/demo2-messaging-load:1.0 17 | imagePullPolicy: IfNotPresent 18 | -------------------------------------------------------------------------------- /worker/integration-tests/src/test/resources/arquillian.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | true 7 | true 8 | false 9 | 10 | 11 | -------------------------------------------------------------------------------- /scripts/setup-aws-all: -------------------------------------------------------------------------------- 1 | ./setup-aws-ic-only 2 | oc apply -f workload.yaml 3 | oc apply -f ../frontend/.openshiftio/application.yaml 4 | oc new-app --template=vertx-messaging-frontend \ 5 | -p SOURCE_REPOSITORY_URL=https://github.com/burrsutter/demo2-amq \ 6 | -p SOURCE_REPOSITORY_REF=master \ 7 | -p SOURCE_REPOSITORY_DIR=frontend 8 | oc apply -f ../worker/.openshiftio/application.yaml 9 | oc new-app --template=vertx-messaging-worker \ 10 | -p SOURCE_REPOSITORY_URL=https://github.com/burrsutter/demo2-amq \ 11 | -p SOURCE_REPOSITORY_REF=master \ 12 | -p SOURCE_REPOSITORY_DIR=worker 13 | 14 | -------------------------------------------------------------------------------- /scripts/setup-onprem-all: -------------------------------------------------------------------------------- 1 | ./setup-onprem-ic-only 2 | oc apply -f workload.yaml 3 | oc apply -f ../frontend/.openshiftio/application.yaml 4 | # oc new-app --template=vertx-messaging-frontend \ 5 | # -p SOURCE_REPOSITORY_URL=https://github.com/burrsutter/demo2-amq \ 6 | # -p SOURCE_REPOSITORY_REF=master \ 7 | # -p SOURCE_REPOSITORY_DIR=frontend 8 | oc apply -f ../worker/.openshiftio/application.yaml 9 | oc new-app --template=vertx-messaging-worker \ 10 | -p SOURCE_REPOSITORY_URL=https://github.com/burrsutter/demo2-amq \ 11 | -p SOURCE_REPOSITORY_REF=master \ 12 | -p SOURCE_REPOSITORY_DIR=worker 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/main/fabric8/deployment.yaml: -------------------------------------------------------------------------------- 1 | spec: 2 | template: 3 | spec: 4 | containers: 5 | - env: 6 | - name: MESSAGING_SERVICE_HOST 7 | value: messaging 8 | - name: MESSAGING_SERVICE_PORT 9 | value: 5672 10 | readinessProbe: 11 | httpGet: 12 | path: /health 13 | port: 8080 14 | scheme: HTTP 15 | initialDelaySeconds: 10 16 | livenessProbe: 17 | httpGet: 18 | path: /health 19 | port: 8080 20 | scheme: HTTP 21 | initialDelaySeconds: 180 22 | 23 | -------------------------------------------------------------------------------- /worker/src/main/fabric8/deployment.yaml: -------------------------------------------------------------------------------- 1 | spec: 2 | template: 3 | spec: 4 | containers: 5 | - env: 6 | - name: MESSAGING_SERVICE_HOST 7 | value: work-queue-broker-amq-amqp 8 | - name: MESSAGING_SERVICE_PORT 9 | value: 5672 10 | - name: AMQ_LOCATION_KEY 11 | value: AWS 12 | readinessProbe: 13 | httpGet: 14 | path: / 15 | port: 8080 16 | scheme: HTTP 17 | initialDelaySeconds: 10 18 | livenessProbe: 19 | httpGet: 20 | path: / 21 | port: 8080 22 | scheme: HTTP 23 | initialDelaySeconds: 180 24 | -------------------------------------------------------------------------------- /controller/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "controller", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "debug": { 8 | "version": "3.1.0", 9 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 10 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 11 | "requires": { 12 | "ms": "2.0.0" 13 | } 14 | }, 15 | "ms": { 16 | "version": "2.0.0", 17 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 18 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 19 | }, 20 | "rhea": { 21 | "version": "0.2.11", 22 | "resolved": "https://registry.npmjs.org/rhea/-/rhea-0.2.11.tgz", 23 | "integrity": "sha1-g/gkgfxGHh44+AvKXu56umQWaek=", 24 | "requires": { 25 | "debug": "3.1.0" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | ## 2 | ## Licensed to the Apache Software Foundation (ASF) under one 3 | ## or more contributor license agreements. See the NOTICE file 4 | ## distributed with this work for additional information 5 | ## regarding copyright ownership. The ASF licenses this file 6 | ## to you under the Apache License, Version 2.0 (the 7 | ## "License"); you may not use this file except in compliance 8 | ## with the License. You may obtain a copy of the License at 9 | ## 10 | ## http://www.apache.org/licenses/LICENSE-2.0 11 | ## 12 | ## Unless required by applicable law or agreed to in writing, 13 | ## software distributed under the License is distributed on an 14 | ## "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | ## KIND, either express or implied. See the License for the 16 | ## specific language governing permissions and limitations 17 | ## under the License. 18 | ## 19 | 20 | FROM tedross/demo-base 21 | MAINTAINER Ted Ross 22 | ADD client.py /client.py 23 | CMD python /client.py 24 | 25 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | ## 2 | ## Licensed to the Apache Software Foundation (ASF) under one 3 | ## or more contributor license agreements. See the NOTICE file 4 | ## distributed with this work for additional information 5 | ## regarding copyright ownership. The ASF licenses this file 6 | ## to you under the Apache License, Version 2.0 (the 7 | ## "License"); you may not use this file except in compliance 8 | ## with the License. You may obtain a copy of the License at 9 | ## 10 | ## http://www.apache.org/licenses/LICENSE-2.0 11 | ## 12 | ## Unless required by applicable law or agreed to in writing, 13 | ## software distributed under the License is distributed on an 14 | ## "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | ## KIND, either express or implied. See the License for the 16 | ## specific language governing permissions and limitations 17 | ## under the License. 18 | ## 19 | 20 | FROM tedross/demo-base 21 | MAINTAINER Ted Ross 22 | ADD service.py /service.py 23 | CMD python /service.py 24 | 25 | -------------------------------------------------------------------------------- /messaging-load/Dockerfile: -------------------------------------------------------------------------------- 1 | ## 2 | ## Licensed to the Apache Software Foundation (ASF) under one 3 | ## or more contributor license agreements. See the NOTICE file 4 | ## distributed with this work for additional information 5 | ## regarding copyright ownership. The ASF licenses this file 6 | ## to you under the Apache License, Version 2.0 (the 7 | ## "License"); you may not use this file except in compliance 8 | ## with the License. You may obtain a copy of the License at 9 | ## 10 | ## http://www.apache.org/licenses/LICENSE-2.0 11 | ## 12 | ## Unless required by applicable law or agreed to in writing, 13 | ## software distributed under the License is distributed on an 14 | ## "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | ## KIND, either express or implied. See the License for the 16 | ## specific language governing permissions and limitations 17 | ## under the License. 18 | ## 19 | 20 | FROM tedross/demo-base 21 | MAINTAINER Ted Ross 22 | ADD local_load.py /local_load.py 23 | CMD python /local_load.py 24 | 25 | -------------------------------------------------------------------------------- /scripts/qdrouterd.conf.public_cloud: -------------------------------------------------------------------------------- 1 | router { 2 | mode: interior 3 | id: ${AMQ_LOCATION_KEY} 4 | } 5 | 6 | listener { 7 | host: 0.0.0.0 8 | port: amqp 9 | authenticatePeer: no 10 | saslMechanisms: ANONYMOUS 11 | } 12 | 13 | listener { 14 | host: 0.0.0.0 15 | port: 55672 16 | role: inter-router 17 | authenticatePeer: yes 18 | sslProfile: ssl_internal_details 19 | saslMechanisms: EXTERNAL 20 | } 21 | 22 | sslProfile { 23 | name: ssl_internal_details 24 | certFile: /etc/qpid-dispatch-certs/internal/tls.crt 25 | privateKeyFile: /etc/qpid-dispatch-certs/internal/tls.key 26 | passwordFile: /etc/qpid-dispatch-certs/internal/tls.pw 27 | caCertFile: /etc/qpid-dispatch-certs/internal/ca.crt 28 | } 29 | 30 | address { 31 | prefix: closest 32 | distribution: closest 33 | } 34 | 35 | address { 36 | prefix: multicast 37 | distribution: multicast 38 | } 39 | 40 | address { 41 | prefix: unicast 42 | distribution: closest 43 | } 44 | 45 | address { 46 | prefix: exclusive 47 | distribution: closest 48 | } 49 | 50 | address { 51 | prefix: broadcast 52 | distribution: multicast 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/main/java/io/openshift/booster/messaging/Request.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Red Hat, Inc, and individual contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.openshift.booster.messaging; 18 | 19 | public class Request { 20 | private String text; 21 | private boolean uppercase; 22 | private boolean reverse; 23 | 24 | public String getText() { 25 | return text; 26 | } 27 | 28 | public void setText(String text) { 29 | this.text = text; 30 | } 31 | 32 | public boolean isUppercase() { 33 | return uppercase; 34 | } 35 | 36 | public boolean isReverse() { 37 | return reverse; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return String.format("Request{text=%s, uppercase=%s, reverse=%s}", 43 | text, uppercase, reverse); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/main/java/io/openshift/booster/messaging/Response.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Red Hat, Inc, and individual contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.openshift.booster.messaging; 18 | 19 | public class Response { 20 | private final String requestId; 21 | private final String workerId; 22 | private final String cloudId; 23 | private final String text; 24 | 25 | public Response(String requestId, String workerId, String cloudId, String text) { 26 | this.requestId = requestId; 27 | this.workerId = workerId; 28 | this.cloudId = cloudId; 29 | this.text = text; 30 | } 31 | 32 | public String getRequestId() { 33 | return requestId; 34 | } 35 | 36 | public String getWorkerId() { 37 | return workerId; 38 | } 39 | 40 | public String getText() { 41 | return text; 42 | } 43 | 44 | public String getCloudId() { 45 | return cloudId; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return String.format("Response{requestId=%s, workerId=%s, cloudId=%s, text=%s}", 51 | requestId, workerId, cloudId, text); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /scripts/ic.aws.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: messaging 5 | labels: 6 | app: amq-interconnect 7 | annotations: 8 | service.alpha.openshift.io/serving-cert-secret-name: qdr-external-cert 9 | spec: 10 | ports: 11 | - port: 5672 12 | name: amqp 13 | - port: 55672 14 | name: inter-router 15 | selector: 16 | type: qdrouterd 17 | --- 18 | apiVersion: extensions/v1beta1 19 | kind: Deployment 20 | metadata: 21 | name: amq-interconnect 22 | spec: 23 | replicas: 1 24 | template: 25 | metadata: 26 | labels: 27 | app: amq-interconnect 28 | version: v1 29 | type: qdrouterd 30 | spec: 31 | containers: 32 | - name: amq-interconnect 33 | env: 34 | - name: AMQ_LOCATION_KEY 35 | value: AWS 36 | image: tedross/qpid-dispatch-router:1.2.0 37 | imagePullPolicy: Always 38 | ports: 39 | - containerPort: 5672 40 | protocol: TCP 41 | volumeMounts: 42 | - name: qdr-config 43 | mountPath: /etc/qpid-dispatch 44 | - name: internal-certs 45 | readOnly: true 46 | mountPath: /etc/qpid-dispatch-certs/internal 47 | volumes: 48 | - name: qdr-config 49 | configMap: 50 | name: qdr-config 51 | - name: internal-certs 52 | secret: 53 | secretName: qdr-internal-cert 54 | --- 55 | apiVersion: v1 56 | kind: Route 57 | metadata: 58 | labels: 59 | app: amq-interconnect 60 | name: inter-router 61 | spec: 62 | host: inter-router-demo2-amq.${AWS_SUFFIX} 63 | port: 64 | targetPort: inter-router 65 | tls: 66 | termination: passthrough 67 | to: 68 | kind: Service 69 | name: messaging 70 | weight: 100 71 | wildcardPolicy: None 72 | -------------------------------------------------------------------------------- /scripts/ic.gcp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: messaging 5 | labels: 6 | app: amq-interconnect 7 | annotations: 8 | service.alpha.openshift.io/serving-cert-secret-name: qdr-external-cert 9 | spec: 10 | ports: 11 | - port: 5672 12 | name: amqp 13 | - port: 55672 14 | name: inter-router 15 | selector: 16 | type: qdrouterd 17 | --- 18 | apiVersion: extensions/v1beta1 19 | kind: Deployment 20 | metadata: 21 | name: amq-interconnect 22 | spec: 23 | replicas: 1 24 | template: 25 | metadata: 26 | labels: 27 | app: amq-interconnect 28 | version: v1 29 | type: qdrouterd 30 | spec: 31 | containers: 32 | - name: amq-interconnect 33 | env: 34 | - name: AMQ_LOCATION_KEY 35 | value: GCP 36 | image: tedross/qpid-dispatch-router:1.2.0 37 | imagePullPolicy: Always 38 | ports: 39 | - containerPort: 5672 40 | protocol: TCP 41 | volumeMounts: 42 | - name: qdr-config 43 | mountPath: /etc/qpid-dispatch 44 | - name: internal-certs 45 | readOnly: true 46 | mountPath: /etc/qpid-dispatch-certs/internal 47 | volumes: 48 | - name: qdr-config 49 | configMap: 50 | name: qdr-config 51 | - name: internal-certs 52 | secret: 53 | secretName: qdr-internal-cert 54 | --- 55 | apiVersion: v1 56 | kind: Route 57 | metadata: 58 | labels: 59 | app: amq-interconnect 60 | name: inter-router 61 | spec: 62 | host: inter-router-demo2-amq.${GCP_SUFFIX} 63 | port: 64 | targetPort: inter-router 65 | tls: 66 | termination: passthrough 67 | to: 68 | kind: Service 69 | name: messaging 70 | weight: 100 71 | wildcardPolicy: None 72 | -------------------------------------------------------------------------------- /scripts/ic.azure.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: messaging 5 | labels: 6 | app: amq-interconnect 7 | annotations: 8 | service.alpha.openshift.io/serving-cert-secret-name: qdr-external-cert 9 | spec: 10 | ports: 11 | - port: 5672 12 | name: amqp 13 | - port: 55672 14 | name: inter-router 15 | selector: 16 | type: qdrouterd 17 | --- 18 | apiVersion: extensions/v1beta1 19 | kind: Deployment 20 | metadata: 21 | name: amq-interconnect 22 | spec: 23 | replicas: 1 24 | template: 25 | metadata: 26 | labels: 27 | app: amq-interconnect 28 | version: v1 29 | type: qdrouterd 30 | spec: 31 | containers: 32 | - name: amq-interconnect 33 | env: 34 | - name: AMQ_LOCATION_KEY 35 | value: AZR 36 | image: tedross/qpid-dispatch-router:1.2.0 37 | imagePullPolicy: Always 38 | ports: 39 | - containerPort: 5672 40 | protocol: TCP 41 | volumeMounts: 42 | - name: qdr-config 43 | mountPath: /etc/qpid-dispatch 44 | - name: internal-certs 45 | readOnly: true 46 | mountPath: /etc/qpid-dispatch-certs/internal 47 | volumes: 48 | - name: qdr-config 49 | configMap: 50 | name: qdr-config 51 | - name: internal-certs 52 | secret: 53 | secretName: qdr-internal-cert 54 | --- 55 | apiVersion: v1 56 | kind: Route 57 | metadata: 58 | labels: 59 | app: amq-interconnect 60 | name: inter-router 61 | spec: 62 | host: inter-router-demo2-amq.${AZURE_SUFFIX} 63 | port: 64 | targetPort: inter-router 65 | tls: 66 | termination: passthrough 67 | to: 68 | kind: Service 69 | name: messaging 70 | weight: 100 71 | wildcardPolicy: None 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | controller/node_modules/ 104 | 105 | # Generated TLS certificates and keys 106 | scripts/tls-certs 107 | 108 | /target 109 | /.vertx 110 | /bin 111 | .settings 112 | .project 113 | .classpath 114 | .DS_Store 115 | .vscode 116 | /scripts/demo -------------------------------------------------------------------------------- /frontend/src/main/java/io/openshift/booster/messaging/Data.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package io.openshift.booster.messaging; 19 | 20 | import java.util.Map; 21 | import java.util.Queue; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | import java.util.concurrent.ConcurrentLinkedQueue; 24 | 25 | public class Data { 26 | private final Queue requestIds; 27 | private final Map responses; 28 | private final Map workers; 29 | 30 | public Data() { 31 | this.requestIds = new ConcurrentLinkedQueue<>(); 32 | this.responses = new ConcurrentHashMap<>(); 33 | this.workers = new ConcurrentHashMap<>(); 34 | } 35 | 36 | public Queue getRequestIds() { 37 | return requestIds; 38 | } 39 | 40 | public Map getResponses() { 41 | return responses; 42 | } 43 | 44 | public Map getWorkers() { 45 | return workers; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return String.format("Data{requestIds=%s, responses=%s, workers=%s}", 51 | requestIds, responses, workers); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/main/java/io/openshift/booster/messaging/WorkerUpdate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Red Hat, Inc, and individual contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.openshift.booster.messaging; 18 | 19 | public class WorkerUpdate { 20 | private final String workerId; 21 | private final String cloud; 22 | private final long timestamp; 23 | private final long requestsProcessed; 24 | private final long processingErrors; 25 | 26 | public WorkerUpdate(String workerId, String cloud, long timestamp, long requestsProcessed, 27 | long processingErrors) { 28 | this.workerId = workerId; 29 | this.cloud = cloud; 30 | this.timestamp = timestamp; 31 | this.requestsProcessed = requestsProcessed; 32 | this.processingErrors = processingErrors; 33 | } 34 | 35 | public String getWorkerId() { 36 | return workerId; 37 | } 38 | 39 | public String getCloud() { 40 | return cloud; 41 | } 42 | 43 | public long getTimestamp() { 44 | return timestamp; 45 | } 46 | 47 | public long getRequestsProcessed() { 48 | return requestsProcessed; 49 | } 50 | 51 | public long getProcessingErrors() { 52 | return processingErrors; 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return String.format("WorkerUpdate{workerId=%s, cloud=%s, timestamp=%s, requestsProcessed=%s, processingErrors=%s}", 58 | workerId, cloud, timestamp, requestsProcessed, processingErrors); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /scripts/ic.onprem.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: messaging 5 | labels: 6 | app: amq-interconnect 7 | annotations: 8 | service.alpha.openshift.io/serving-cert-secret-name: qdr-external-cert 9 | spec: 10 | ports: 11 | - port: 5671 12 | name: amqps 13 | - port: 5672 14 | name: amqp 15 | - port: 8672 16 | name: console 17 | selector: 18 | type: qdrouterd 19 | --- 20 | apiVersion: extensions/v1beta1 21 | kind: Deployment 22 | metadata: 23 | name: amq-interconnect 24 | spec: 25 | replicas: 1 26 | template: 27 | metadata: 28 | labels: 29 | app: amq-interconnect 30 | version: v1 31 | type: qdrouterd 32 | spec: 33 | containers: 34 | - name: amq-interconnect 35 | env: 36 | - name: AMQ_LOCATION_KEY 37 | value: BURR 38 | - name: ONPREM_SUFFIX 39 | value: ${ONPREM_SUFFIX} 40 | - name: AWS_SUFFIX 41 | value: ${AWS_SUFFIX} 42 | - name: GCP_SUFFIX 43 | value: ${GCP_SUFFIX} 44 | - name: AZURE_SUFFIX 45 | value: ${AZURE_SUFFIX} 46 | image: tedross/qpid-dispatch-router:1.2.0 47 | imagePullPolicy: IfNotPresent 48 | ports: 49 | - containerPort: 5672 50 | protocol: TCP 51 | volumeMounts: 52 | - name: qdr-config 53 | mountPath: /etc/qpid-dispatch 54 | - name: internal-certs 55 | readOnly: true 56 | mountPath: /etc/qpid-dispatch-certs/internal 57 | volumes: 58 | - name: qdr-config 59 | configMap: 60 | name: qdr-config 61 | - name: internal-certs 62 | secret: 63 | secretName: qdr-internal-cert 64 | --- 65 | apiVersion: v1 66 | kind: Route 67 | metadata: 68 | name: amqps 69 | spec: 70 | host: amqps-demo2-amq.apps.${ONPREM_SUFFIX} 71 | port: 72 | targetPort: amqps 73 | tls: 74 | termination: passthrough 75 | to: 76 | kind: Service 77 | name: messaging 78 | weight: 100 79 | wildcardPolicy: None 80 | --- 81 | apiVersion: v1 82 | kind: Route 83 | metadata: 84 | name: console 85 | spec: 86 | host: console-demo2-amq.apps.${ONPREM_SUFFIX} 87 | port: 88 | targetPort: console 89 | to: 90 | kind: Service 91 | name: messaging 92 | weight: 100 93 | wildcardPolicy: None 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # demo2-amq 2 | Workload, Tools, and scripts for the AMQ section of Demo-2. 3 | 4 | This demonstration shows how to deploy a distributed application into multiple OpenShift clusters 5 | in both on-premises and public-cloud data centers. 6 | 7 | The locations used in this demonstration are: 8 | - ONPREM - An on-premises cluster. This may be your laptop, if you wish. 9 | - AWS - A cluster hosted on Amazon Web Services 10 | - GCP - A cluster hosted on Google Cloud Platform 11 | - AZURE - A cluster hosted on Azure 12 | 13 | ## Setting up the Demo 14 | 15 | The first time you set up this demo, you must generate the TLS 16 | certificate authority, certificates, and keys that will be used to secure the 17 | inter-cluster connections. 18 | 19 | Note that this script uses `openssl` to generate keys and certificates. 20 | 21 | ``` 22 | $ cd scripts 23 | $ ./generate-certs 24 | ``` 25 | 26 | You must create environment variables in your working environment to identify the 27 | route suffixes of each of the OpenShift clusters. The route suffixes will likely 28 | be in the form ".nip.io". 29 | 30 | The environment variables are: 31 | - ONPREM_SUFFIX 32 | - AWS_SUFFIX 33 | - GCP_SUFFIX 34 | - AZURE_SUFFIX 35 | 36 | Please note that this demonstration may be run without all of the public cloud clusters 37 | in place. There must be at least one public cloud cluster and the on-premises cluster 38 | for the demonstration to be meaningful. 39 | 40 | You can now deploy the various footprints by logging into OpenShift for 41 | each cluster and running the `setup-*-all` script. Use the `Copy Login 42 | Command` feature on the OpenShift Web UI to obtain the correct login command. 43 | 44 |
45 | 46 |
47 | 48 | ``` 49 | $ oc login 50 | $ ./setup-aws-all 51 | 52 | $ oc login 53 | $ ./setup-azure-all 54 | 55 | $ oc login 56 | $ ./setup-gcp-all 57 | 58 | $ oc login 59 | $ ./setup-onprem-all 60 | ``` 61 | 62 | Please note that you may use `setup-*-ic-only` to deploy only the AMQ 63 | Interconnect network without the services for this demo. This might be useful 64 | if you wish to use the network for a different distributed service deployment. 65 | 66 | Once the projects are running on their respective clusters, you 67 | may access the AMQ Interconnect Console using the "console" route configured on the 68 | ONPREM cluster. 69 | 70 | -------------------------------------------------------------------------------- /scripts/generate-certs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License 19 | # 20 | 21 | # Creates a root CA and creates password protected server and client certificates using openssl commands 22 | 23 | D=./tls-certs 24 | 25 | rm -rf $D 26 | mkdir $D 27 | 28 | ##### Create root CA ##### 29 | openssl genrsa -aes256 -passout pass:ca-password -out $D/ca.key 4096 30 | openssl req -key $D/ca.key -new -x509 -days 99999 -sha256 -out $D/ca.crt -passin pass:ca-password -subj "/C=US/ST=New York/L=Brooklyn/O=Trust Me Inc./CN=Trusted.CA.com" 31 | 32 | 33 | for L in onprem azure aws gcp; do 34 | echo "$L-password" | cat > $D/tls.$L.pw 35 | openssl genrsa -aes256 -passout file:$D/tls.$L.pw -out $D/tls.$L.key 4096 36 | done 37 | 38 | openssl req -new -key $D/tls.onprem.key -passin file:$D/tls.onprem.pw -out $D/tls.onprem.csr -subj "/C=US/ST=CA/L=San Francisco/O=Red Hat Inc./CN=inter-router-demo2-amq.apps.summit.sysdeseng.com" 39 | openssl req -new -key $D/tls.azure.key -passin file:$D/tls.azure.pw -out $D/tls.azure.csr -subj "/C=US/ST=TX/L=Azure/O=Red Hat Inc./CN=inter-router-demo2-amq.apps.summit-azr.sysdeseng.com" 40 | openssl req -new -key $D/tls.aws.key -passin file:$D/tls.aws.pw -out $D/tls.aws.csr -subj "/C=US/ST=OH/L=AWS/O=Red Hat Inc./CN=inter-router-demo2-amq.apps.summit-aws.sysdeseng.com" 41 | openssl req -new -key $D/tls.gcp.key -passin file:$D/tls.gcp.pw -out $D/tls.gcp.csr -subj "/C=US/ST=OH/L=GCP/O=Red Hat Inc./CN=inter-router-demo2-amq.apps.summit-gcp.sysdeseng.com" 42 | 43 | for L in onprem azure aws gcp; do 44 | openssl x509 -req -in $D/tls.$L.csr -CA $D/ca.crt -CAkey $D/ca.key -CAcreateserial -days 9999 -out $D/tls.$L.crt -passin pass:ca-password 45 | done 46 | 47 | rm -f $D/*.csr .srl $D/ca.key 48 | 49 | for L in onprem azure aws gcp; do 50 | openssl verify -verbose -CAfile $D/ca.crt $D/tls.$L.crt 51 | done 52 | -------------------------------------------------------------------------------- /frontend/src/main/resources/webroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AMQP Hybrid Cloud 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 32 | 33 | 34 |
35 |
36 |

AMQP Hybrid Cloud

37 | 40 |

Request

41 | 42 |
43 | 44 | 45 |
46 | 47 | 48 |
49 | 50 |
51 | 52 | 53 |
54 | 55 | 56 | 57 |
58 | 59 |

Responses

60 | 61 |
62 | 63 |

Workers

64 | 65 |
Loading
66 |
67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /scripts/qdrouterd.conf.onprem: -------------------------------------------------------------------------------- 1 | ## 2 | ## Licensed to the Apache Software Foundation (ASF) under one 3 | ## or more contributor license agreements. See the NOTICE file 4 | ## distributed with this work for additional information 5 | ## regarding copyright ownership. The ASF licenses this file 6 | ## to you under the Apache License, Version 2.0 (the 7 | ## "License"); you may not use this file except in compliance 8 | ## with the License. You may obtain a copy of the License at 9 | ## 10 | ## http://www.apache.org/licenses/LICENSE-2.0 11 | ## 12 | ## Unless required by applicable law or agreed to in writing, 13 | ## software distributed under the License is distributed on an 14 | ## "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | ## KIND, either express or implied. See the License for the 16 | ## specific language governing permissions and limitations 17 | ## under the License 18 | ## 19 | 20 | # See the qdrouterd.conf (5) manual page for information about this 21 | # file's format and options. 22 | 23 | router { 24 | mode: interior 25 | id: ${AMQ_LOCATION_KEY} 26 | } 27 | 28 | sslProfile { 29 | name: TLS 30 | certFile: /etc/qpid-dispatch-certs/internal/tls.crt 31 | keyFile: /etc/qpid-dispatch-certs/internal/tls.key 32 | passwordFile: /etc/qpid-dispatch-certs/internal/tls.pw 33 | certDb: /etc/qpid-dispatch-certs/internal/ca.crt 34 | } 35 | 36 | listener { 37 | host: 0.0.0.0 38 | port: amqp 39 | authenticatePeer: no 40 | saslMechanisms: ANONYMOUS 41 | } 42 | 43 | listener { 44 | host: 0.0.0.0 45 | port: 5671 46 | authenticatePeer: no 47 | saslMechanisms: ANONYMOUS 48 | sslProfile: TLS 49 | } 50 | 51 | listener { 52 | host: 0.0.0.0 53 | port: 8672 54 | authenticatePeer: no 55 | http: yes 56 | httpRootDir: /usr/share/qpid-dispatch/console/ 57 | } 58 | 59 | connector { 60 | host: inter-router-demo2-amq.${AWS_SUFFIX} 61 | port: 443 62 | role: inter-router 63 | sslProfile: TLS 64 | saslMechanisms: EXTERNAL 65 | verifyHostName: no 66 | cost: 5 67 | } 68 | 69 | 70 | connector { 71 | host: inter-router-demo2-amq.${AZURE_SUFFIX} 72 | port: 443 73 | role: inter-router 74 | sslProfile: TLS 75 | saslMechanisms: EXTERNAL 76 | verifyHostName: no 77 | cost: 6 78 | } 79 | 80 | connector { 81 | host: inter-router-demo2-amq.${GCP_SUFFIX} 82 | port: 443 83 | role: inter-router 84 | sslProfile: TLS 85 | saslMechanisms: EXTERNAL 86 | verifyHostName: no 87 | cost: 3 88 | } 89 | 90 | address { 91 | prefix: closest 92 | distribution: closest 93 | } 94 | 95 | address { 96 | prefix: multicast 97 | distribution: multicast 98 | } 99 | 100 | address { 101 | prefix: unicast 102 | distribution: closest 103 | } 104 | 105 | address { 106 | prefix: exclusive 107 | distribution: closest 108 | } 109 | 110 | address { 111 | prefix: broadcast 112 | distribution: multicast 113 | } 114 | -------------------------------------------------------------------------------- /messaging-load/load.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # 20 | 21 | from __future__ import print_function, unicode_literals 22 | import optparse 23 | import uuid 24 | from proton import Message 25 | from proton.handlers import MessagingHandler 26 | from proton.reactor import Container, DynamicNodeProperties 27 | import os 28 | 29 | class Client(MessagingHandler): 30 | def __init__(self, url): 31 | super(Client, self).__init__() 32 | self.uuid = str(uuid.uuid4())[0:8] 33 | self.url = url 34 | self.service_address = "work-requests" 35 | self.capacity = 10 36 | self.outstanding = 0 37 | self.request_count = 0 38 | self.locations = {} 39 | self.receiver = None 40 | self.service_sender = None 41 | 42 | def send(self): 43 | if self.outstanding >= self.capacity: 44 | return 45 | to_send = self.capacity - self.outstanding 46 | if self.service_sender.credit < to_send: 47 | to_send = self.service_sender.credit 48 | for i in range(to_send): 49 | ap = {u'uppercase': True, u'reverse': True} 50 | msg = Message(reply_to = self.reply_to, 51 | correlation_id = 0, 52 | properties = ap, 53 | body = "Load Request") 54 | self.service_sender.send(msg) 55 | self.outstanding += 1 56 | self.request_count += 1 57 | 58 | def on_start(self, event): 59 | self.container = event.container 60 | self.reactor = event.reactor 61 | self.conn = event.container.connect(self.url) 62 | 63 | def on_connection_opened(self, event): 64 | if self.receiver == None: 65 | self.receiver = event.container.create_receiver(self.conn, None, dynamic=True) 66 | self.outstanding = 0 67 | 68 | def on_link_opened(self, event): 69 | if event.receiver == self.receiver: 70 | self.reply_to = event.receiver.remote_source.address 71 | if self.service_sender == None: 72 | self.service_sender = event.container.create_sender(self.conn, self.service_address) 73 | 74 | def on_sendable(self, event): 75 | self.send() 76 | 77 | def on_message(self, event): 78 | pass 79 | 80 | def on_accepted(self, event): 81 | if event.sender == self.service_sender: 82 | self.outstanding -= 1 83 | self.send() 84 | 85 | def on_rejected(self, event): 86 | if event.sender == self.service_sender: 87 | self.outstanding -= 1 88 | self.send() 89 | 90 | def on_released(self, event): 91 | if event.sender == self.service_sender: 92 | self.outstanding -= 1 93 | self.send() 94 | 95 | 96 | try: 97 | ## 98 | ## Try to get the message bus hostname from the openshift environment 99 | ## Fall back to 127.0.0.1 (loopback) 100 | ## 101 | host = os.getenv("MESSAGING_SERVICE_HOST", "127.0.0.1") 102 | container = Container(Client(host)) 103 | container.container_id = os.getenv("HOSTNAME", "client") 104 | container.run() 105 | except KeyboardInterrupt: pass 106 | -------------------------------------------------------------------------------- /messaging-load/local_load.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # 20 | 21 | from __future__ import print_function, unicode_literals 22 | import optparse 23 | import uuid 24 | from proton import Message 25 | from proton.handlers import MessagingHandler 26 | from proton.reactor import Container, DynamicNodeProperties 27 | import os 28 | 29 | class Client(MessagingHandler): 30 | def __init__(self, url): 31 | super(Client, self).__init__() 32 | self.uuid = str(uuid.uuid4())[0:8] 33 | self.url = url 34 | self.service_address = "work-requests" 35 | self.capacity = 10 36 | self.outstanding = 0 37 | self.request_count = 0 38 | self.locations = {} 39 | self.receiver = None 40 | self.service_sender = None 41 | 42 | def send(self): 43 | if self.outstanding >= self.capacity: 44 | return 45 | to_send = self.capacity - self.outstanding 46 | if self.service_sender.credit < to_send: 47 | to_send = self.service_sender.credit 48 | for i in range(to_send): 49 | ap = {u'uppercase': True, u'reverse': True} 50 | msg = Message(reply_to = self.reply_to, 51 | correlation_id = 0, 52 | properties = ap, 53 | body = "Load Request") 54 | self.service_sender.send(msg) 55 | self.outstanding += 1 56 | self.request_count += 1 57 | 58 | def on_start(self, event): 59 | self.container = event.container 60 | self.reactor = event.reactor 61 | self.conn = event.container.connect(self.url) 62 | 63 | def on_connection_opened(self, event): 64 | if self.receiver == None: 65 | self.receiver = event.container.create_receiver(self.conn, None, dynamic=True) 66 | self.outstanding = 0 67 | 68 | def on_link_opened(self, event): 69 | if event.receiver == self.receiver: 70 | self.reply_to = event.receiver.remote_source.address 71 | if self.service_sender == None: 72 | self.service_sender = event.container.create_sender(self.conn, self.service_address) 73 | 74 | def on_sendable(self, event): 75 | self.send() 76 | 77 | def on_message(self, event): 78 | pass 79 | 80 | def on_accepted(self, event): 81 | if event.sender == self.service_sender: 82 | self.outstanding -= 1 83 | self.send() 84 | 85 | def on_rejected(self, event): 86 | if event.sender == self.service_sender: 87 | self.outstanding -= 1 88 | self.send() 89 | 90 | def on_released(self, event): 91 | if event.sender == self.service_sender: 92 | self.outstanding -= 1 93 | self.send() 94 | 95 | 96 | try: 97 | ## 98 | ## Try to get the message bus hostname from the openshift environment 99 | ## Fall back to 127.0.0.1 (loopback) 100 | ## 101 | ## host = os.getenv("MESSAGING_SERVICE_HOST", "amqps://amqps-demo2-amq.apps.192.168.99.102.nip.io:443") 102 | host = os.getenv("MESSAGING_SERVICE_HOST", "amqps://amqps-demo2-amq.35.230.115.194.nip.io:443") 103 | container = Container(Client(host)) 104 | container.container_id = os.getenv("HOSTNAME", "client") 105 | container.run() 106 | except KeyboardInterrupt: pass 107 | -------------------------------------------------------------------------------- /controller/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AMQ Hybrid Cloud Demo Controller 5 | 6 | 7 | 8 | 9 | 10 |

AMQ Hybrid Cloud Demo Controller

11 |
12 |
Current Rates
13 | 14 |
15 | 16 |
17 |
18 | 19 |
20 |
Load Control
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
ThreadsOutstandingRateControls
000
32 |
33 | 34 |
35 |
Server Rate Limiting
36 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 |
Server LocationControls
AWS
45 |
46 | 47 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /worker/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 4.0.0 20 | 21 | io.openshift.booster 22 | vertx-messaging-worker 23 | 1-SNAPSHOT 24 | jar 25 | 26 | Eclipse Vert.x - Messaging - Work Queue Booster - Worker 27 | 28 | 29 | io.openshift.booster.messaging.Worker 30 | / 31 | 1.8 32 | 1.8 33 | 3.5.3 34 | 3.5.40 35 | 1.0.17 36 | 37 | 38 | 39 | 40 | 41 | io.vertx 42 | vertx-dependencies 43 | ${vertx.version} 44 | pom 45 | import 46 | 47 | 48 | 49 | 50 | 51 | 52 | io.vertx 53 | vertx-proton 54 | 55 | 56 | org.slf4j 57 | slf4j-simple 58 | 1.7.25 59 | 60 | 61 | io.vertx 62 | vertx-core 63 | 64 | 65 | io.vertx 66 | vertx-rx-java2 67 | 68 | 69 | io.vertx 70 | vertx-config 71 | 72 | 73 | 74 | 75 | 76 | 77 | io.reactiverse 78 | vertx-maven-plugin 79 | ${vertx-maven-plugin.version} 80 | 81 | 82 | vmp 83 | 84 | initialize 85 | package 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | openshift 96 | 97 | 98 | 99 | io.fabric8 100 | fabric8-maven-plugin 101 | ${fabric8-maven-plugin.version} 102 | 103 | 104 | fmp 105 | 106 | resource 107 | build 108 | 109 | 110 | 111 | 112 | 113 | 114 | f8-maven-scm 115 | 116 | 117 | 118 | 119 | / 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /worker/integration-tests/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.openshift.booster 8 | vertx-messaging-work-queue-parent 9 | 1-SNAPSHOT 10 | 11 | 12 | vertx-messaging-booster-integration-tests 13 | 14 | Eclipse Vert.x - Messaging - Work Queue Booster - Integration Tests 15 | 16 | 17 | 18 | io.rest-assured 19 | rest-assured 20 | 3.1.0 21 | test 22 | 23 | 24 | org.assertj 25 | assertj-core 26 | 3.10.0 27 | test 28 | 29 | 30 | org.jboss.arquillian.junit 31 | arquillian-junit-standalone 32 | test 33 | 1.4.0.Final 34 | 35 | 36 | org.arquillian.cube 37 | arquillian-cube-openshift 38 | 1.16.0 39 | test 40 | 41 | 42 | org.arquillian.cube 43 | arquillian-cube-requirement 44 | 1.16.0 45 | test 46 | 47 | 48 | 49 | 50 | 51 | 52 | io.fabric8 53 | fabric8-maven-plugin 54 | 55 | true 56 | 57 | 58 | 59 | org.apache.maven.plugins 60 | maven-install-plugin 61 | 62 | true 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | openshift 71 | 72 | 73 | 74 | io.fabric8 75 | fabric8-maven-plugin 76 | 77 | 78 | 79 | resource 80 | 81 | 82 | 83 | 84 | false 85 | aggregate 86 | 87 | 88 | 89 | io.openshift.booster 90 | vertx-messaging-frontend 91 | ${project.version} 92 | 93 | 94 | io.openshift.booster 95 | vertx-messaging-worker 96 | ${project.version} 97 | 98 | 99 | 100 | 101 | org.apache.maven.plugins 102 | maven-failsafe-plugin 103 | 104 | 105 | file://${basedir}/target/test-classes/amq.yaml 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | openshift-it 114 | 115 | 116 | 117 | org.apache.maven.plugins 118 | maven-failsafe-plugin 119 | 120 | 121 | 122 | integration-test 123 | verify 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /worker/integration-tests/src/test/java/io/openshift/booster/OpenShiftIT.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Red Hat, Inc, and individual contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.openshift.booster; 18 | 19 | import java.net.MalformedURLException; 20 | import java.net.URL; 21 | 22 | import io.restassured.path.json.JsonPath; 23 | import io.restassured.response.Response; 24 | import org.arquillian.cube.openshift.impl.enricher.AwaitRoute; 25 | import org.arquillian.cube.openshift.impl.enricher.RouteURL; 26 | import org.arquillian.cube.openshift.impl.requirement.RequiresOpenshift; 27 | import org.arquillian.cube.requirement.ArquillianConditionalRunner; 28 | import org.junit.Before; 29 | import org.junit.Test; 30 | import org.junit.experimental.categories.Category; 31 | import org.junit.runner.RunWith; 32 | 33 | import static io.restassured.RestAssured.given; 34 | import static io.restassured.RestAssured.when; 35 | import static io.restassured.RestAssured.withArgs; 36 | import static java.util.concurrent.TimeUnit.SECONDS; 37 | import static org.assertj.core.api.Assertions.assertThat; 38 | import static org.awaitility.Awaitility.await; 39 | import static org.hamcrest.Matchers.equalTo; 40 | import static org.hamcrest.Matchers.hasItem; 41 | import static org.hamcrest.Matchers.is; 42 | import static org.hamcrest.Matchers.isEmptyString; 43 | import static org.hamcrest.Matchers.not; 44 | import static org.hamcrest.Matchers.notNullValue; 45 | 46 | @Category(RequiresOpenshift.class) 47 | @RequiresOpenshift 48 | @RunWith(ArquillianConditionalRunner.class) 49 | public class OpenShiftIT { 50 | 51 | @RouteURL("vertx-messaging-frontend") 52 | @AwaitRoute(path = "/health") 53 | private URL dashboardUrl; 54 | 55 | private URL dataUrl; 56 | 57 | private URL requestUrl; 58 | 59 | private URL responseUrl; 60 | 61 | @Before 62 | public void before() throws MalformedURLException { 63 | dataUrl = new URL(dashboardUrl, "api/data"); 64 | requestUrl = new URL(dashboardUrl, "api/send-request"); 65 | responseUrl = new URL(dashboardUrl, "api/receive-response"); 66 | } 67 | 68 | @Test 69 | public void shouldHandleRequest() { 70 | // Issue a request 71 | Response requestResponse = given() 72 | .body("{\"text\":\"test-message\",\"uppercase\":true,\"reverse\":true}") 73 | .and() 74 | .contentType("application/json") 75 | .when() 76 | .post(requestUrl) 77 | .thenReturn(); 78 | 79 | assertThat(requestResponse.getStatusCode()).isEqualTo(202); 80 | String requestId = requestResponse.getBody().asString(); 81 | 82 | // Wait for the request to be handled 83 | await().atMost(10, SECONDS) 84 | .untilAsserted(() -> given() 85 | .queryParam("request", requestId) 86 | .when() 87 | .get(responseUrl) 88 | .then() 89 | .statusCode(200) 90 | .body("requestId", is(equalTo(requestId))) 91 | .body("workerId", not((isEmptyString()))) 92 | .body("text", is(equalTo("EGASSEM-TSET")))); 93 | 94 | JsonPath responseJson = given() 95 | .queryParam("request", requestId) 96 | .when() 97 | .get(responseUrl) 98 | .thenReturn() 99 | .jsonPath(); 100 | String workerId = responseJson.getString("workerId"); 101 | String text = responseJson.getString("text"); 102 | 103 | // Verify data 104 | await().atMost(10, SECONDS) 105 | .untilAsserted(() -> when() 106 | .get(dataUrl) 107 | .then() 108 | .statusCode(200) 109 | .body("requestIds", hasItem(requestId)) 110 | .body("responses.%s.requestId", withArgs(requestId), is(equalTo(requestId))) 111 | .body("responses.%s.workerId", withArgs(requestId), is(equalTo(workerId))) 112 | .body("responses.%s.text", withArgs(requestId), is(equalTo(text))) 113 | .body("workers.%s.workerId", withArgs(workerId), is(equalTo(workerId))) 114 | .body("workers.%s.timestamp", withArgs(workerId), is(notNullValue())) 115 | .body("workers.%s.requestsProcessed", withArgs(workerId), is(notNullValue())) 116 | .body("workers.%s.processingErrors", withArgs(workerId), is(notNullValue()))); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /frontend/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 4.0.0 20 | 21 | io.openshift.booster 22 | vertx-messaging-frontend 23 | 1-SNAPSHOT 24 | jar 25 | 26 | Eclipse Vert.x - Messaging - Work Queue Booster - Frontend 27 | 28 | 29 | 1.8 30 | 1.8 31 | 3.5.3 32 | io.openshift.booster.messaging.Frontend 33 | 3.5.40 34 | 1.0.17 35 | 36 | 37 | 38 | 39 | 40 | io.vertx 41 | vertx-dependencies 42 | ${vertx.version} 43 | pom 44 | import 45 | 46 | 47 | 48 | 49 | 50 | 51 | io.vertx 52 | vertx-web 53 | 54 | 55 | io.vertx 56 | vertx-proton 57 | 58 | 59 | org.slf4j 60 | slf4j-simple 61 | 1.7.25 62 | 63 | 64 | io.vertx 65 | vertx-core 66 | 67 | 68 | io.vertx 69 | vertx-rx-java2 70 | 71 | 72 | io.vertx 73 | vertx-config 74 | 75 | 76 | 77 | 78 | 79 | 80 | io.reactiverse 81 | vertx-maven-plugin 82 | ${vertx-maven-plugin.version} 83 | 84 | 85 | vmp 86 | 87 | initialize 88 | package 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | openshift 99 | 100 | 101 | 102 | io.fabric8 103 | fabric8-maven-plugin 104 | ${fabric8-maven-plugin.version} 105 | 106 | 107 | fmp 108 | 109 | resource 110 | build 111 | 112 | 113 | 114 | 115 | 116 | 117 | f8-maven-scm 118 | 119 | 120 | 121 | 122 | /health 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /worker/integration-tests/src/test/resources/amq.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: ImageStream 3 | apiVersion: v1 4 | metadata: 5 | name: amq-broker-71-openshift 6 | annotations: 7 | openshift.io/display-name: Red Hat AMQ Broker 7.1 8 | openshift.io/provider-display-name: Red Hat, Inc. 9 | spec: 10 | lookupPolicy: 11 | local: true 12 | tags: 13 | - name: '1.0' 14 | annotations: 15 | description: Red Hat AMQ Broker 7.1 image. 16 | iconClass: icon-jboss 17 | tags: messaging,amq,jboss,xpaas 18 | supports: amq:7.1,messaging:1.0 19 | version: '1.0' 20 | from: 21 | kind: DockerImage 22 | name: registry.access.redhat.com/amq-broker-7-tech-preview/amq-broker-71-openshift:1.0 23 | --- 24 | kind: Service 25 | apiVersion: v1 26 | spec: 27 | ports: 28 | - port: 5672 29 | targetPort: 5672 30 | selector: 31 | deploymentConfig: "work-queue-broker-amq" 32 | metadata: 33 | name: "work-queue-broker-amq-amqp" 34 | labels: 35 | application: "work-queue-broker" 36 | annotations: 37 | description: The broker's AMQP port. 38 | --- 39 | kind: Service 40 | apiVersion: v1 41 | spec: 42 | ports: 43 | - port: 61613 44 | targetPort: 61613 45 | selector: 46 | deploymentConfig: "work-queue-broker-amq" 47 | metadata: 48 | name: "work-queue-broker-amq-stomp" 49 | labels: 50 | application: "work-queue-broker" 51 | annotations: 52 | description: The broker's STOMP port. 53 | --- 54 | kind: Service 55 | apiVersion: v1 56 | spec: 57 | ports: 58 | - port: 61616 59 | targetPort: 61616 60 | selector: 61 | deploymentConfig: "work-queue-broker-amq" 62 | metadata: 63 | name: "work-queue-broker-amq-tcp" 64 | labels: 65 | application: "work-queue-broker" 66 | annotations: 67 | description: The broker's OpenWire port. 68 | service.alpha.openshift.io/dependencies: '[{"name": "work-queue-broker-amq-amqp", 69 | "kind": "Service"},{"name": "work-queue-broker-amq-mqtt", "kind": "Service"},{"name": 70 | "work-queue-broker-amq-stomp", "kind": "Service"}]' 71 | --- 72 | kind: Service 73 | apiVersion: v1 74 | spec: 75 | clusterIP: None 76 | ports: 77 | - name: mesh 78 | port: 61616 79 | selector: 80 | deploymentConfig: "work-queue-broker-amq" 81 | metadata: 82 | name: "work-queue-broker-amq-mesh" 83 | labels: 84 | application: "work-queue-broker" 85 | annotations: 86 | service.alpha.kubernetes.io/tolerate-unready-endpoints: 'true' 87 | description: Supports node discovery for mesh formation. 88 | --- 89 | kind: DeploymentConfig 90 | apiVersion: v1 91 | metadata: 92 | name: "work-queue-broker-amq" 93 | labels: 94 | application: "work-queue-broker" 95 | spec: 96 | strategy: 97 | type: Rolling 98 | rollingParams: 99 | maxSurge: 0 100 | triggers: 101 | - type: ImageChange 102 | imageChangeParams: 103 | automatic: true 104 | containerNames: 105 | - "work-queue-broker-amq" 106 | from: 107 | kind: ImageStreamTag 108 | name: amq-broker-71-openshift:1.0 109 | - type: ConfigChange 110 | replicas: 1 111 | selector: 112 | deploymentConfig: "work-queue-broker-amq" 113 | template: 114 | metadata: 115 | name: "work-queue-broker-amq" 116 | labels: 117 | deploymentConfig: "work-queue-broker-amq" 118 | application: "work-queue-broker" 119 | spec: 120 | terminationGracePeriodSeconds: 60 121 | containers: 122 | - name: "work-queue-broker-amq" 123 | image: amq-broker-71-openshift 124 | imagePullPolicy: Always 125 | readinessProbe: 126 | exec: 127 | command: 128 | - "/bin/bash" 129 | - "-c" 130 | - "/opt/amq/bin/readinessProbe.sh" 131 | ports: 132 | - name: jolokia 133 | containerPort: 8778 134 | protocol: TCP 135 | - name: amqp 136 | containerPort: 5672 137 | protocol: TCP 138 | - name: mqtt 139 | containerPort: 1883 140 | protocol: TCP 141 | - name: stomp 142 | containerPort: 61613 143 | protocol: TCP 144 | - name: tcp 145 | containerPort: 61616 146 | protocol: TCP 147 | env: 148 | - name: AMQ_USER 149 | value: "work-queue" 150 | - name: AMQ_PASSWORD 151 | value: "work-queue" 152 | - name: AMQ_TRANSPORTS 153 | value: "amqp" 154 | - name: AMQ_QUEUES 155 | value: "work-queue/requests,work-queue/responses" 156 | - name: AMQ_ADDRESSES 157 | value: "work-queue/worker-updates" 158 | - name: AMQ_MESH_DISCOVERY_TYPE 159 | value: "dns" 160 | - name: AMQ_MESH_SERVICE_NAME 161 | value: "work-queue-broker-amq-mesh" 162 | - name: AMQ_MESH_SERVICE_NAMESPACE 163 | valueFrom: 164 | fieldRef: 165 | fieldPath: metadata.namespace 166 | - name: AMQ_STORAGE_USAGE_LIMIT 167 | value: "100 gb" 168 | - name: AMQ_QUEUE_MEMORY_LIMIT 169 | value: "" 170 | -------------------------------------------------------------------------------- /frontend/src/main/resources/webroot/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. See the NOTICE file 5 | * distributed with this work for additional information 6 | * regarding copyright ownership. The ASF licenses this file 7 | * to you under the Apache License, Version 2.0 (the 8 | * "License"); you may not use this file except in compliance 9 | * with the License. You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, 14 | * software distributed under the License is distributed on an 15 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | * KIND, either express or implied. See the License for the 17 | * specific language governing permissions and limitations 18 | * under the License. 19 | * 20 | */ 21 | 22 | "use strict"; 23 | 24 | const gesso = new Gesso(); 25 | 26 | class Application { 27 | constructor() { 28 | this.data = null; 29 | 30 | window.addEventListener("statechange", (event) => { 31 | this.renderResponses(); 32 | this.renderWorkers(); 33 | }); 34 | 35 | window.addEventListener("load", (event) => { 36 | this.fetchDataPeriodically(); 37 | 38 | $("#requests").addEventListener("submit", (event) => { 39 | this.sendRequest(event.target); 40 | this.fetchDataPeriodically(); 41 | }); 42 | }); 43 | } 44 | 45 | fetchDataPeriodically() { 46 | gesso.fetchPeriodically("/api/data", (data) => { 47 | this.data = data; 48 | window.dispatchEvent(new Event("statechange")); 49 | }); 50 | } 51 | 52 | sendRequest(form) { 53 | console.log("Sending request"); 54 | 55 | let request = gesso.openRequest("POST", "/api/send-request", (event) => { 56 | if (event.target.status >= 200 && event.target.status < 300) { 57 | this.fetchDataPeriodically(); 58 | } 59 | }); 60 | 61 | let data = { 62 | text: form.text.value, 63 | uppercase: form.uppercase.checked, 64 | reverse: form.reverse.checked, 65 | }; 66 | 67 | let json = JSON.stringify(data); 68 | 69 | request.setRequestHeader("Content-Type", "application/json"); 70 | request.send(json); 71 | 72 | form.text.value = ""; 73 | } 74 | 75 | renderResponses() { 76 | if (this.data.requestIds.length === 0) { 77 | return; 78 | } 79 | 80 | console.log("Rendering responses"); 81 | 82 | let div = gesso.createDiv(null, "#responses"); 83 | 84 | // let headings = ["Worker", "Cloud", "Response"]; 85 | // let rows = []; 86 | 87 | for (let requestId of this.data.requestIds.reverse()) { 88 | let response = this.data.responses[requestId]; 89 | 90 | if (response == null) { 91 | continue; 92 | } 93 | 94 | let item = gesso.createDiv(div, "response"); 95 | gesso.createDiv(item, "worker", response.workerId); 96 | gesso.createDiv(item, "cloud", response.cloudId); 97 | gesso.createDiv(item, "text", response.text); 98 | 99 | // rows.push([response.workerId, response.cloudId, response.text]); 100 | } 101 | // let table = gesso.createTable(null, headings, rows, {id: "responses"}); 102 | // gesso.replaceElement($("#responses"), table); 103 | 104 | gesso.replaceElement($("#responses"), div); 105 | 106 | } 107 | 108 | renderWorkers() { 109 | console.log("Rendering workers"); 110 | 111 | if (Object.keys(this.data.workers).length === 0) { 112 | let div = gesso.createDiv(null, "#workers"); 113 | let span = gesso.createSpan(div, "placeholder", "None"); 114 | 115 | gesso.replaceElement($("#workers"), div); 116 | 117 | return; 118 | } 119 | 120 | let headings = ["ID", "Cloud", "Requests", "Errors"]; 121 | // let headings = ["ID", "Cloud", "Updated", "Requests", "Errors"]; 122 | let rows = []; 123 | let now = new Date().getTime(); 124 | 125 | for (let workerId in this.data.workers) { 126 | let update = this.data.workers[workerId]; 127 | let cloud = update.cloud; 128 | // let time = new Date(update.timestamp).toLocaleDateString(); 129 | let requestsProcessed = update.requestsProcessed; 130 | let processingErrors = update.processingErrors; 131 | 132 | // rows.push([workerId, cloud, time, requestsProcessed, processingErrors]); 133 | rows.push([workerId, cloud, requestsProcessed, processingErrors]); 134 | } 135 | 136 | let table = gesso.createTable(null, headings, rows, {id: "workers"}); 137 | 138 | gesso.replaceElement($("#workers"), table); 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /worker/.openshiftio/application.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | metadata: 4 | name: vertx-messaging-worker 5 | annotations: 6 | description: This template creates a Build Configuration using an S2I builder. 7 | tags: instant-app 8 | parameters: 9 | - name: SOURCE_REPOSITORY_URL 10 | description: The source URL for the application 11 | displayName: Source URL 12 | required: true 13 | - name: SOURCE_REPOSITORY_REF 14 | description: The branch name for the application 15 | displayName: Source Branch 16 | value: master 17 | required: true 18 | - name: SOURCE_REPOSITORY_DIR 19 | description: The location within the source repo of the application 20 | displayName: Source Directory 21 | value: . 22 | required: true 23 | - name: GITHUB_WEBHOOK_SECRET 24 | description: A secret string used to configure the GitHub webhook. 25 | displayName: GitHub Webhook Secret 26 | required: true 27 | from: '[a-zA-Z0-9]{40}' 28 | generate: expression 29 | 30 | objects: 31 | - apiVersion: v1 32 | kind: ImageStream 33 | metadata: 34 | name: vertx-messaging-worker 35 | labels: 36 | booster: vertx-messaging-worker 37 | spec: {} 38 | 39 | - apiVersion: v1 40 | kind: ImageStream 41 | metadata: 42 | name: runtime-vertx-messaging-worker 43 | labels: 44 | booster: vertx-messaging-work-queue-booster 45 | spec: 46 | tags: 47 | - name: latest 48 | from: 49 | kind: DockerImage 50 | name: registry.access.redhat.com/redhat-openjdk-18/openjdk18-openshift 51 | 52 | - apiVersion: v1 53 | kind: BuildConfig 54 | metadata: 55 | name: vertx-messaging-worker 56 | labels: 57 | booster: vertx-messaging-worker 58 | spec: 59 | output: 60 | to: 61 | kind: ImageStreamTag 62 | name: vertx-messaging-worker:latest 63 | postCommit: {} 64 | resources: {} 65 | source: 66 | git: 67 | uri: ${SOURCE_REPOSITORY_URL} 68 | ref: ${SOURCE_REPOSITORY_REF} 69 | #contextDir: ${SOURCE_REPOSITORY_DIR} 70 | type: Git 71 | strategy: 72 | sourceStrategy: 73 | from: 74 | kind: ImageStreamTag 75 | name: runtime-vertx-messaging-worker:latest 76 | incremental: true 77 | env: 78 | - name: MAVEN_ARGS_APPEND 79 | value: "-pl ${SOURCE_REPOSITORY_DIR}" 80 | - name: ARTIFACT_DIR 81 | value: "${SOURCE_REPOSITORY_DIR}/target" 82 | type: Source 83 | triggers: 84 | - github: 85 | secret: ${GITHUB_WEBHOOK_SECRET} 86 | type: GitHub 87 | - type: ConfigChange 88 | - imageChange: {} 89 | type: ImageChange 90 | 91 | - apiVersion: v1 92 | kind: Service 93 | metadata: 94 | annotations: 95 | prometheus.io/scrape: "true" 96 | prometheus.io/port: "9779" 97 | labels: 98 | app: vertx-messaging-worker 99 | group: io.openshift.booster 100 | name: vertx-messaging-worker 101 | spec: 102 | ports: 103 | - name: http 104 | port: 8080 105 | protocol: TCP 106 | targetPort: 8080 107 | selector: 108 | app: vertx-messaging-worker 109 | group: io.openshift.booster 110 | 111 | - apiVersion: apps.openshift.io/v1 112 | kind: DeploymentConfig 113 | metadata: 114 | labels: 115 | app: vertx-messaging-worker 116 | group: io.openshift.booster 117 | name: vertx-messaging-worker 118 | spec: 119 | replicas: 1 120 | selector: 121 | app: vertx-messaging-worker 122 | group: io.openshift.booster 123 | template: 124 | metadata: 125 | labels: 126 | app: vertx-messaging-worker 127 | group: io.openshift.booster 128 | spec: 129 | containers: 130 | - env: 131 | - name: KUBERNETES_NAMESPACE 132 | valueFrom: 133 | fieldRef: 134 | fieldPath: metadata.namespace 135 | - name: AMQ_LOCATION_KEY 136 | value: BURR 137 | readinessProbe: 138 | httpGet: 139 | path: / 140 | port: 8080 141 | scheme: HTTP 142 | initialDelaySeconds: 10 143 | livenessProbe: 144 | httpGet: 145 | path: / 146 | port: 8080 147 | scheme: HTTP 148 | initialDelaySeconds: 180 149 | image: vertx-messaging-worker:latest 150 | imagePullPolicy: IfNotPresent 151 | name: vertx 152 | ports: 153 | - containerPort: 8080 154 | name: http 155 | protocol: TCP 156 | - containerPort: 9779 157 | name: prometheus 158 | protocol: TCP 159 | - containerPort: 8778 160 | name: jolokia 161 | protocol: TCP 162 | triggers: 163 | - type: ConfigChange 164 | - imageChangeParams: 165 | automatic: true 166 | containerNames: 167 | - vertx 168 | from: 169 | kind: ImageStreamTag 170 | name: vertx-messaging-worker:latest 171 | type: ImageChange 172 | 173 | - apiVersion: route.openshift.io/v1 174 | kind: Route 175 | metadata: 176 | labels: 177 | app: vertx-messaging-worker 178 | group: io.openshift.booster 179 | name: vertx-messaging-worker 180 | spec: 181 | port: 182 | targetPort: 8080 183 | to: 184 | kind: Service 185 | name: vertx-messaging-worker 186 | -------------------------------------------------------------------------------- /server/service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # 20 | 21 | from __future__ import print_function 22 | from proton import Message 23 | from proton.handlers import MessagingHandler 24 | from proton.reactor import Container, DynamicNodeProperties 25 | from time import time 26 | import os 27 | 28 | class Timer(object): 29 | def __init__(self, parent): 30 | self.parent = parent 31 | 32 | def on_timer_task(self, event): 33 | self.parent.tick() 34 | 35 | 36 | class Request(object): 37 | def __init__(self, parent, delivery, message): 38 | self.parent = parent 39 | self.delivery = delivery 40 | self.body = message.body + "\nProcessed by service running on %s" % os.uname()[1] 41 | self.start = time() 42 | self.cid = message.correlation_id 43 | self.reply_to = message.reply_to 44 | 45 | def response(self): 46 | return Message(body = self.body, address = self.reply_to, correlation_id = self.cid, properties={'location':self.parent.location}) 47 | 48 | 49 | class Service(MessagingHandler): 50 | def __init__(self, url, location, rate): 51 | super(Service, self).__init__(auto_accept = False) 52 | self.url = url 53 | self.rate = rate 54 | self.address = "FraudDetection/v1" 55 | self.control_address = "amq-demo.server-control.%s" % location 56 | self.requests = [] 57 | self.can_process = self.rate / 2 # acceptances per half-second 58 | self.location = location 59 | 60 | def process_requests(self): 61 | while self.can_process > 0 and len(self.requests) > 0: 62 | request = self.requests.pop() 63 | self.anon_sender.send(request.response()) 64 | self.accept(request.delivery) 65 | self.can_process -= 1 66 | 67 | def tick(self): 68 | ## 69 | ## Schedule the next half-second tick 70 | ## 71 | self.timer = self.reactor.schedule(0.5, Timer(self)) 72 | self.can_process = self.rate / 2 73 | 74 | ## 75 | ## Process any pending requests 76 | ## 77 | self.process_requests() 78 | 79 | def on_start(self, event): 80 | self.container = event.container 81 | self.reactor = event.reactor 82 | self.conn = self.container.connect(self.url) 83 | self.timer = self.reactor.schedule(0.5, Timer(self)) 84 | self.receiver = self.container.create_receiver(self.conn, self.address) 85 | self.control_receiver = self.container.create_receiver(self.conn, self.control_address) 86 | self.anon_sender = self.container.create_sender(self.conn, None) 87 | 88 | def handle_control_request(self, msg): 89 | opcode = msg.properties.get("opcode") 90 | rate = int(msg.properties.get("rate", 0)) 91 | if opcode == "SET_RATE": 92 | if rate > 0: 93 | self.rate = rate 94 | elif opcode == "GET_RATE": 95 | pass 96 | else: 97 | return 98 | 99 | if msg.reply_to: 100 | response_properties["api"] = "amq-demo.server-control.v1" 101 | response_properties["opcode"] = opcode 102 | response_properties["rate"] = self.rate 103 | response_properties["location"] = self.location 104 | response = Message(address=msg.reply_to, correlation_id=msg.correlation_id, properties=response_properties) 105 | self.anon_sender.send(response) 106 | 107 | def on_message(self, event): 108 | if event.receiver == self.receiver: 109 | ## 110 | ## This is a new client request received on the service address 111 | ## 112 | self.requests.append(Request(self, event.delivery, event.message)) 113 | self.process_requests() 114 | elif event.receiver == self.control_receiver: 115 | ## 116 | ## This is a control request 117 | ## 118 | self.handle_control_request(event.message) 119 | 120 | try: 121 | ## 122 | ## Try to get the message bus hostname from the openshift environment 123 | ## Fall back to 127.0.0.1 (loopback) 124 | ## 125 | host = os.getenv("MESSAGING_SERVICE_HOST", "127.0.0.1") 126 | location = os.getenv("AMQ_LOCATION_KEY", "On-Stage") 127 | initial_rate = int(os.getenv("AMQ_INITIAL_RATE", "200")) 128 | container = Container(Service(host, location, initial_rate)) 129 | container.container_id = os.getenv("HOSTNAME", "Service") 130 | container.run() 131 | except KeyboardInterrupt: pass 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /frontend/.openshiftio/application.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | metadata: 4 | name: vertx-messaging-frontend 5 | annotations: 6 | description: This template creates a Vert.x application sending message to an AMQP broker and receiving replies. 7 | tags: instant-app 8 | parameters: 9 | - name: SOURCE_REPOSITORY_URL 10 | description: The source URL for the application 11 | displayName: Source URL 12 | required: true 13 | - name: SOURCE_REPOSITORY_REF 14 | description: The branch name for the application 15 | displayName: Source Branch 16 | value: master 17 | required: true 18 | - name: SOURCE_REPOSITORY_DIR 19 | description: The location within the source repo of the application 20 | displayName: Source Directory 21 | value: . 22 | required: true 23 | - name: GITHUB_WEBHOOK_SECRET 24 | description: A secret string used to configure the GitHub webhook. 25 | displayName: GitHub Webhook Secret 26 | required: true 27 | from: '[a-zA-Z0-9]{40}' 28 | generate: expression 29 | 30 | objects: 31 | - apiVersion: v1 32 | kind: ImageStream 33 | metadata: 34 | name: vertx-messaging-frontend 35 | labels: 36 | booster: vertx-messaging-frontend 37 | spec: {} 38 | 39 | - apiVersion: v1 40 | kind: ImageStream 41 | metadata: 42 | name: runtime-vertx-messaging-frontend 43 | labels: 44 | booster: vertx-messaging-frontend 45 | spec: 46 | tags: 47 | - name: latest 48 | from: 49 | kind: DockerImage 50 | name: registry.access.redhat.com/redhat-openjdk-18/openjdk18-openshift 51 | 52 | - apiVersion: v1 53 | kind: BuildConfig 54 | metadata: 55 | name: vertx-messaging-frontend 56 | labels: 57 | booster: vertx-messaging-work-queue-booster 58 | spec: 59 | output: 60 | to: 61 | kind: ImageStreamTag 62 | name: vertx-messaging-frontend:latest 63 | postCommit: {} 64 | resources: {} 65 | source: 66 | git: 67 | uri: ${SOURCE_REPOSITORY_URL} 68 | ref: ${SOURCE_REPOSITORY_REF} 69 | #contextDir: ${SOURCE_REPOSITORY_DIR} 70 | type: Git 71 | strategy: 72 | sourceStrategy: 73 | from: 74 | kind: ImageStreamTag 75 | name: runtime-vertx-messaging-frontend:latest 76 | incremental: true 77 | env: 78 | - name: MAVEN_ARGS_APPEND 79 | value: "-pl ${SOURCE_REPOSITORY_DIR}" 80 | - name: ARTIFACT_DIR 81 | value: "${SOURCE_REPOSITORY_DIR}/target" 82 | type: Source 83 | triggers: 84 | - github: 85 | secret: ${GITHUB_WEBHOOK_SECRET} 86 | type: GitHub 87 | - type: ConfigChange 88 | - imageChange: {} 89 | type: ImageChange 90 | 91 | - apiVersion: v1 92 | kind: Service 93 | metadata: 94 | annotations: 95 | prometheus.io/scrape: "true" 96 | prometheus.io/port: "9779" 97 | labels: 98 | app: vertx-messaging-frontend 99 | group: io.openshift.booster 100 | name: vertx-messaging-frontend 101 | spec: 102 | ports: 103 | - name: http 104 | port: 8080 105 | protocol: TCP 106 | targetPort: 8080 107 | selector: 108 | app: vertx-messaging-frontend 109 | group: io.openshift.booster 110 | 111 | - apiVersion: apps.openshift.io/v1 112 | kind: DeploymentConfig 113 | metadata: 114 | labels: 115 | app: vertx-messaging-frontend 116 | group: io.openshift.booster 117 | name: vertx-messaging-frontend 118 | spec: 119 | replicas: 1 120 | selector: 121 | app: vertx-messaging-frontend 122 | group: io.openshift.booster 123 | template: 124 | metadata: 125 | labels: 126 | app: vertx-messaging-frontend 127 | group: io.openshift.booster 128 | spec: 129 | containers: 130 | - env: 131 | - name: KUBERNETES_NAMESPACE 132 | valueFrom: 133 | fieldRef: 134 | fieldPath: metadata.namespace 135 | readinessProbe: 136 | httpGet: 137 | path: /health 138 | port: 8080 139 | scheme: HTTP 140 | initialDelaySeconds: 10 141 | livenessProbe: 142 | httpGet: 143 | path: /health 144 | port: 8080 145 | scheme: HTTP 146 | initialDelaySeconds: 180 147 | image: vertx-messaging-frontend:latest 148 | imagePullPolicy: IfNotPresent 149 | name: vertx 150 | ports: 151 | - containerPort: 8080 152 | name: http 153 | protocol: TCP 154 | - containerPort: 9779 155 | name: prometheus 156 | protocol: TCP 157 | - containerPort: 8778 158 | name: jolokia 159 | protocol: TCP 160 | securityContext: 161 | privileged: false 162 | triggers: 163 | - type: ConfigChange 164 | - imageChangeParams: 165 | automatic: true 166 | containerNames: 167 | - vertx 168 | from: 169 | kind: ImageStreamTag 170 | name: vertx-messaging-frontend:latest 171 | type: ImageChange 172 | 173 | - apiVersion: route.openshift.io/v1 174 | kind: Route 175 | metadata: 176 | labels: 177 | app: vertx-messaging-frontend 178 | group: io.openshift.booster 179 | name: vertx-messaging-frontend 180 | spec: 181 | port: 182 | targetPort: 8080 183 | to: 184 | kind: Service 185 | name: vertx-messaging-frontend 186 | -------------------------------------------------------------------------------- /frontend/.openshiftio/service.yaml: -------------------------------------------------------------------------------- 1 | kind: List 2 | apiVersion: v1 3 | metadata: 4 | name: amq-broker-7-image-streams 5 | annotations: 6 | description: ImageStream definitions for Red Hat AMQ Broker 7.1 Container Image 7 | openshift.io/provider-display-name: Red Hat, Inc. 8 | items: 9 | - kind: ImageStream 10 | apiVersion: v1 11 | metadata: 12 | name: amq-broker-71-openshift 13 | annotations: 14 | openshift.io/display-name: Red Hat AMQ Broker 7.1 15 | openshift.io/provider-display-name: Red Hat, Inc. 16 | spec: 17 | lookupPolicy: 18 | local: true 19 | tags: 20 | - name: '1.0' 21 | annotations: 22 | description: Red Hat AMQ Broker 7.1 image. 23 | iconClass: icon-jboss 24 | tags: messaging,amq,jboss,xpaas 25 | supports: amq:7.1,messaging:1.0 26 | version: '1.0' 27 | from: 28 | kind: DockerImage 29 | name: registry.access.redhat.com/amq-broker-7-tech-preview/amq-broker-71-openshift:1.0 30 | 31 | - kind: Service 32 | apiVersion: v1 33 | spec: 34 | ports: 35 | - port: 5672 36 | targetPort: 5672 37 | selector: 38 | deploymentConfig: "work-queue-broker-amq" 39 | metadata: 40 | name: "work-queue-broker-amq-amqp" 41 | labels: 42 | application: "work-queue-broker" 43 | annotations: 44 | description: The broker's AMQP port. 45 | - apiVersion: v1 46 | kind: Service 47 | metadata: 48 | annotations: 49 | description: The broker's console and Jolokia port. 50 | labels: 51 | application: "work-queue-broker" 52 | name: "work-queue-broker-amq-jolokia" 53 | spec: 54 | ports: 55 | - port: 8161 56 | targetPort: 8161 57 | selector: 58 | deploymentConfig: work-queue-broker-amq 59 | - kind: Service 60 | apiVersion: v1 61 | spec: 62 | ports: 63 | - port: 1883 64 | targetPort: 1883 65 | selector: 66 | deploymentConfig: "work-queue-broker-amq" 67 | metadata: 68 | name: "work-queue-broker-amq-mqtt" 69 | labels: 70 | application: "work-queue-broker" 71 | annotations: 72 | description: The broker's MQTT port. 73 | - kind: Service 74 | apiVersion: v1 75 | spec: 76 | ports: 77 | - port: 61613 78 | targetPort: 61613 79 | selector: 80 | deploymentConfig: "work-queue-broker-amq" 81 | metadata: 82 | name: "work-queue-broker-amq-stomp" 83 | labels: 84 | application: "work-queue-broker" 85 | annotations: 86 | description: The broker's STOMP port. 87 | - kind: Service 88 | apiVersion: v1 89 | spec: 90 | ports: 91 | - port: 61616 92 | targetPort: 61616 93 | selector: 94 | deploymentConfig: "work-queue-broker-amq" 95 | metadata: 96 | name: "work-queue-broker-amq-tcp" 97 | labels: 98 | application: "work-queue-broker" 99 | annotations: 100 | description: The broker's OpenWire port. 101 | service.alpha.openshift.io/dependencies: '[{"name": "work-queue-broker-amq-amqp", 102 | "kind": "Service"},{"name": "work-queue-broker-amq-mqtt", "kind": "Service"},{"name": 103 | "work-queue-broker-amq-stomp", "kind": "Service"}]' 104 | - kind: Service 105 | apiVersion: v1 106 | spec: 107 | clusterIP: None 108 | ports: 109 | - name: mesh 110 | port: 61616 111 | selector: 112 | deploymentConfig: "work-queue-broker-amq" 113 | metadata: 114 | name: "work-queue-broker-amq-mesh" 115 | labels: 116 | application: "work-queue-broker" 117 | annotations: 118 | service.alpha.kubernetes.io/tolerate-unready-endpoints: 'true' 119 | description: Supports node discovery for mesh formation. 120 | - kind: DeploymentConfig 121 | apiVersion: v1 122 | metadata: 123 | name: "work-queue-broker-amq" 124 | labels: 125 | application: "work-queue-broker" 126 | spec: 127 | strategy: 128 | type: Rolling 129 | rollingParams: 130 | maxSurge: 0 131 | triggers: 132 | - type: ImageChange 133 | imageChangeParams: 134 | automatic: true 135 | containerNames: 136 | - "work-queue-broker-amq" 137 | from: 138 | kind: ImageStreamTag 139 | name: amq-broker-71-openshift:1.0 140 | - type: ConfigChange 141 | replicas: 1 142 | selector: 143 | deploymentConfig: "work-queue-broker-amq" 144 | template: 145 | metadata: 146 | name: "work-queue-broker-amq" 147 | labels: 148 | deploymentConfig: "work-queue-broker-amq" 149 | application: "work-queue-broker" 150 | spec: 151 | terminationGracePeriodSeconds: 60 152 | containers: 153 | - name: "work-queue-broker-amq" 154 | image: amq-broker-71-openshift 155 | imagePullPolicy: Always 156 | readinessProbe: 157 | exec: 158 | command: 159 | - "/bin/bash" 160 | - "-c" 161 | - "/opt/amq/bin/readinessProbe.sh" 162 | ports: 163 | - name: jolokia 164 | containerPort: 8778 165 | protocol: TCP 166 | - name: amqp 167 | containerPort: 5672 168 | protocol: TCP 169 | - name: mqtt 170 | containerPort: 1883 171 | protocol: TCP 172 | - name: stomp 173 | containerPort: 61613 174 | protocol: TCP 175 | - name: tcp 176 | containerPort: 61616 177 | protocol: TCP 178 | env: 179 | - name: AMQ_USER 180 | value: "work-queue" 181 | - name: AMQ_PASSWORD 182 | value: "work-queue" 183 | - name: AMQ_TRANSPORTS 184 | value: "amqp" 185 | - name: AMQ_QUEUES 186 | value: "work-queue/requests,work-queue/responses" 187 | - name: AMQ_ADDRESSES 188 | value: "work-queue/worker-updates" 189 | - name: AMQ_MESH_DISCOVERY_TYPE 190 | value: "dns" 191 | - name: AMQ_MESH_SERVICE_NAME 192 | value: "work-queue-broker-amq-mesh" 193 | - name: AMQ_MESH_SERVICE_NAMESPACE 194 | valueFrom: 195 | fieldRef: 196 | fieldPath: metadata.namespace 197 | - name: AMQ_STORAGE_USAGE_LIMIT 198 | value: "100 gb" 199 | - name: AMQ_QUEUE_MEMORY_LIMIT 200 | value: "" 201 | -------------------------------------------------------------------------------- /worker/src/main/java/io/openshift/booster/messaging/Worker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package io.openshift.booster.messaging; 19 | 20 | import io.vertx.core.Future; 21 | import io.vertx.core.logging.Logger; 22 | import io.vertx.core.logging.LoggerFactory; 23 | import io.vertx.proton.ProtonClient; 24 | import io.vertx.proton.ProtonConnection; 25 | import io.vertx.proton.ProtonReceiver; 26 | import io.vertx.proton.ProtonSender; 27 | import io.vertx.reactivex.config.ConfigRetriever; 28 | import io.vertx.reactivex.core.AbstractVerticle; 29 | import org.apache.qpid.proton.amqp.messaging.AmqpValue; 30 | import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; 31 | import org.apache.qpid.proton.message.Message; 32 | 33 | import java.util.HashMap; 34 | import java.util.Map; 35 | import java.util.UUID; 36 | import java.util.concurrent.atomic.AtomicInteger; 37 | 38 | public class Worker extends AbstractVerticle { 39 | private static final Logger LOGGER = LoggerFactory.getLogger(Worker.class); 40 | private static final String ID = "worker-vertx-" + UUID.randomUUID() 41 | .toString().substring(0, 4); 42 | 43 | private static final String AMQ_LOCATION_KEY = 44 | System.getenv().getOrDefault("AMQ_LOCATION_KEY", "burrUnknown"); 45 | 46 | 47 | private static final AtomicInteger requestsProcessed = new AtomicInteger(0); 48 | private static final AtomicInteger processingErrors = new AtomicInteger(0); 49 | 50 | @Override 51 | public void start(Future future) { 52 | ConfigRetriever.create(vertx).rxGetConfig() 53 | .doOnSuccess(json -> { 54 | String amqpHost = json.getString("MESSAGING_SERVICE_HOST", "localhost"); 55 | int amqpPort = json.getInteger("MESSAGING_SERVICE_PORT", 5672); 56 | String amqpUser = json.getString("MESSAGING_SERVICE_USER", "work-queue"); 57 | String amqpPassword = json.getString("MESSAGING_SERVICE_PASSWORD", "work-queue"); 58 | 59 | ProtonClient client = ProtonClient.create(vertx.getDelegate()); 60 | client.connect(amqpHost, amqpPort, amqpUser, amqpPassword, result -> { 61 | if (result.failed()) { 62 | future.fail(result.cause()); 63 | } else { 64 | ProtonConnection conn = result.result(); 65 | conn.setContainer(ID); 66 | conn.open(); 67 | 68 | receiveRequests(conn); 69 | sendUpdates(conn); 70 | future.complete(); 71 | } 72 | }); 73 | }).flatMap(x -> vertx.createHttpServer().requestHandler(req -> req.response().end("Ready")).rxListen(8080)) 74 | .subscribe(); 75 | } 76 | 77 | private void receiveRequests(ProtonConnection conn) { 78 | // Ordinarily, a sender or receiver is tied to a named message 79 | // source or target. By contrast, a null sender transmits 80 | // messages using an "anonymous" link and routes them to their 81 | // destination using the "to" property of the message. 82 | ProtonSender sender = conn.createSender(null); 83 | 84 | ProtonReceiver receiver = conn.createReceiver("work-requests"); 85 | 86 | receiver.handler((delivery, request) -> { 87 | LOGGER.info("{0}: Receiving request {1}", ID, request); 88 | String responseBody; 89 | 90 | try { 91 | responseBody = processRequest(request); 92 | } catch (Exception e) { 93 | LOGGER.error("{0}: Failed processing message: {1}", ID, e.getMessage()); 94 | processingErrors.incrementAndGet(); 95 | return; 96 | } 97 | 98 | Map props = new HashMap<>(); 99 | props.put("workerId", conn.getContainer()); 100 | props.put("AMQ_LOCATION_KEY",AMQ_LOCATION_KEY); 101 | 102 | Message response = Message.Factory.create(); 103 | response.setAddress(request.getReplyTo()); 104 | response.setCorrelationId(request.getMessageId()); 105 | response.setBody(new AmqpValue(responseBody)); 106 | response.setApplicationProperties(new ApplicationProperties(props)); 107 | 108 | sender.send(response); 109 | 110 | requestsProcessed.incrementAndGet(); 111 | 112 | LOGGER.info("{0}: Sent {1}", ID, response); 113 | }); 114 | 115 | sender.open(); 116 | receiver.open(); 117 | } 118 | 119 | private String processRequest(Message request) { 120 | Map props = request.getApplicationProperties().getValue(); 121 | boolean uppercase = (boolean) props.get("uppercase"); 122 | boolean reverse = (boolean) props.get("reverse"); 123 | String text = (String) ((AmqpValue) request.getBody()).getValue(); 124 | 125 | if (uppercase) { 126 | text = text.toUpperCase(); 127 | } 128 | 129 | if (reverse) { 130 | text = new StringBuilder(text).reverse().toString(); 131 | } 132 | return "Aloha " + text; 133 | // return text; 134 | } 135 | 136 | private void sendUpdates(ProtonConnection conn) { 137 | ProtonSender sender = conn.createSender("worker-updates"); 138 | 139 | vertx.setPeriodic(5000, timer -> { 140 | if (conn.isDisconnected()) { 141 | vertx.cancelTimer(timer); 142 | return; 143 | } 144 | 145 | if (sender.sendQueueFull()) { 146 | return; 147 | } 148 | 149 | LOGGER.debug("{0}: Sending status update", ID); 150 | 151 | Map properties = new HashMap<>(); 152 | properties.put("workerId", conn.getContainer()); 153 | properties.put("AMQ_LOCATION_KEY",AMQ_LOCATION_KEY); 154 | properties.put("timestamp", System.currentTimeMillis()); 155 | properties.put("requestsProcessed", (long) requestsProcessed.get()); 156 | properties.put("processingErrors", (long) processingErrors.get()); 157 | 158 | Message message = Message.Factory.create(); 159 | message.setApplicationProperties(new ApplicationProperties(properties)); 160 | 161 | sender.send(message); 162 | }); 163 | 164 | sender.open(); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /client/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # 20 | 21 | from __future__ import print_function, unicode_literals 22 | import optparse 23 | import uuid 24 | from proton import Message 25 | from proton.handlers import MessagingHandler 26 | from proton.reactor import Container, DynamicNodeProperties 27 | import os 28 | 29 | class Timer(object): 30 | def __init__(self, parent): 31 | self.parent = parent 32 | 33 | def on_timer_task(self, event): 34 | self.parent.tick_500ms() 35 | 36 | 37 | class Client(MessagingHandler): 38 | def __init__(self, url): 39 | super(Client, self).__init__() 40 | self.uuid = str(uuid.uuid4())[0:8] 41 | self.url = url 42 | self.report_address = "multicast.amq-demo-report" 43 | self.stats_address = "multicast.amq-demo-stats" 44 | self.control_address = "amq-demo-control" 45 | self.service_address = "FraudDetection/v1" 46 | self.capacity = 0 47 | self.outstanding = 0 48 | self.samples = [] 49 | self.max_samples = 4 50 | self.request_count = 0 51 | self.locations = {} 52 | self.receiver = None 53 | self.service_sender = None 54 | 55 | def send(self): 56 | if self.outstanding >= self.capacity: 57 | return 58 | to_send = self.capacity - self.outstanding 59 | if self.service_sender.credit < to_send: 60 | to_send = self.service_sender.credit 61 | for i in range(to_send): 62 | msg = Message(reply_to = self.reply_to, correlation_id = 0, body = "Client Request") 63 | self.service_sender.send(msg) 64 | self.outstanding += 1 65 | self.request_count += 1 66 | 67 | def count_received(self, location): 68 | if location not in self.locations: 69 | self.locations[location] = [] 70 | for i in range(self.max_samples): 71 | self.locations[location].append(0) 72 | self.locations[location][0] += 1 73 | 74 | def send_stats_update(self): 75 | stat_map = {} 76 | for loc,samples in self.locations.items(): 77 | stat_map[loc] = sum(samples) / (len(samples) * 0.5) 78 | self.locations[loc].pop() 79 | self.locations[loc].insert(0, 0) 80 | if sum(self.locations[loc]) == 0: 81 | self.locations.pop(loc) 82 | if self.stats_sender.credit > 0: 83 | msg = Message(properties={'api':'amq-demo.server-stats.v1'}, body=stat_map) 84 | dlv = self.stats_sender.send(msg) 85 | dlv.settle() 86 | 87 | def tick_500ms(self): 88 | self.timer = self.reactor.schedule(0.5, Timer(self)) 89 | self.samples.append(self.request_count) 90 | self.request_count = 0 91 | if len(self.samples) > self.max_samples: 92 | self.samples.pop(0) 93 | rate = sum(self.samples) / (len(self.samples) * 0.5) # average rate/sec over the sample span 94 | report = {u'capacity': self.capacity, 95 | u'outstanding': self.outstanding, 96 | u'rate': int(rate)} 97 | if self.report_sender.credit > 0: 98 | msg = Message(properties={'api': 'amq-demo.client-report.v1'}, body=report) 99 | dlv = self.report_sender.send(msg) 100 | dlv.settle() 101 | self.send_stats_update() 102 | 103 | def on_start(self, event): 104 | self.container = event.container 105 | self.reactor = event.reactor 106 | self.conn = event.container.connect(self.url) 107 | 108 | def on_connection_opened(self, event): 109 | if self.receiver == None: 110 | self.receiver = event.container.create_receiver(self.conn, None, dynamic=True) 111 | self.control_receiver = event.container.create_receiver(self.conn, self.control_address) 112 | self.outstanding = 0 113 | 114 | def on_link_opened(self, event): 115 | if event.receiver == self.receiver: 116 | self.reply_to = event.receiver.remote_source.address 117 | if self.service_sender == None: 118 | self.service_sender = event.container.create_sender(self.conn, self.service_address) 119 | self.report_sender = event.container.create_sender(self.conn, self.report_address) 120 | self.stats_sender = event.container.create_sender(self.conn, self.stats_address) 121 | self.timer = self.reactor.schedule(0.5, Timer(self)) 122 | 123 | def on_sendable(self, event): 124 | self.send() 125 | 126 | def on_message(self, event): 127 | try: 128 | if event.receiver == self.control_receiver: 129 | props = event.message.properties 130 | opcode = props.get('opcode') 131 | value = int(props.get('value', 0)) 132 | if opcode == 'INC_CAPACITY': 133 | self.capacity += value 134 | if opcode == 'DEC_CAPACITY': 135 | self.capacity -= value 136 | if self.capacity < 0: 137 | self.capacity = 0 138 | elif event.receiver == self.receiver: 139 | ap = event.message.properties 140 | if 'location' in ap: 141 | self.count_received(ap['location']) 142 | except: 143 | pass 144 | 145 | def on_accepted(self, event): 146 | if event.sender == self.service_sender: 147 | self.outstanding -= 1 148 | self.send() 149 | 150 | def on_rejected(self, event): 151 | if event.sender == self.service_sender: 152 | self.outstanding -= 1 153 | self.send() 154 | 155 | def on_released(self, event): 156 | if event.sender == self.service_sender: 157 | self.outstanding -= 1 158 | self.send() 159 | 160 | 161 | try: 162 | ## 163 | ## Try to get the message bus hostname from the openshift environment 164 | ## Fall back to 127.0.0.1 (loopback) 165 | ## 166 | host = os.getenv("MESSAGING_SERVICE_HOST", "127.0.0.1") 167 | container = Container(Client(host)) 168 | container.container_id = os.getenv("HOSTNAME", "client") 169 | container.run() 170 | except KeyboardInterrupt: pass 171 | -------------------------------------------------------------------------------- /frontend/src/main/resources/webroot/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. See the NOTICE file 5 | * distributed with this work for additional information 6 | * regarding copyright ownership. The ASF licenses this file 7 | * to you under the Apache License, Version 2.0 (the 8 | * "License"); you may not use this file except in compliance 9 | * with the License. You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, 14 | * software distributed under the License is distributed on an 15 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | * KIND, either express or implied. See the License for the 17 | * specific language governing permissions and limitations 18 | * under the License. 19 | * 20 | */ 21 | 22 | * { 23 | margin: 1em 0; 24 | padding: 0; 25 | box-sizing: border-box; 26 | border-style: none; 27 | } 28 | 29 | *:first-child { 30 | margin-top: 0; 31 | } 32 | 33 | *:last-child { 34 | margin-bottom: 0; 35 | } 36 | 37 | html, 38 | body, 39 | div, 40 | a, 41 | br { 42 | margin: 0; 43 | } 44 | 45 | body { 46 | font: 13pt/19pt "Source Sans Pro", sans-serif; 47 | font-weight: 400; 48 | color: rgba(0, 0, 0, 0.8); 49 | text-rendering: optimizeLegibility; 50 | } 51 | 52 | code, 53 | pre { 54 | font-family: "Ubuntu Mono", monospace; 55 | } 56 | 57 | h1 { 58 | font-size: 1em; 59 | font-weight: 400; 60 | margin-bottom: 2em; 61 | } 62 | 63 | h2 { 64 | font-weight: 400; 65 | font-size: 1.4em; 66 | margin-top: 1.4em; 67 | margin-bottom: 0.9em; 68 | } 69 | 70 | h1 a, 71 | h2 a, 72 | h3 a, 73 | h4 a, 74 | h5 a, 75 | h6 a { 76 | color: inherit; 77 | } 78 | 79 | h1.selected, 80 | h2.selected, 81 | h3.selected, 82 | h4.selected, 83 | h5.selected, 84 | h6.selected { 85 | color: rgba(127, 0, 0, 0.8); 86 | } 87 | 88 | a { 89 | text-decoration: none; 90 | color: rgba(15, 63, 127, 0.8); 91 | } 92 | 93 | a.heading-link { 94 | opacity: 0; 95 | font-weight: 300; 96 | padding-left: 0.3em; 97 | padding-right: 0.3em; 98 | } 99 | 100 | a.heading-link:hover { 101 | opacity: 1; 102 | } 103 | 104 | ol, 105 | ul, 106 | dd { 107 | padding-left: 1.2em; 108 | } 109 | 110 | ul { 111 | list-style-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAdAAAAHQBMYXlgQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAABMSURBVCjPY/j//z8DFtwAxRhyuBTDQAMhDWDF9+/f/wfC2DRhVRwcHPwfhLFpwqrYxcUFjLFpwqkYlyayNJDsJLI8TVawkhVxBJMGABnYvZjQ/qzjAAAAAElFTkSuQmCC"); 112 | } 113 | 114 | li, 115 | dt, 116 | dd { 117 | margin: 0; 118 | -webkit-column-break-inside: avoid; 119 | page-break-inside: avoid; 120 | } 121 | 122 | li > ul, 123 | li > ol, 124 | dd > dl { 125 | margin-top: 0; 126 | margin-bottom: 0; 127 | } 128 | 129 | li > p:last-child, 130 | dd > p:last-child { 131 | margin-bottom: 1em; 132 | } 133 | 134 | form input, 135 | form textarea, 136 | form button { 137 | font-family: inherit; 138 | font-size: inherit; 139 | border: 1px solid #ccc; 140 | margin-bottom: 0; 141 | } 142 | 143 | form textarea { 144 | width: 16em; 145 | } 146 | 147 | form button { 148 | background-color: rgb(247, 247, 247); 149 | padding: 0 0.4em; 150 | font-weight: 300; 151 | } 152 | 153 | table { 154 | border-collapse: collapse; 155 | } 156 | 157 | td, 158 | th { 159 | border-bottom: 1px solid #ddd; 160 | padding: 0.3em 0.5em 0.3em 0; 161 | vertical-align: top; 162 | text-align: left; 163 | } 164 | 165 | td em, 166 | th { 167 | font-weight: 300; 168 | font-style: normal; 169 | } 170 | 171 | hr { 172 | border-bottom: 1px solid #ddd; 173 | } 174 | 175 | code, 176 | pre { 177 | background-color: rgb(247, 247, 247); 178 | } 179 | 180 | pre { 181 | overflow: auto; 182 | padding: 1em; 183 | } 184 | 185 | blockquote { 186 | margin-left: 2em; 187 | } 188 | 189 | section { 190 | margin: 1.5em 0; 191 | } 192 | 193 | span.placeholder { 194 | font-style: italic; 195 | } 196 | 197 | .two-column { 198 | -webkit-column-count: 2; 199 | -moz-column-count: 2; 200 | column-count: 2; 201 | } 202 | 203 | .three-column { 204 | -webkit-column-count: 3; 205 | -moz-column-count: 3; 206 | column-count: 3; 207 | } 208 | 209 | .four-column { 210 | -webkit-column-count: 4; 211 | -moz-column-count: 4; 212 | column-count: 4; 213 | } 214 | 215 | .ruled { 216 | border-top: 1px solid #ddd; 217 | padding-top: 2em; 218 | } 219 | 220 | .flex { 221 | display: flex; 222 | justify-content: space-between; 223 | } 224 | 225 | .flex > section, 226 | .flex > div { 227 | flex: 1 0 0; 228 | } 229 | 230 | #-head, #-body, #-foot { 231 | margin-bottom: 0; 232 | } 233 | 234 | #-head { 235 | padding: 1em 2em; 236 | border-bottom: 1px solid #ddd; 237 | } 238 | 239 | #-body { 240 | padding: 2em; 241 | } 242 | 243 | #-foot { 244 | border-top: 1px solid #ddd; 245 | padding: 1em 2em; 246 | } 247 | 248 | #-head-content, #-body-content, #-foot-content { 249 | max-width: 640px; 250 | margin: 0 auto; 251 | } 252 | 253 | #-head-content { 254 | font-size: 0.9em; 255 | } 256 | 257 | #-foot-content { 258 | font-size: 0.8em; 259 | } 260 | 261 | #-path-navigation { 262 | margin: 0; 263 | padding: 0; 264 | display: inline; 265 | } 266 | 267 | #-path-navigation > li { 268 | display: inline; 269 | } 270 | 271 | #-path-navigation > li:after { 272 | content: " \00a0\003e\00a0\00a0 "; 273 | } 274 | 275 | #-path-navigation > li:last-child:after { 276 | content: none; 277 | } 278 | 279 | #-global-navigation { 280 | float: right; 281 | } 282 | 283 | #requests { 284 | font-size: 1.2em; 285 | display: grid; 286 | grid: "t t t t" 287 | "u r x b"; 288 | grid-gap: 1em; 289 | grid-template-columns: repeat(4, 1fr); 290 | } 291 | 292 | #requests > * { 293 | margin: 0; 294 | } 295 | 296 | #requests > input[name="text"] { 297 | grid-area: t; 298 | width: 100%; 299 | padding: 0.2em 0.4em; 300 | } 301 | 302 | #requests input[type="checkbox"] { 303 | margin-right: 0.4em; 304 | } 305 | 306 | #requests > button { 307 | grid-area: b; 308 | font-size: 0.9em; 309 | padding: 0.3em; 310 | } 311 | 312 | #request-uppercase-option { 313 | grid-area: u; 314 | font-size: 0.9em; 315 | font-weight: 300; 316 | padding-top: 0.3em; 317 | } 318 | 319 | #request-reverse-option { 320 | grid-area: r; 321 | font-size: 0.9em; 322 | font-weight: 300; 323 | padding-top: 0.3em; 324 | } 325 | 326 | #responses { 327 | width: 100%; 328 | height: 10em; 329 | overflow: auto; 330 | padding: 0.2em 0.4em; 331 | border: 1px solid #ccc; 332 | } 333 | 334 | #responses > .response > .worker { 335 | display: inline-block; 336 | font-weight: 300; 337 | width: 15%; 338 | } 339 | 340 | #responses > .response > .cloud { 341 | display: inline-block; 342 | font-weight: 300; 343 | width: 15%; 344 | } 345 | 346 | #responses > .response > .text { 347 | display: inline; 348 | } 349 | 350 | #workers { 351 | width: 100%; 352 | } 353 | 354 | #workers th:nth-child(3), 355 | #workers th:nth-child(3), 356 | #workers th:nth-child(4), 357 | #workers td:nth-child(3), 358 | #workers td:nth-child(4) { 359 | text-align: right; 360 | } 361 | 362 | @media screen and (max-width: 720px) { 363 | .two-column, 364 | .three-column, 365 | .four-column { 366 | -webkit-column-count: 2; 367 | -moz-column-count: 2; 368 | column-count: 2; 369 | } 370 | 371 | .flex { 372 | display: block; 373 | } 374 | 375 | .flex > section { 376 | margin: 1.5em 0; 377 | } 378 | 379 | .flex > section:first-child { 380 | margin-top: 0; 381 | } 382 | 383 | .flex > section:last-child { 384 | margin-bottom: 0; 385 | } 386 | } 387 | 388 | @media screen and (max-width: 480px) { 389 | .two-column, .three-column, .four-column { 390 | -webkit-column-count: 1; 391 | -moz-column-count: 1; 392 | column-count: 1; 393 | } 394 | } 395 | 396 | @media print { 397 | body { 398 | font-size: 10pt; 399 | } 400 | 401 | pre { 402 | font-size: 0.9em; 403 | overflow: hidden; 404 | } 405 | 406 | #-head, #-foot { 407 | display: none; 408 | } 409 | 410 | #-body { 411 | padding: 0; 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /frontend/src/main/resources/webroot/gesso.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. See the NOTICE file 5 | * distributed with this work for additional information 6 | * regarding copyright ownership. The ASF licenses this file 7 | * to you under the Apache License, Version 2.0 (the 8 | * "License"); you may not use this file except in compliance 9 | * with the License. You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, 14 | * software distributed under the License is distributed on an 15 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | * KIND, either express or implied. See the License for the 17 | * specific language governing permissions and limitations 18 | * under the License. 19 | * 20 | */ 21 | 22 | "use strict"; 23 | 24 | const $ = document.querySelector.bind(document); 25 | const $$ = document.querySelectorAll.bind(document); 26 | 27 | Element.prototype.$ = function () { 28 | return this.querySelector.apply(this, arguments); 29 | }; 30 | 31 | Element.prototype.$$ = function () { 32 | return this.querySelectorAll.apply(this, arguments); 33 | }; 34 | 35 | class Gesso { 36 | constructor() { 37 | this._minFetchInterval = 500; 38 | this._maxFetchInterval = 60 * 1000; 39 | this._fetchStates = new Map(); // By path 40 | } 41 | 42 | openRequest(method, url, loadHandler) { 43 | let request = new XMLHttpRequest(); 44 | 45 | request.open(method, url); 46 | 47 | if (loadHandler != null) { 48 | request.addEventListener("load", loadHandler); 49 | } 50 | 51 | return request; 52 | } 53 | 54 | _getFetchState(path) { 55 | let state = this._fetchStates[path]; 56 | 57 | if (state == null) { 58 | state = { 59 | currentInterval: null, 60 | currentTimeoutId: null, 61 | failedAttempts: 0, 62 | etag: null, 63 | timestamp: null 64 | }; 65 | 66 | this._fetchStates[path] = state; 67 | } 68 | 69 | return state; 70 | } 71 | 72 | fetch(path, dataHandler) { 73 | console.log("Fetching data from", path); 74 | 75 | let state = this._getFetchState(path); 76 | 77 | let request = this.openRequest("GET", path, (event) => { 78 | if (event.target.status >= 200 && event.target.status < 300) { 79 | state.failedAttempts = 0; 80 | state.etag = event.target.getResponseHeader("ETag"); 81 | 82 | dataHandler(JSON.parse(event.target.responseText)); 83 | } else if (event.target.status == 304) { 84 | state.failedAttempts = 0; 85 | } 86 | 87 | state.timestamp = new Date().getTime(); 88 | }); 89 | 90 | request.addEventListener("error", (event) => { 91 | console.log("Fetch failed"); 92 | state.failedAttempts++; 93 | }); 94 | 95 | let etag = state.etag; 96 | 97 | if (etag) { 98 | request.setRequestHeader("If-None-Match", etag); 99 | } 100 | 101 | request.send(); 102 | 103 | return state; 104 | } 105 | 106 | fetchPeriodically(path, dataHandler) { 107 | let state = this._getFetchState(path); 108 | 109 | clearTimeout(state.currentTimeoutId); 110 | state.currentInterval = this._minFetchInterval; 111 | 112 | this._doFetchPeriodically(path, dataHandler, state); 113 | 114 | return state; 115 | } 116 | 117 | _doFetchPeriodically(path, dataHandler, state) { 118 | if (state.currentInterval >= this._maxFetchInterval) { 119 | setInterval(() => { 120 | this.fetch(path, dataHandler); 121 | }, this._maxFetchInterval); 122 | 123 | return; 124 | } 125 | 126 | state.currentTimeoutId = setTimeout(() => { 127 | this._doFetchPeriodically(path, dataHandler, state); 128 | }, state.currentInterval); 129 | 130 | state.currentInterval = Math.min(state.currentInterval * 2, this._maxFetchInterval); 131 | 132 | this.fetch(path, dataHandler); 133 | } 134 | 135 | parseQueryString(str) { 136 | if (str.startsWith("?")) { 137 | str = str.slice(1); 138 | } 139 | 140 | let qvars = str.split(/[&;]/); 141 | let obj = {}; 142 | 143 | for (let i = 0; i < qvars.length; i++) { 144 | let [name, value] = qvars[i].split("=", 2); 145 | 146 | name = decodeURIComponent(name); 147 | value = decodeURIComponent(value); 148 | 149 | obj[name] = value; 150 | } 151 | 152 | return obj; 153 | } 154 | 155 | emitQueryString(obj) { 156 | let tokens = []; 157 | 158 | for (let name in obj) { 159 | if (!obj.hasOwnProperty(name)) { 160 | continue; 161 | } 162 | 163 | let value = obj[name]; 164 | 165 | name = decodeURIComponent(name); 166 | value = decodeURIComponent(value); 167 | 168 | tokens.push(name + "=" + value); 169 | } 170 | 171 | return tokens.join(";"); 172 | } 173 | 174 | createElement(parent, tag, options) { 175 | let elem = document.createElement(tag); 176 | 177 | if (parent != null) { 178 | parent.appendChild(elem); 179 | } 180 | 181 | if (options != null) { 182 | if (typeof options === "string" || typeof options === "number") { 183 | this.createText(elem, options); 184 | } else if (typeof options === "object") { 185 | if (options.hasOwnProperty("text")) { 186 | this.createText(elem, options["text"]); 187 | delete options["text"]; 188 | } 189 | 190 | for (let key of Object.keys(options)) { 191 | elem.setAttribute(key, options[key]); 192 | } 193 | } else { 194 | throw `illegal argument: ${options}`; 195 | } 196 | } 197 | 198 | return elem; 199 | } 200 | 201 | createText(parent, text) { 202 | let node = document.createTextNode(text); 203 | 204 | if (parent != null) { 205 | parent.appendChild(node); 206 | } 207 | 208 | return node; 209 | } 210 | 211 | _setSelector(elem, selector) { 212 | if (selector == null) { 213 | return; 214 | } 215 | 216 | if (selector.startsWith("#")) { 217 | elem.setAttribute("id", selector.slice(1)); 218 | } else { 219 | elem.setAttribute("class", selector); 220 | } 221 | } 222 | 223 | createDiv(parent, selector, options) { 224 | let elem = this.createElement(parent, "div", options); 225 | 226 | this._setSelector(elem, selector); 227 | 228 | return elem; 229 | } 230 | 231 | createSpan(parent, selector, options) { 232 | let elem = this.createElement(parent, "span", options); 233 | 234 | this._setSelector(elem, selector); 235 | 236 | return elem; 237 | } 238 | 239 | createLink(parent, href, options) { 240 | let elem = this.createElement(parent, "a", options); 241 | 242 | if (href != null) { 243 | elem.setAttribute("href", href); 244 | } 245 | 246 | return elem; 247 | } 248 | 249 | createTable(parent, headings, rows, options) { 250 | let elem = this.createElement(parent, "table", options); 251 | let thead = this.createElement(elem, "thead"); 252 | let tbody = this.createElement(elem, "tbody"); 253 | 254 | if (headings) { 255 | let tr = this.createElement(thead, "tr"); 256 | 257 | for (let heading of headings) { 258 | this.createElement(tr, "th", heading); 259 | } 260 | } 261 | 262 | for (let row of rows) { 263 | let tr = this.createElement(tbody, "tr"); 264 | 265 | for (let cell of row) { 266 | this.createElement(tr, "td", cell); 267 | } 268 | } 269 | 270 | return elem; 271 | } 272 | 273 | replaceElement(oldElement, newElement) { 274 | oldElement.parentNode.replaceChild(newElement, oldElement); 275 | } 276 | 277 | formatDuration(milliseconds) { 278 | if (milliseconds == null) { 279 | return "-"; 280 | } 281 | 282 | let seconds = Math.round(milliseconds / 1000); 283 | let minutes = Math.round(milliseconds / 60 / 1000); 284 | let hours = Math.round(milliseconds / 3600 / 1000); 285 | let days = Math.round(milliseconds / 86400 / 1000); 286 | let weeks = Math.round(milliseconds / 432000 / 1000); 287 | 288 | if (weeks >= 2) { 289 | return `${weeks} weeks`; 290 | } 291 | 292 | if (days >= 2) { 293 | return `${days} days`; 294 | } 295 | 296 | if (hours >= 1) { 297 | return `${hours} hours`; 298 | } 299 | 300 | if (minutes >= 1) { 301 | return `${minutes} minutes`; 302 | } 303 | 304 | if (seconds === 1) { 305 | return "1 second"; 306 | } 307 | 308 | return `${seconds} seconds`; 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /worker/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Template 4 | labels: 5 | template: amq-broker-71-basic 6 | xpaas: 1.4.12 7 | message: A new messaging service has been created in your project. It will handle the protocol(s) "${AMQ_PROTOCOL}". The username/password for accessing the service is ${AMQ_USER}/${AMQ_PASSWORD}. 8 | metadata: 9 | annotations: 10 | description: Application template for Red Hat AMQ brokers. These can be deployed as standalone or in a mesh. This template doesn't feature SSL support. 11 | iconClass: icon-amq 12 | openshift.io/display-name: Red Hat AMQ Broker 7.1 (Ephemeral, no SSL) 13 | openshift.io/provider-display-name: Red Hat, Inc. 14 | tags: messaging,amq,jboss 15 | template.openshift.io/documentation-url: 'https://access.redhat.com/documentation/en/red-hat-amq/' 16 | template.openshift.io/long-description: >- 17 | This template defines resources needed to develop Red Hat AMQ Broker 7.1 based application, including a deployment configuration, using ephemeral (temporary) storage. These can be deployed as standalone or in a mesh. 18 | template.openshift.io/support-url: 'https://access.redhat.com' 19 | version: 1.4.12 20 | name: amq-broker-71-basic 21 | objects: 22 | - apiVersion: v1 23 | kind: Service 24 | metadata: 25 | annotations: 26 | description: The broker's console and Jolokia port. 27 | labels: 28 | application: ${APPLICATION_NAME} 29 | name: ${APPLICATION_NAME}-amq-jolokia 30 | spec: 31 | ports: 32 | - port: 8161 33 | targetPort: 8161 34 | selector: 35 | deploymentConfig: ${APPLICATION_NAME}-amq 36 | - apiVersion: v1 37 | kind: Service 38 | metadata: 39 | annotations: 40 | description: The broker's AMQP port. 41 | labels: 42 | application: ${APPLICATION_NAME} 43 | name: ${APPLICATION_NAME}-amq-amqp 44 | spec: 45 | ports: 46 | - port: 5672 47 | targetPort: 5672 48 | selector: 49 | deploymentConfig: ${APPLICATION_NAME}-amq 50 | - apiVersion: v1 51 | kind: Service 52 | metadata: 53 | annotations: 54 | description: The broker's MQTT port. 55 | labels: 56 | application: ${APPLICATION_NAME} 57 | name: ${APPLICATION_NAME}-amq-mqtt 58 | spec: 59 | ports: 60 | - port: 1883 61 | targetPort: 1883 62 | selector: 63 | deploymentConfig: ${APPLICATION_NAME}-amq 64 | - apiVersion: v1 65 | kind: Service 66 | metadata: 67 | annotations: 68 | description: The broker's STOMP port. 69 | labels: 70 | application: ${APPLICATION_NAME} 71 | name: ${APPLICATION_NAME}-amq-stomp 72 | spec: 73 | ports: 74 | - port: 61613 75 | targetPort: 61613 76 | selector: 77 | deploymentConfig: ${APPLICATION_NAME}-amq 78 | - apiVersion: v1 79 | kind: Service 80 | metadata: 81 | annotations: 82 | description: The broker's OpenWire port. 83 | service.alpha.openshift.io/dependencies: '[{"name": "${APPLICATION_NAME}-amq-amqp", 84 | "kind": "Service"},{"name": "${APPLICATION_NAME}-amq-mqtt", "kind": "Service"},{"name": 85 | "${APPLICATION_NAME}-amq-stomp", "kind": "Service"}]' 86 | labels: 87 | application: ${APPLICATION_NAME} 88 | name: ${APPLICATION_NAME}-amq-tcp 89 | spec: 90 | ports: 91 | - port: 61616 92 | targetPort: 61616 93 | selector: 94 | deploymentConfig: ${APPLICATION_NAME}-amq 95 | - apiVersion: v1 96 | kind: DeploymentConfig 97 | metadata: 98 | labels: 99 | application: ${APPLICATION_NAME} 100 | name: ${APPLICATION_NAME}-amq 101 | spec: 102 | replicas: 1 103 | selector: 104 | deploymentConfig: ${APPLICATION_NAME}-amq 105 | strategy: 106 | rollingParams: 107 | maxSurge: 0 108 | type: Rolling 109 | template: 110 | metadata: 111 | labels: 112 | application: ${APPLICATION_NAME} 113 | deploymentConfig: ${APPLICATION_NAME}-amq 114 | name: ${APPLICATION_NAME}-amq 115 | spec: 116 | containers: 117 | - env: 118 | - name: AMQ_USER 119 | value: ${AMQ_USER} 120 | - name: AMQ_PASSWORD 121 | value: ${AMQ_PASSWORD} 122 | - name: AMQ_ROLE 123 | value: ${AMQ_ROLE} 124 | - name: AMQ_NAME 125 | value: ${AMQ_NAME} 126 | - name: AMQ_TRANSPORTS 127 | value: ${AMQ_PROTOCOL} 128 | - name: AMQ_QUEUES 129 | value: ${AMQ_QUEUES} 130 | - name: AMQ_ADDRESSES 131 | value: ${AMQ_ADDRESSES} 132 | - name: AMQ_GLOBAL_MAX_SIZE 133 | value: ${AMQ_GLOBAL_MAX_SIZE} 134 | - name: AMQ_ALLOW_ANONYMOUS 135 | value: ${AMQ_ALLOW_ANONYMOUS} 136 | image: amq-broker-71-openshift 137 | imagePullPolicy: Always 138 | readinessProbe: 139 | exec: 140 | command: 141 | - "/bin/bash" 142 | - "-c" 143 | - "/opt/amq/bin/readinessProbe.sh" 144 | name: ${APPLICATION_NAME}-amq 145 | ports: 146 | - containerPort: 8161 147 | name: console-jolokia 148 | protocol: TCP 149 | - containerPort: 5672 150 | name: amqp 151 | protocol: TCP 152 | - containerPort: 1883 153 | name: mqtt 154 | protocol: TCP 155 | - containerPort: 61613 156 | name: stomp 157 | protocol: TCP 158 | - containerPort: 61616 159 | name: artemis 160 | protocol: TCP 161 | terminationGracePeriodSeconds: 60 162 | triggers: 163 | - imageChangeParams: 164 | automatic: true 165 | containerNames: 166 | - ${APPLICATION_NAME}-amq 167 | from: 168 | kind: ImageStreamTag 169 | name: amq-broker-71-openshift:1.0 170 | namespace: ${IMAGE_STREAM_NAMESPACE} 171 | type: ImageChange 172 | - type: ConfigChange 173 | - apiVersion: v1 174 | kind: Route 175 | metadata: 176 | labels: 177 | application: ${APPLICATION_NAME} 178 | name: console 179 | spec: 180 | to: 181 | kind: Service 182 | name: ${APPLICATION_NAME}-amq-jolokia 183 | - kind: ImageStream 184 | apiVersion: v1 185 | metadata: 186 | name: amq-broker-71-openshift 187 | annotations: 188 | openshift.io/display-name: Red Hat AMQ Broker 7.1 189 | openshift.io/provider-display-name: Red Hat, Inc. 190 | spec: 191 | lookupPolicy: 192 | local: true 193 | tags: 194 | - name: '1.0' 195 | annotations: 196 | description: Red Hat AMQ Broker 7.1 image. 197 | iconClass: icon-jboss 198 | tags: messaging,amq,jboss,xpaas 199 | supports: amq:7.1,messaging:1.0 200 | version: '1.0' 201 | from: 202 | kind: DockerImage 203 | name: registry.access.redhat.com/amq-broker-7-tech-preview/amq-broker-71-openshift:1.0 204 | - apiVersion: v1 205 | kind: ConfigMap 206 | metadata: 207 | name: messaging-service 208 | data: 209 | MESSAGING_SERVICE_HOST: work-queue-broker-amq-amqp 210 | MESSAGING_SERVICE_PORT: "5672" 211 | parameters: 212 | - description: The name for the application. 213 | displayName: Application Name 214 | name: APPLICATION_NAME 215 | required: true 216 | value: broker 217 | - description: 'Protocols to configure, separated by commas. Allowed values are: `openwire`, `amqp`, `stomp`, `mqtt` and `hornetq`.' 218 | displayName: AMQ Protocols 219 | name: AMQ_PROTOCOL 220 | value: openwire,amqp,stomp,mqtt,hornetq 221 | - description: Queue names, separated by commas. These queues will be automatically created when the broker starts. If left empty, queues will be still created dynamically. 222 | displayName: Queues 223 | name: AMQ_QUEUES 224 | - description: Address names, separated by commas. These addresses will be automatically created when the broker starts. If left empty, addresses will be still created dynamically. 225 | displayName: Addresses 226 | name: AMQ_ADDRESSES 227 | - description: User name for standard broker user. It is required for connecting to the broker. If left empty, it will be generated. 228 | displayName: AMQ Username 229 | from: user[a-zA-Z0-9]{3} 230 | generate: expression 231 | name: AMQ_USER 232 | - description: Password for standard broker user. It is required for connecting to the broker. If left empty, it will be generated. 233 | displayName: AMQ Password 234 | from: '[a-zA-Z0-9]{8}' 235 | generate: expression 236 | name: AMQ_PASSWORD 237 | - description: User role for standard broker user. 238 | displayName: AMQ Role 239 | name: AMQ_ROLE 240 | value: admin 241 | - description: The name of the broker 242 | displayName: AMQ Name 243 | name: AMQ_NAME 244 | value: broker 245 | - description: "Maximum amount of memory which message data may consume (Default: Undefined, half of the system's memory)." 246 | displayName: AMQ Global Max Size 247 | name: AMQ_GLOBAL_MAX_SIZE 248 | value: 100 gb 249 | - description: "Determines whether or not the broker will allow anonymous access." 250 | displayName: AMQ Allow Anonymous 251 | name: AMQ_ALLOW_ANONYMOUS 252 | - description: Namespace in which the ImageStreams for Red Hat Middleware images are installed. These ImageStreams are normally installed in the openshift namespace. You should only need to modify this if you've installed the ImageStreams in a different namespace/project. 253 | displayName: ImageStream Namespace 254 | name: IMAGE_STREAM_NAMESPACE 255 | required: true 256 | value: openshift 257 | 258 | -------------------------------------------------------------------------------- /frontend/src/main/java/io/openshift/booster/messaging/Frontend.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package io.openshift.booster.messaging; 19 | 20 | import io.reactivex.Completable; 21 | import io.vertx.core.Future; 22 | import io.vertx.core.json.Json; 23 | import io.vertx.core.logging.Logger; 24 | import io.vertx.core.logging.LoggerFactory; 25 | import io.vertx.proton.ProtonClient; 26 | import io.vertx.proton.ProtonConnection; 27 | import io.vertx.proton.ProtonReceiver; 28 | import io.vertx.proton.ProtonSender; 29 | import io.vertx.reactivex.CompletableHelper; 30 | import io.vertx.reactivex.config.ConfigRetriever; 31 | import io.vertx.reactivex.core.AbstractVerticle; 32 | import io.vertx.reactivex.core.impl.AsyncResultCompletable; 33 | import io.vertx.reactivex.ext.web.Router; 34 | import io.vertx.reactivex.ext.web.RoutingContext; 35 | import io.vertx.reactivex.ext.web.handler.BodyHandler; 36 | import io.vertx.reactivex.ext.web.handler.StaticHandler; 37 | import org.apache.qpid.proton.amqp.messaging.AmqpValue; 38 | import org.apache.qpid.proton.amqp.messaging.ApplicationProperties; 39 | import org.apache.qpid.proton.amqp.messaging.Source; 40 | import org.apache.qpid.proton.message.Message; 41 | 42 | import java.util.*; 43 | import java.util.concurrent.ConcurrentLinkedQueue; 44 | import java.util.concurrent.atomic.AtomicInteger; 45 | 46 | public class Frontend extends AbstractVerticle { 47 | private static final Logger LOGGER = LoggerFactory.getLogger(Frontend.class); 48 | private static final String ID = "frontend-vertx-" + UUID.randomUUID() 49 | .toString().substring(0, 4); 50 | 51 | private ProtonSender requestSender; 52 | private ProtonReceiver responseReceiver; 53 | 54 | private final AtomicInteger requestSequence = new AtomicInteger(0); 55 | private final Queue requestMessages = new ConcurrentLinkedQueue<>(); 56 | private final Data data = new Data(); 57 | 58 | @Override 59 | public void start(Future future) { 60 | Router router = Router.router(vertx); 61 | router.route().handler(BodyHandler.create()); 62 | router.post("/api/send-request").handler(this::handleSendRequest); 63 | router.get("/api/receive-response").handler(this::handleReceiveResponse); 64 | router.get("/api/data").handler(this::handleGetData); 65 | router.get("/health").handler(rc -> rc.response().end("OK")); 66 | router.get("/*").handler(StaticHandler.create()); 67 | 68 | ConfigRetriever.create(vertx).rxGetConfig() 69 | .flatMapCompletable(json -> { 70 | String amqpHost = json.getString("MESSAGING_SERVICE_HOST", "localhost"); 71 | int amqpPort = json.getInteger("MESSAGING_SERVICE_PORT", 5672); 72 | //String amqpUser = json.getString("MESSAGING_SERVICE_USER", "work-queue"); 73 | String amqpUser = json.getString("MESSAGING_SERVICE_USER", ""); 74 | // String amqpPassword = json.getString("MESSAGING_SERVICE_PASSWORD", "work-queue"); 75 | String amqpPassword = json.getString("MESSAGING_SERVICE_PASSWORD", ""); 76 | 77 | String httpHost = json.getString("HTTP_HOST", "0.0.0.0"); 78 | int httpPort = json.getInteger("HTTP_PORT", 8080); 79 | 80 | // AMQP 81 | ProtonClient client = ProtonClient.create(vertx.getDelegate()); 82 | Future connected = Future.future(); 83 | client.connect(amqpHost, amqpPort, amqpUser, amqpPassword, result -> { 84 | if (result.failed()) { 85 | LOGGER.error("MESSAGING_SERVICE_HOST " + amqpHost); 86 | LOGGER.error("MESSAGING_SERVICE_PORT " + amqpPort); 87 | connected.fail(result.cause()); 88 | } else { 89 | ProtonConnection conn = result.result(); 90 | conn.setContainer(ID); 91 | conn.open(); 92 | 93 | sendRequests(conn); 94 | receiveWorkerUpdates(conn); 95 | pruneStaleWorkers(); 96 | connected.complete(); 97 | } 98 | }); 99 | 100 | Completable brokerConnected = new AsyncResultCompletable(connected::setHandler); 101 | Completable serverStarted = vertx.createHttpServer() 102 | .requestHandler(router::accept) 103 | .rxListen(httpPort, httpHost) 104 | .toCompletable(); 105 | 106 | return brokerConnected.andThen(serverStarted); 107 | }) 108 | .subscribe(CompletableHelper.toObserver(future)); 109 | } 110 | 111 | private void sendRequests(ProtonConnection conn) { 112 | requestSender = conn.createSender("work-requests"); 113 | 114 | // Using a null address and setting the source dynamic tells 115 | // the remote peer to generate the reply address. 116 | responseReceiver = conn.createReceiver(null); 117 | Source source = (Source) responseReceiver.getSource(); 118 | source.setDynamic(true); 119 | 120 | responseReceiver.openHandler(result -> requestSender.sendQueueDrainHandler(s -> doSendRequests())); 121 | 122 | responseReceiver.handler((delivery, message) -> { 123 | Map props = message.getApplicationProperties().getValue(); 124 | String workerId = (String) props.get("workerId"); 125 | String cloudId = (String) props.get("AMQ_LOCATION_KEY"); 126 | String requestId = (String) message.getCorrelationId(); 127 | String text = (String) ((AmqpValue) message.getBody()).getValue(); 128 | 129 | /* 130 | trim down to the relevant substring 131 | */ 132 | 133 | int lastIndex = workerId.lastIndexOf("-"); 134 | String uniquePart = workerId.substring(lastIndex + 1); 135 | // LOGGER.info("BURR: " + uniquePart); 136 | Response response = new Response(requestId, uniquePart, cloudId, text); 137 | 138 | data.getResponses().put(response.getRequestId(), response); 139 | 140 | LOGGER.info("{0}: Received {1}", ID, response); 141 | }); 142 | 143 | requestSender.open(); 144 | responseReceiver.open(); 145 | } 146 | 147 | private void doSendRequests() { 148 | if (responseReceiver == null) { 149 | return; 150 | } 151 | 152 | if (responseReceiver.getRemoteSource().getAddress() == null) { 153 | return; 154 | } 155 | 156 | while (!requestSender.sendQueueFull()) { 157 | Message message = requestMessages.poll(); 158 | 159 | if (message == null) { 160 | break; 161 | } 162 | 163 | message.setReplyTo(responseReceiver.getRemoteSource().getAddress()); 164 | 165 | requestSender.send(message); 166 | 167 | LOGGER.info("{0}: Sent {1}", ID, message); 168 | } 169 | } 170 | 171 | private void receiveWorkerUpdates(ProtonConnection conn) { 172 | ProtonReceiver receiver = conn.createReceiver("worker-updates"); 173 | 174 | receiver.handler((delivery, message) -> { 175 | Map props = message.getApplicationProperties().getValue(); 176 | String workerId = (String) props.get("workerId"); 177 | String cloud = (String) props.get("AMQ_LOCATION_KEY"); 178 | long timestamp = (long) props.get("timestamp"); 179 | long requestsProcessed = (long) props.get("requestsProcessed"); 180 | long processingErrors = (long) props.get("processingErrors"); 181 | 182 | WorkerUpdate update = new WorkerUpdate(workerId, cloud, timestamp, requestsProcessed, 183 | processingErrors); 184 | 185 | data.getWorkers().put(update.getWorkerId(), update); 186 | }); 187 | 188 | receiver.open(); 189 | } 190 | 191 | private void handleSendRequest(RoutingContext rc) { 192 | String json = rc.getBodyAsString(); 193 | String requestId = ID + "/" + requestSequence.incrementAndGet(); 194 | Request request = Json.decodeValue(json, Request.class); 195 | Map props = new HashMap<>(); 196 | props.put("uppercase", request.isUppercase()); 197 | props.put("reverse", request.isReverse()); 198 | 199 | Message message = Message.Factory.create(); 200 | message.setMessageId(requestId); 201 | message.setAddress("work-requests"); 202 | message.setBody(new AmqpValue(request.getText())); 203 | message.setApplicationProperties(new ApplicationProperties(props)); 204 | 205 | requestMessages.add(message); 206 | 207 | data.getRequestIds().add(requestId); 208 | 209 | doSendRequests(); 210 | 211 | rc.response().setStatusCode(202).end(requestId); 212 | } 213 | 214 | private void handleReceiveResponse(RoutingContext rc) { 215 | String value = rc.request().getParam("request"); 216 | 217 | if (value == null) { 218 | rc.fail(500); 219 | return; 220 | } 221 | 222 | Response response = data.getResponses().get(value); 223 | 224 | if (response == null) { 225 | rc.response().setStatusCode(404).end(); 226 | return; 227 | } 228 | 229 | rc.response() 230 | .setStatusCode(200) 231 | .putHeader("Content-Type", "application/json; charset=utf-8") 232 | .end(Json.encodePrettily(response)); 233 | } 234 | 235 | private void handleGetData(RoutingContext rc) { 236 | rc.response() 237 | .putHeader("Content-Type", "application/json; charset=utf-8") 238 | .end(Json.encodePrettily(data)); 239 | } 240 | 241 | private void pruneStaleWorkers() { 242 | vertx.setPeriodic(5000, timer -> { 243 | LOGGER.debug("{0}: Pruning stale workers", ID); 244 | 245 | Map workers = data.getWorkers(); 246 | long now = System.currentTimeMillis(); 247 | 248 | for (Map.Entry entry : workers.entrySet()) { 249 | String workerId = entry.getKey(); 250 | WorkerUpdate update = entry.getValue(); 251 | 252 | if (now - update.getTimestamp() > 10 * 1000) { 253 | workers.remove(workerId); 254 | LOGGER.info("{0}: Pruned {1}", ID, workerId); 255 | } 256 | } 257 | }); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /worker/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------