├── .github └── ISSUE_TEMPLATE ├── .gitignore ├── ExampleVotingApp.sln ├── LICENSE ├── MAINTAINERS ├── README.md ├── architecture.png ├── azure-pipelines.yml ├── docker-compose-javaworker.yml ├── docker-compose-k8s.yml ├── docker-compose-simple.yml ├── docker-compose-windows-1809.yml ├── docker-compose-windows.yml ├── docker-compose.seed.yml ├── docker-compose.yml ├── docker-stack-simple.yml ├── docker-stack-windows-1809.yml ├── docker-stack-windows.yml ├── docker-stack.yml ├── dockercloud.yml ├── healthchecks ├── postgres.sh └── redis.sh ├── k8s-specifications ├── db-deployment.yaml ├── db-service.yaml ├── redis-deployment.yaml ├── redis-service.yaml ├── result-deployment.yaml ├── result-service.yaml ├── vote-deployment.yaml ├── vote-namespace.yml ├── vote-service.yaml └── worker-deployment.yaml ├── kube-deployment.yml ├── result ├── .dockerignore ├── .vscode │ └── launch.json ├── Dockerfile ├── docker-compose.test.yml ├── dotnet │ ├── Dockerfile │ ├── Dockerfile.1809 │ └── Result │ │ ├── Data │ │ ├── IResultData.cs │ │ └── MySqlResultData.cs │ │ ├── Hubs │ │ └── ResultsHub.cs │ │ ├── Models │ │ └── ResultsModel.cs │ │ ├── Pages │ │ ├── Index.cshtml │ │ └── Index.cshtml.cs │ │ ├── Program.cs │ │ ├── Properties │ │ └── launchSettings.json │ │ ├── Result.csproj │ │ ├── Startup.cs │ │ ├── Timers │ │ └── PublishResultsTimer.cs │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── libman.json │ │ └── wwwroot │ │ ├── css │ │ ├── site.css │ │ └── site.min.css │ │ ├── js │ │ └── results.js │ │ └── lib │ │ └── signalr │ │ └── dist │ │ └── browser │ │ ├── signalr.js │ │ └── signalr.min.js ├── package-lock.json ├── package.json ├── server.js ├── tests │ ├── Dockerfile │ ├── render.js │ └── tests.sh └── views │ ├── angular.min.js │ ├── app.js │ ├── index.html │ ├── socket.io.js │ └── stylesheets │ └── style.css ├── seed-data ├── Dockerfile ├── generate-votes.sh └── make-data.py ├── vote ├── Dockerfile ├── app.py ├── dotnet │ ├── Dockerfile │ ├── Dockerfile.1809 │ └── Vote │ │ ├── Messaging │ │ ├── IMessageQueue.cs │ │ ├── MessageHelper.cs │ │ ├── MessageQueue.cs │ │ └── Messages │ │ │ ├── Events │ │ │ └── VoteCastEvent.cs │ │ │ └── Message.cs │ │ ├── Pages │ │ ├── Index.cshtml │ │ └── Index.cshtml.cs │ │ ├── Program.cs │ │ ├── Properties │ │ └── launchSettings.json │ │ ├── Startup.cs │ │ ├── Vote.csproj │ │ ├── Vote.sln │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ └── wwwroot │ │ ├── css │ │ ├── site.css │ │ └── site.min.css │ │ └── js │ │ └── jquery-1.11.1-min.js ├── requirements.txt ├── static │ └── stylesheets │ │ └── style.css └── templates │ └── index.html └── worker ├── .classpath ├── .project ├── .settings ├── org.eclipse.jdt.apt.core.prefs ├── org.eclipse.jdt.core.prefs └── org.eclipse.m2e.core.prefs ├── Dockerfile ├── Dockerfile.j ├── Dockerfile.net ├── dotnet ├── Dockerfile ├── Dockerfile.1809 └── Worker │ ├── Data │ ├── IVoteData.cs │ └── MySqlVoteData.cs │ ├── Entities │ ├── Vote.cs │ └── VoteContext.cs │ ├── Messaging │ ├── IMessageQueue.cs │ ├── MessageHelper.cs │ ├── MessageQueue.cs │ └── Messages │ │ ├── Events │ │ └── VoteCastEvent.cs │ │ └── Message.cs │ ├── Program.cs │ ├── Worker.csproj │ ├── Workers │ └── QueueWorker.cs │ └── appsettings.json ├── pom.xml ├── src ├── Worker │ ├── Program.cs │ └── Worker.csproj └── main │ └── java │ └── worker │ └── Worker.java └── target └── classes └── worker └── Worker.class /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | ** PLEASE ONLY USE THIS ISSUE TRACKER TO SUBMIT ISSUES WITH THE EXAMPLE VOTING APP ** 2 | 3 | * If you have a bug working with Docker itself, not related to these labs, please file the bug on the [Docker repo](https://github.com/docker/docker) * 4 | * If you would like general support figuring out how to do something with Docker, please use the Docker Slack channel. If you're not on that channel, sign up for the [Docker Community](http://dockr.ly/MeetUp) and you'll get an invite. * 5 | * Or go to the [Docker Forums](https://forums.docker.com/) * 6 | 7 | Please provide the following information so we can assess the issue you're having 8 | 9 | **Description** 10 | 11 | 14 | 15 | **Steps to reproduce the issue, if relevant:** 16 | 1. 17 | 2. 18 | 3. 19 | 20 | **Describe the results you received:** 21 | 22 | 23 | **Describe the results you expected:** 24 | 25 | 26 | **Additional information you deem important (e.g. issue happens only occasionally):** 27 | 28 | **Output of `docker version`:** 29 | 30 | ``` 31 | (paste your output here) 32 | ``` 33 | 34 | **Output of `docker info`:** 35 | 36 | ``` 37 | (paste your output here) 38 | ``` 39 | 40 | **Additional environment details (AWS, Docker for Mac, Docker for Windows, VirtualBox, physical, etc.):** 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | project.lock.json 3 | bin/ 4 | obj/ -------------------------------------------------------------------------------- /ExampleVotingApp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2036 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vote", "vote\dotnet\Vote\Vote.csproj", "{9687EAF5-BFF3-4F8D-9C78-1B8EE12CE091}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker", "worker\dotnet\Worker\Worker.csproj", "{083764E8-4C34-43FB-A468-F80CE0ADE10A}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Result", "result\dotnet\Result\Result.csproj", "{9AD16D72-E3F5-4A76-814C-40EBD1EE7892}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {9687EAF5-BFF3-4F8D-9C78-1B8EE12CE091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {9687EAF5-BFF3-4F8D-9C78-1B8EE12CE091}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {9687EAF5-BFF3-4F8D-9C78-1B8EE12CE091}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {9687EAF5-BFF3-4F8D-9C78-1B8EE12CE091}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {083764E8-4C34-43FB-A468-F80CE0ADE10A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {083764E8-4C34-43FB-A468-F80CE0ADE10A}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {083764E8-4C34-43FB-A468-F80CE0ADE10A}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {083764E8-4C34-43FB-A468-F80CE0ADE10A}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {9AD16D72-E3F5-4A76-814C-40EBD1EE7892}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {9AD16D72-E3F5-4A76-814C-40EBD1EE7892}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {9AD16D72-E3F5-4A76-814C-40EBD1EE7892}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {9AD16D72-E3F5-4A76-814C-40EBD1EE7892}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {9DEFC158-1225-4393-8A38-22256211D43D} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2016 Docker, Inc. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Bret Fisher 2 | Michael Irwin 3 | 4 | # Alumni, thanks for your work! 5 | Aanand Prasad 6 | Ben Firshman 7 | Fernando Mayo 8 | Mano Marks 9 | Maxime Heckel 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Example Voting App 2 | ========= 3 | 4 | A simple distributed application running across multiple Docker containers. 5 | 6 | Getting started 7 | --------------- 8 | 9 | Download [Docker Desktop](https://www.docker.com/products/docker-desktop) for Mac or Windows. [Docker Compose](https://docs.docker.com/compose) will be automatically installed. On Linux, make sure you have the latest version of [Compose](https://docs.docker.com/compose/install/). 10 | 11 | 12 | ## Linux Containers 13 | 14 | The Linux stack uses Python, Node.js, .NET Core (or optionally Java), with Redis for messaging and Postgres for storage. 15 | 16 | > If you're using [Docker Desktop on Windows](https://store.docker.com/editions/community/docker-ce-desktop-windows), you can run the Linux version by [switching to Linux containers](https://docs.docker.com/docker-for-windows/#switch-between-windows-and-linux-containers), or run the Windows containers version. 17 | 18 | Run in this directory: 19 | ``` 20 | docker-compose up 21 | ``` 22 | The app will be running at [http://localhost:5000](http://localhost:5000), and the results will be at [http://localhost:5001](http://localhost:5001). 23 | 24 | Alternately, if you want to run it on a [Docker Swarm](https://docs.docker.com/engine/swarm/), first make sure you have a swarm. If you don't, run: 25 | ``` 26 | docker swarm init 27 | ``` 28 | Once you have your swarm, in this directory run: 29 | ``` 30 | docker stack deploy --compose-file docker-stack.yml vote 31 | ``` 32 | 33 | ## Windows Containers 34 | 35 | An alternative version of the app uses Windows containers based on Nano Server. This stack runs on .NET Core, using [NATS](https://nats.io) for messaging and [TiDB](https://github.com/pingcap/tidb) for storage. 36 | 37 | You can build from source using: 38 | 39 | ``` 40 | docker-compose -f docker-compose-windows.yml build 41 | ``` 42 | 43 | Then run the app using: 44 | 45 | ``` 46 | docker-compose -f docker-compose-windows.yml up -d 47 | ``` 48 | 49 | > Or in a Windows swarm, run `docker stack deploy -c docker-stack-windows.yml vote` 50 | 51 | The app will be running at [http://localhost:5000](http://localhost:5000), and the results will be at [http://localhost:5001](http://localhost:5001). 52 | 53 | 54 | Run the app in Kubernetes 55 | ------------------------- 56 | 57 | The folder k8s-specifications contains the yaml specifications of the Voting App's services. 58 | 59 | First create the vote namespace 60 | 61 | ``` 62 | $ kubectl create namespace vote 63 | ``` 64 | 65 | Run the following command to create the deployments and services objects: 66 | ``` 67 | $ kubectl create -f k8s-specifications/ 68 | deployment "db" created 69 | service "db" created 70 | deployment "redis" created 71 | service "redis" created 72 | deployment "result" created 73 | service "result" created 74 | deployment "vote" created 75 | service "vote" created 76 | deployment "worker" created 77 | ``` 78 | 79 | The vote interface is then available on port 31000 on each host of the cluster, the result one is available on port 31001. 80 | 81 | Architecture 82 | ----- 83 | 84 | ![Architecture diagram](architecture.png) 85 | 86 | * A front-end web app in [Python](/vote) or [ASP.NET Core](/vote/dotnet) which lets you vote between two options 87 | * A [Redis](https://hub.docker.com/_/redis/) or [NATS](https://hub.docker.com/_/nats/) queue which collects new votes 88 | * A [.NET Core](/worker/src/Worker), [Java](/worker/src/main) or [.NET Core 2.1](/worker/dotnet) worker which consumes votes and stores them in… 89 | * A [Postgres](https://hub.docker.com/_/postgres/) or [TiDB](https://hub.docker.com/r/dockersamples/tidb/tags/) database backed by a Docker volume 90 | * A [Node.js](/result) or [ASP.NET Core SignalR](/result/dotnet) webapp which shows the results of the voting in real time 91 | 92 | 93 | Notes 94 | ----- 95 | 96 | The voting application only accepts one vote per client. It does not register votes if a vote has already been submitted from a client. 97 | 98 | This isn't an example of a properly architected perfectly designed distributed app... it's just a simple 99 | example of the various types of pieces and languages you might see (queues, persistent data, etc), and how to 100 | deal with them in Docker at a basic level. 101 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmumshad/example-voting-app/38826ff7c00c398203f3074c18c9fcee41e1a512/architecture.png -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Docker image 2 | # Build a Docker image to deploy, run, or push to a container registry. 3 | # Add steps that use Docker Compose, tag images, push to a registry, run an image, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/docker 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'Ubuntu-16.04' 11 | 12 | variables: 13 | acrId: acrtestmumshad 14 | acrPswd: LRoyTf/oFUZMdHEOXb7bNidtC=lJUlUN 15 | 16 | steps: 17 | - script: | 18 | docker build -t $(acrId).azurecr.io/result:$(build.buildId) ./result 19 | displayName: 'Build result' 20 | 21 | - script: docker build -t $(acrId).azurecr.io/vote:$(build.buildId) ./vote 22 | displayName: 'Build vote' 23 | 24 | - script: docker build -t $(acrId).azurecr.io/worker:$(build.buildId) ./worker 25 | displayName: 'Build worker' 26 | 27 | - script: | 28 | docker login -u $(acrId) -p $(acrPswd) $(acrId).azurecr.io 29 | docker push $(acrId).azurecr.io/result:$(build.buildId) 30 | docker push $(acrId).azurecr.io/vote:$(build.buildId) 31 | docker push $(acrId).azurecr.io/worker:$(build.buildId) 32 | displayName: 'Push all images' -------------------------------------------------------------------------------- /docker-compose-javaworker.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | vote: 5 | build: ./vote 6 | command: python app.py 7 | volumes: 8 | - ./vote:/app 9 | ports: 10 | - "5000:80" 11 | networks: 12 | - front-tier 13 | - back-tier 14 | 15 | result: 16 | build: ./result 17 | command: nodemon server.js 18 | volumes: 19 | - ./result:/app 20 | ports: 21 | - "5001:80" 22 | - "5858:5858" 23 | networks: 24 | - front-tier 25 | - back-tier 26 | 27 | worker: 28 | build: 29 | context: ./worker 30 | dockerfile: Dockerfile.j 31 | networks: 32 | - back-tier 33 | 34 | redis: 35 | image: redis:alpine 36 | container_name: redis 37 | ports: ["6379"] 38 | networks: 39 | - back-tier 40 | 41 | db: 42 | image: postgres:9.4 43 | container_name: db 44 | environment: 45 | POSTGRES_USER: "postgres" 46 | POSTGRES_PASSWORD: "postgres" 47 | volumes: 48 | - "db-data:/var/lib/postgresql/data" 49 | networks: 50 | - back-tier 51 | 52 | volumes: 53 | db-data: 54 | 55 | networks: 56 | front-tier: 57 | back-tier: 58 | -------------------------------------------------------------------------------- /docker-compose-k8s.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | redis: 5 | image: redis:alpine 6 | ports: 7 | - "6379:6379" 8 | db: 9 | image: postgres:9.4 10 | environment: 11 | POSTGRES_USER: "postgres" 12 | POSTGRES_PASSWORD: "postgres" 13 | ports: 14 | - "5432:5432" 15 | vote: 16 | image: dockersamples/examplevotingapp_vote:before 17 | ports: 18 | - "5000:80" 19 | deploy: 20 | replicas: 1 21 | result: 22 | image: dockersamples/examplevotingapp_result:before 23 | ports: 24 | - "5001:80" 25 | worker: 26 | image: dockersamples/examplevotingapp_worker 27 | visualizer: 28 | image: dockersamples/visualizer:stable 29 | ports: 30 | - "8080:8080" 31 | -------------------------------------------------------------------------------- /docker-compose-simple.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | vote: 5 | build: ./vote 6 | command: python app.py 7 | volumes: 8 | - ./vote:/app 9 | ports: 10 | - "5000:80" 11 | 12 | redis: 13 | image: redis:alpine 14 | ports: ["6379"] 15 | 16 | worker: 17 | build: ./worker 18 | 19 | db: 20 | image: postgres:9.4 21 | 22 | result: 23 | build: ./result 24 | command: nodemon --debug server.js 25 | volumes: 26 | - ./result:/app 27 | ports: 28 | - "5001:80" 29 | - "5858:5858" 30 | -------------------------------------------------------------------------------- /docker-compose-windows-1809.yml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | 3 | services: 4 | vote: 5 | image: dockersamples/examplevotingapp_vote:dotnet-nanoserver-1809 6 | build: 7 | context: ./vote/dotnet 8 | dockerfile: Dockerfile.1809 9 | ports: 10 | - "5000:80" 11 | depends_on: 12 | - message-queue 13 | 14 | result: 15 | image: dockersamples/examplevotingapp_result:dotnet-nanoserver-1809 16 | build: 17 | context: ./result/dotnet 18 | dockerfile: Dockerfile.1809 19 | ports: 20 | - "5001:80" 21 | environment: 22 | - "ConnectionStrings:ResultData=Server=db;Port=4000;Database=votes;User=root;SslMode=None" 23 | depends_on: 24 | - db 25 | 26 | worker: 27 | image: dockersamples/examplevotingapp_worker:dotnet-nanoserver-1809 28 | build: 29 | context: ./worker/dotnet 30 | dockerfile: Dockerfile.1809 31 | environment: 32 | - "ConnectionStrings:VoteData=Server=db;Port=4000;Database=votes;User=root;SslMode=None" 33 | depends_on: 34 | - message-queue 35 | - db 36 | 37 | message-queue: 38 | image: nats:2.0.4 39 | 40 | db: 41 | image: dockersamples/tidb:nanoserver-1809 42 | ports: 43 | - "3306:4000" 44 | 45 | networks: 46 | default: 47 | external: 48 | name: nat 49 | -------------------------------------------------------------------------------- /docker-compose-windows.yml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | 3 | services: 4 | vote: 5 | image: dockersamples/examplevotingapp_vote:dotnet-nanoserver-sac2016 6 | build: 7 | context: ./vote/dotnet 8 | ports: 9 | - "5000:80" 10 | depends_on: 11 | - message-queue 12 | 13 | result: 14 | image: dockersamples/examplevotingapp_result:dotnet-nanoserver-sac2016 15 | build: 16 | context: ./result/dotnet 17 | ports: 18 | - "5001:80" 19 | environment: 20 | - "ConnectionStrings:ResultData=Server=db;Port=4000;Database=votes;User=root;SslMode=None" 21 | depends_on: 22 | - db 23 | 24 | worker: 25 | image: dockersamples/examplevotingapp_worker:dotnet-nanoserver-sac2016 26 | build: 27 | context: ./worker/dotnet 28 | environment: 29 | - "ConnectionStrings:VoteData=Server=db;Port=4000;Database=votes;User=root;SslMode=None" 30 | depends_on: 31 | - message-queue 32 | - db 33 | 34 | message-queue: 35 | image: nats:nanoserver 36 | 37 | db: 38 | image: dockersamples/tidb:nanoserver 39 | ports: 40 | - "3306:4000" 41 | 42 | networks: 43 | default: 44 | external: 45 | name: nat -------------------------------------------------------------------------------- /docker-compose.seed.yml: -------------------------------------------------------------------------------- 1 | services: 2 | seed: 3 | build: ./seed-data 4 | networks: 5 | - front-tier 6 | restart: "no" 7 | 8 | networks: 9 | front-tier: -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | vote: 5 | build: ./vote 6 | command: python app.py 7 | volumes: 8 | - ./vote:/app 9 | ports: 10 | - "5000:80" 11 | 12 | redis: 13 | image: redis:alpine 14 | ports: ["6379"] 15 | 16 | worker: 17 | build: ./worker 18 | 19 | db: 20 | image: postgres:9.4 21 | environment: 22 | POSTGRES_USER: "postgres" 23 | POSTGRES_PASSWORD: "postgres" 24 | 25 | result: 26 | build: ./result 27 | command: nodemon server.js 28 | volumes: 29 | - ./result:/app 30 | ports: 31 | - "5001:80" 32 | - "5858:5858" 33 | -------------------------------------------------------------------------------- /docker-stack-simple.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | redis: 5 | image: redis:alpine 6 | ports: 7 | - "6379" 8 | networks: 9 | - frontend 10 | deploy: 11 | replicas: 1 12 | update_config: 13 | parallelism: 2 14 | delay: 10s 15 | restart_policy: 16 | condition: on-failure 17 | db: 18 | image: postgres:9.4 19 | environment: 20 | POSTGRES_USER: "postgres" 21 | POSTGRES_PASSWORD: "postgres" 22 | volumes: 23 | - db-data:/var/lib/postgresql/data 24 | networks: 25 | - backend 26 | deploy: 27 | placement: 28 | constraints: [node.role == manager] 29 | vote: 30 | image: dockersamples/examplevotingapp_vote:before 31 | ports: 32 | - 5000:80 33 | networks: 34 | - frontend 35 | depends_on: 36 | - redis 37 | deploy: 38 | replicas: 1 39 | update_config: 40 | parallelism: 2 41 | restart_policy: 42 | condition: on-failure 43 | result: 44 | image: dockersamples/examplevotingapp_result:before 45 | ports: 46 | - 5001:80 47 | networks: 48 | - backend 49 | depends_on: 50 | - db 51 | deploy: 52 | replicas: 1 53 | update_config: 54 | parallelism: 2 55 | delay: 10s 56 | restart_policy: 57 | condition: on-failure 58 | 59 | worker: 60 | image: dockersamples/examplevotingapp_worker 61 | networks: 62 | - frontend 63 | - backend 64 | depends_on: 65 | - db 66 | - redis 67 | deploy: 68 | mode: replicated 69 | replicas: 1 70 | labels: [APP=VOTING] 71 | restart_policy: 72 | condition: on-failure 73 | delay: 10s 74 | max_attempts: 3 75 | window: 120s 76 | placement: 77 | constraints: [node.role == manager] 78 | 79 | networks: 80 | frontend: 81 | backend: 82 | 83 | volumes: 84 | db-data: 85 | -------------------------------------------------------------------------------- /docker-stack-windows-1809.yml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | 3 | services: 4 | vote: 5 | image: dockersamples/examplevotingapp_vote:dotnet-nanoserver-1809 6 | ports: 7 | - "5000:80" 8 | deploy: 9 | mode: replicated 10 | replicas: 4 11 | networks: 12 | - frontend 13 | - backend 14 | 15 | result: 16 | image: dockersamples/examplevotingapp_result:dotnet-nanoserver-1809 17 | environment: 18 | - "ConnectionStrings:ResultData=Server=db;Port=4000;Database=votes;User=root;SslMode=None" 19 | ports: 20 | - "5001:80" 21 | networks: 22 | - frontend 23 | - backend 24 | 25 | worker: 26 | image: dockersamples/examplevotingapp_worker:dotnet-nanoserver-1809 27 | environment: 28 | - "ConnectionStrings:VoteData=Server=db;Port=4000;Database=votes;User=root;SslMode=None" 29 | deploy: 30 | mode: replicated 31 | replicas: 3 32 | networks: 33 | - backend 34 | 35 | message-queue: 36 | image: dockersamples/nats:nanoserver-1809 37 | networks: 38 | - backend 39 | 40 | db: 41 | image: dockersamples/tidb:nanoserver-1809 42 | ports: 43 | - "3306:4000" 44 | networks: 45 | - backend 46 | 47 | networks: 48 | frontend: 49 | backend: -------------------------------------------------------------------------------- /docker-stack-windows.yml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | 3 | services: 4 | vote: 5 | image: dockersamples/examplevotingapp_vote:dotnet-nanoserver-sac2016 6 | ports: 7 | - mode: host 8 | target: 80 9 | published: 5000 10 | deploy: 11 | endpoint_mode: dnsrr 12 | networks: 13 | - frontend 14 | - backend 15 | 16 | result: 17 | image: dockersamples/examplevotingapp_result:dotnet-nanoserver-sac2016 18 | environment: 19 | - "ConnectionStrings:ResultData=Server=db;Port=4000;Database=votes;User=root;SslMode=None" 20 | ports: 21 | - mode: host 22 | target: 80 23 | published: 5001 24 | deploy: 25 | endpoint_mode: dnsrr 26 | networks: 27 | - frontend 28 | - backend 29 | 30 | worker: 31 | image: dockersamples/examplevotingapp_worker:dotnet-nanoserver-sac2016 32 | environment: 33 | - "ConnectionStrings:VoteData=Server=db;Port=4000;Database=votes;User=root;SslMode=None" 34 | deploy: 35 | endpoint_mode: dnsrr 36 | mode: replicated 37 | replicas: 3 38 | networks: 39 | - backend 40 | 41 | message-queue: 42 | image: nats:nanoserver 43 | deploy: 44 | endpoint_mode: dnsrr 45 | networks: 46 | - backend 47 | 48 | db: 49 | image: dockersamples/tidb:nanoserver 50 | ports: 51 | - mode: host 52 | target: 4000 53 | published: 3306 54 | deploy: 55 | endpoint_mode: dnsrr 56 | networks: 57 | - backend 58 | 59 | networks: 60 | frontend: 61 | backend: -------------------------------------------------------------------------------- /docker-stack.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | redis: 5 | image: redis:alpine 6 | networks: 7 | - frontend 8 | deploy: 9 | replicas: 1 10 | update_config: 11 | parallelism: 2 12 | delay: 10s 13 | restart_policy: 14 | condition: on-failure 15 | db: 16 | image: postgres:9.4 17 | environment: 18 | POSTGRES_USER: "postgres" 19 | POSTGRES_PASSWORD: "postgres" 20 | volumes: 21 | - db-data:/var/lib/postgresql/data 22 | networks: 23 | - backend 24 | deploy: 25 | placement: 26 | constraints: [node.role == manager] 27 | vote: 28 | image: dockersamples/examplevotingapp_vote:before 29 | ports: 30 | - 5000:80 31 | networks: 32 | - frontend 33 | depends_on: 34 | - redis 35 | deploy: 36 | replicas: 2 37 | update_config: 38 | parallelism: 2 39 | restart_policy: 40 | condition: on-failure 41 | result: 42 | image: dockersamples/examplevotingapp_result:before 43 | ports: 44 | - 5001:80 45 | networks: 46 | - backend 47 | depends_on: 48 | - db 49 | deploy: 50 | replicas: 1 51 | update_config: 52 | parallelism: 2 53 | delay: 10s 54 | restart_policy: 55 | condition: on-failure 56 | 57 | worker: 58 | image: dockersamples/examplevotingapp_worker 59 | networks: 60 | - frontend 61 | - backend 62 | depends_on: 63 | - db 64 | - redis 65 | deploy: 66 | mode: replicated 67 | replicas: 1 68 | labels: [APP=VOTING] 69 | restart_policy: 70 | condition: on-failure 71 | delay: 10s 72 | max_attempts: 3 73 | window: 120s 74 | placement: 75 | constraints: [node.role == manager] 76 | 77 | visualizer: 78 | image: dockersamples/visualizer:stable 79 | ports: 80 | - "8080:8080" 81 | stop_grace_period: 1m30s 82 | volumes: 83 | - "/var/run/docker.sock:/var/run/docker.sock" 84 | deploy: 85 | placement: 86 | constraints: [node.role == manager] 87 | 88 | networks: 89 | frontend: 90 | backend: 91 | 92 | volumes: 93 | db-data: 94 | -------------------------------------------------------------------------------- /dockercloud.yml: -------------------------------------------------------------------------------- 1 | db: 2 | image: 'postgres:9.4' 3 | restart: always 4 | redis: 5 | image: 'redis:latest' 6 | restart: always 7 | result: 8 | autoredeploy: true 9 | image: 'docker/example-voting-app-result:latest' 10 | ports: 11 | - '80:80' 12 | restart: always 13 | lb: 14 | autoredeploy: true 15 | image: 'dockercloud/haproxy:latest' 16 | links: 17 | - vote 18 | ports: 19 | - "80:80" 20 | roles: 21 | - global 22 | restart: always 23 | vote: 24 | autoredeploy: true 25 | image: 'docker/example-voting-app-vote:latest' 26 | restart: always 27 | target_num_containers: 5 28 | 29 | worker: 30 | autoredeploy: true 31 | image: 'docker/example-voting-app-worker:latest' 32 | restart: always 33 | target_num_containers: 3 34 | -------------------------------------------------------------------------------- /healthchecks/postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | host="$(hostname -i || echo '127.0.0.1')" 5 | user="${POSTGRES_USER:-postgres}" 6 | db="${POSTGRES_DB:-$POSTGRES_USER}" 7 | export PGPASSWORD="${POSTGRES_PASSWORD:-}" 8 | 9 | args=( 10 | # force postgres to not use the local unix socket (test "external" connectibility) 11 | --host "$host" 12 | --username "$user" 13 | --dbname "$db" 14 | --quiet --no-align --tuples-only 15 | ) 16 | 17 | if select="$(echo 'SELECT 1' | psql "${args[@]}")" && [ "$select" = '1' ]; then 18 | exit 0 19 | fi 20 | 21 | exit 1 22 | -------------------------------------------------------------------------------- /healthchecks/redis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eo pipefail 3 | 4 | host="$(hostname -i || echo '127.0.0.1')" 5 | 6 | if ping="$(redis-cli -h "$host" ping)" && [ "$ping" = 'PONG' ]; then 7 | exit 0 8 | fi 9 | 10 | exit 1 11 | -------------------------------------------------------------------------------- /k8s-specifications/db-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: db 6 | name: db 7 | namespace: vote 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: db 13 | template: 14 | metadata: 15 | labels: 16 | app: db 17 | spec: 18 | containers: 19 | - image: postgres:9.4 20 | name: postgres 21 | env: 22 | - name: POSTGRES_USER 23 | value: postgres 24 | - name: POSTGRES_PASSWORD 25 | value: postgres 26 | ports: 27 | - containerPort: 5432 28 | name: postgres 29 | volumeMounts: 30 | - mountPath: /var/lib/postgresql/data 31 | name: db-data 32 | volumes: 33 | - name: db-data 34 | emptyDir: {} 35 | -------------------------------------------------------------------------------- /k8s-specifications/db-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: db 6 | name: db 7 | namespace: vote 8 | spec: 9 | type: ClusterIP 10 | ports: 11 | - name: "db-service" 12 | port: 5432 13 | targetPort: 5432 14 | selector: 15 | app: db 16 | 17 | -------------------------------------------------------------------------------- /k8s-specifications/redis-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: redis 6 | name: redis 7 | namespace: vote 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: redis 13 | template: 14 | metadata: 15 | labels: 16 | app: redis 17 | spec: 18 | containers: 19 | - image: redis:alpine 20 | name: redis 21 | ports: 22 | - containerPort: 6379 23 | name: redis 24 | volumeMounts: 25 | - mountPath: /data 26 | name: redis-data 27 | volumes: 28 | - name: redis-data 29 | emptyDir: {} 30 | -------------------------------------------------------------------------------- /k8s-specifications/redis-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: redis 6 | name: redis 7 | namespace: vote 8 | spec: 9 | type: ClusterIP 10 | ports: 11 | - name: "redis-service" 12 | port: 6379 13 | targetPort: 6379 14 | selector: 15 | app: redis 16 | 17 | -------------------------------------------------------------------------------- /k8s-specifications/result-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: result 6 | name: result 7 | namespace: vote 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: result 13 | template: 14 | metadata: 15 | labels: 16 | app: result 17 | spec: 18 | containers: 19 | - image: dockersamples/examplevotingapp_result:before 20 | name: result 21 | ports: 22 | - containerPort: 80 23 | name: result 24 | -------------------------------------------------------------------------------- /k8s-specifications/result-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: result 6 | name: result 7 | namespace: vote 8 | spec: 9 | type: NodePort 10 | ports: 11 | - name: "result-service" 12 | port: 5001 13 | targetPort: 80 14 | nodePort: 31001 15 | selector: 16 | app: result 17 | -------------------------------------------------------------------------------- /k8s-specifications/vote-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: vote 6 | name: vote 7 | namespace: vote 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: vote 13 | template: 14 | metadata: 15 | labels: 16 | app: vote 17 | spec: 18 | containers: 19 | - image: dockersamples/examplevotingapp_vote:before 20 | name: vote 21 | ports: 22 | - containerPort: 80 23 | name: vote 24 | -------------------------------------------------------------------------------- /k8s-specifications/vote-namespace.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: vote 5 | 6 | -------------------------------------------------------------------------------- /k8s-specifications/vote-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: vote 6 | name: vote 7 | namespace: vote 8 | spec: 9 | type: NodePort 10 | ports: 11 | - name: "vote-service" 12 | port: 5000 13 | targetPort: 80 14 | nodePort: 31000 15 | selector: 16 | app: vote 17 | 18 | -------------------------------------------------------------------------------- /k8s-specifications/worker-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: worker 6 | name: worker 7 | namespace: vote 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: worker 13 | template: 14 | metadata: 15 | labels: 16 | app: worker 17 | spec: 18 | containers: 19 | - image: dockersamples/examplevotingapp_worker 20 | name: worker 21 | -------------------------------------------------------------------------------- /kube-deployment.yml: -------------------------------------------------------------------------------- 1 | # redis 2 | --- 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | labels: 7 | app: redis 8 | name: redis 9 | spec: 10 | clusterIP: None 11 | ports: 12 | - name: redis-service 13 | port: 6379 14 | targetPort: 6379 15 | selector: 16 | app: redis 17 | --- 18 | apiVersion: apps/v1 19 | kind: Deployment 20 | metadata: 21 | name: redis 22 | labels: 23 | app: redis 24 | spec: 25 | replicas: 1 26 | selector: 27 | matchLabels: 28 | app: redis 29 | template: 30 | metadata: 31 | labels: 32 | app: redis 33 | spec: 34 | containers: 35 | - name: redis 36 | image: redis:alpine 37 | ports: 38 | - containerPort: 6379 39 | name: redis 40 | 41 | # db 42 | --- 43 | apiVersion: v1 44 | kind: Service 45 | metadata: 46 | labels: 47 | app: db 48 | name: db 49 | spec: 50 | clusterIP: None 51 | ports: 52 | - name: db 53 | port: 5432 54 | targetPort: 5432 55 | selector: 56 | app: db 57 | --- 58 | apiVersion: apps/v1 59 | kind: Deployment 60 | metadata: 61 | name: db 62 | labels: 63 | app: db 64 | spec: 65 | replicas: 1 66 | selector: 67 | matchLabels: 68 | app: db 69 | template: 70 | metadata: 71 | labels: 72 | app: db 73 | spec: 74 | containers: 75 | - name: db 76 | image: postgres:9.4 77 | env: 78 | - name: PGDATA 79 | value: /var/lib/postgresql/data/pgdata 80 | - name: POSTGRES_USER 81 | value: postgres 82 | - name: POSTGRES_PASSWORD 83 | value: postgres 84 | ports: 85 | - containerPort: 5432 86 | name: db 87 | volumeMounts: 88 | - name: db-data 89 | mountPath: /var/lib/postgresql/data 90 | volumes: 91 | - name: db-data 92 | persistentVolumeClaim: 93 | claimName: postgres-pv-claim 94 | --- 95 | apiVersion: v1 96 | kind: PersistentVolumeClaim 97 | metadata: 98 | name: postgres-pv-claim 99 | spec: 100 | accessModes: 101 | - ReadWriteOnce 102 | resources: 103 | requests: 104 | storage: 1Gi 105 | 106 | # result 107 | --- 108 | apiVersion: v1 109 | kind: Service 110 | metadata: 111 | name: result 112 | labels: 113 | app: result 114 | spec: 115 | type: LoadBalancer 116 | ports: 117 | - port: 5001 118 | targetPort: 80 119 | name: result-service 120 | selector: 121 | app: result 122 | --- 123 | apiVersion: apps/v1 124 | kind: Deployment 125 | metadata: 126 | name: result 127 | labels: 128 | app: result 129 | spec: 130 | replicas: 1 131 | selector: 132 | matchLabels: 133 | app: result 134 | template: 135 | metadata: 136 | labels: 137 | app: result 138 | spec: 139 | containers: 140 | - name: result 141 | image: dockersamples/examplevotingapp_result:before 142 | ports: 143 | - containerPort: 80 144 | name: result 145 | 146 | # vote 147 | --- 148 | apiVersion: v1 149 | kind: Service 150 | metadata: 151 | name: vote 152 | labels: 153 | apps: vote 154 | spec: 155 | type: LoadBalancer 156 | ports: 157 | - port: 5000 158 | targetPort: 80 159 | name: vote-service 160 | selector: 161 | app: vote 162 | --- 163 | apiVersion: apps/v1 164 | kind: Deployment 165 | metadata: 166 | name: vote 167 | labels: 168 | app: vote 169 | spec: 170 | replicas: 2 171 | selector: 172 | matchLabels: 173 | app: vote 174 | template: 175 | metadata: 176 | labels: 177 | app: vote 178 | spec: 179 | containers: 180 | - name: vote 181 | image: dockersamples/examplevotingapp_vote:before 182 | ports: 183 | - containerPort: 80 184 | name: vote 185 | 186 | # worker 187 | --- 188 | apiVersion: v1 189 | kind: Service 190 | metadata: 191 | labels: 192 | apps: worker 193 | name: worker 194 | spec: 195 | clusterIP: None 196 | selector: 197 | app: worker 198 | --- 199 | apiVersion: apps/v1 200 | kind: Deployment 201 | metadata: 202 | labels: 203 | app: worker 204 | name: worker 205 | spec: 206 | replicas: 1 207 | selector: 208 | matchLabels: 209 | app: worker 210 | template: 211 | metadata: 212 | labels: 213 | app: worker 214 | spec: 215 | containers: 216 | - image: dockersamples/examplevotingapp_worker 217 | name: worker 218 | -------------------------------------------------------------------------------- /result/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /result/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach", 6 | "type": "node", 7 | "request": "attach", 8 | "port": 5858, 9 | "address": "localhost", 10 | "restart": true, 11 | "sourceMaps": false, 12 | "outDir": null, 13 | "localRoot": "${workspaceRoot}", 14 | "remoteRoot": "/app", 15 | "timeout": 10000 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /result/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-slim 2 | 3 | # add curl for healthcheck 4 | RUN apt-get update \ 5 | && apt-get install -y --no-install-recommends \ 6 | curl \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | # Add Tini for proper init of signals 10 | ENV TINI_VERSION v0.19.0 11 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 12 | RUN chmod +x /tini 13 | 14 | WORKDIR /app 15 | 16 | # have nodemon available for local dev use (file watching) 17 | RUN npm install -g nodemon 18 | 19 | COPY package*.json ./ 20 | 21 | RUN npm ci \ 22 | && npm cache clean --force \ 23 | && mv /app/node_modules /node_modules 24 | 25 | COPY . . 26 | 27 | ENV PORT 80 28 | 29 | EXPOSE 80 30 | 31 | CMD ["/tini", "--", "node", "server.js"] 32 | -------------------------------------------------------------------------------- /result/docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | 5 | sut: 6 | build: ./tests/ 7 | depends_on: 8 | - vote 9 | - result 10 | - worker 11 | networks: 12 | - front-tier 13 | 14 | vote: 15 | build: ../vote/ 16 | ports: ["80"] 17 | depends_on: 18 | - redis 19 | - db 20 | networks: 21 | - front-tier 22 | - back-tier 23 | 24 | result: 25 | build: . 26 | ports: ["80"] 27 | depends_on: 28 | - redis 29 | - db 30 | networks: 31 | - front-tier 32 | - back-tier 33 | 34 | worker: 35 | build: ../worker/ 36 | depends_on: 37 | - redis 38 | - db 39 | networks: 40 | - back-tier 41 | 42 | redis: 43 | image: redis:alpine 44 | ports: ["6379"] 45 | networks: 46 | - back-tier 47 | 48 | db: 49 | image: postgres:9.4 50 | environment: 51 | POSTGRES_USER: "postgres" 52 | POSTGRES_PASSWORD: "postgres" 53 | volumes: 54 | - "db-data:/var/lib/postgresql/data" 55 | networks: 56 | - back-tier 57 | 58 | volumes: 59 | db-data: 60 | 61 | networks: 62 | front-tier: 63 | back-tier: 64 | -------------------------------------------------------------------------------- /result/dotnet/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.1-sdk-nanoserver-sac2016 as builder 2 | 3 | WORKDIR /Result 4 | COPY Result/Result.csproj . 5 | RUN dotnet restore 6 | 7 | COPY /Result . 8 | RUN dotnet publish -c Release -o /out Result.csproj 9 | 10 | # app image 11 | FROM microsoft/dotnet:2.1-aspnetcore-runtime-nanoserver-sac2016 12 | 13 | WORKDIR /app 14 | ENTRYPOINT ["dotnet", "Result.dll"] 15 | 16 | COPY --from=builder /out . -------------------------------------------------------------------------------- /result/dotnet/Dockerfile.1809: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 as builder 2 | 3 | WORKDIR /Result 4 | COPY Result/Result.csproj . 5 | RUN dotnet restore 6 | 7 | COPY /Result . 8 | RUN dotnet publish -c Release -o /out Result.csproj 9 | 10 | # app image 11 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 12 | 13 | WORKDIR /app 14 | ENTRYPOINT ["dotnet", "Result.dll"] 15 | 16 | COPY --from=builder /out . -------------------------------------------------------------------------------- /result/dotnet/Result/Data/IResultData.cs: -------------------------------------------------------------------------------- 1 | using Result.Models; 2 | 3 | namespace Result.Data 4 | { 5 | public interface IResultData 6 | { 7 | ResultsModel GetResults(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /result/dotnet/Result/Data/MySqlResultData.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Dapper; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.Logging; 5 | using MySql.Data.MySqlClient; 6 | using Result.Models; 7 | 8 | namespace Result.Data 9 | { 10 | public class MySqlResultData : IResultData 11 | { 12 | private readonly string _connectionString; 13 | private readonly ILogger _logger; 14 | 15 | public MySqlResultData(IConfiguration config, ILogger logger) 16 | { 17 | _connectionString = config.GetConnectionString("ResultData"); 18 | _logger = logger; 19 | } 20 | 21 | public ResultsModel GetResults() 22 | { 23 | var model = new ResultsModel(); 24 | using (var connection = new MySqlConnection(_connectionString)) 25 | { 26 | var results = connection.Query("SELECT vote, COUNT(id) AS count FROM votes GROUP BY vote ORDER BY vote"); 27 | if (results.Any(x => x.vote == "a")) 28 | { 29 | model.OptionA = (int) results.First(x => x.vote == "a").count; 30 | } 31 | if (results.Any(x => x.vote == "b")) 32 | { 33 | model.OptionB = (int) results.First(x => x.vote == "b").count; 34 | } 35 | model.VoteCount = model.OptionA + model.OptionB; 36 | } 37 | return model; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /result/dotnet/Result/Hubs/ResultsHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | 3 | namespace Result.Hubs 4 | { 5 | public class ResultsHub : Hub 6 | { 7 | //no public methods, only used for push from PublishRTesultsTimer 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /result/dotnet/Result/Models/ResultsModel.cs: -------------------------------------------------------------------------------- 1 | namespace Result.Models 2 | { 3 | public class ResultsModel 4 | { 5 | public int OptionA { get; set; } 6 | 7 | public int OptionB { get; set; } 8 | 9 | public int VoteCount { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /result/dotnet/Result/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Result.Pages.IndexModel 3 | 4 | 5 | 6 | 7 | 8 | @Model.OptionA vs @Model.OptionB -- Result 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 | 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
@Model.OptionA
29 |
50%
30 |
31 |
32 |
33 |
@Model.OptionB
34 |
50%
35 |
36 |
37 |
38 |
39 |
40 | No votes yet 41 |
42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /result/dotnet/Result/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | using Microsoft.Extensions.Configuration; 8 | 9 | namespace Result.Pages 10 | { 11 | public class IndexModel : PageModel 12 | { 13 | private string _optionA; 14 | private string _optionB; 15 | protected readonly IConfiguration _configuration; 16 | 17 | public string OptionA { get; private set; } 18 | public string OptionB { get; private set; } 19 | 20 | public IndexModel(IConfiguration configuration) 21 | { 22 | _configuration = configuration; 23 | _optionA = _configuration.GetValue("Voting:OptionA"); 24 | _optionB = _configuration.GetValue("Voting:OptionB"); 25 | } 26 | 27 | public void OnGet() 28 | { 29 | OptionA = _optionA; 30 | OptionB = _optionB; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /result/dotnet/Result/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Result 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | CreateWebHostBuilder(args).Build().Run(); 18 | } 19 | 20 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /result/dotnet/Result/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:56785", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Result": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /result/dotnet/Result/Result.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /result/dotnet/Result/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Result.Data; 7 | using Result.Hubs; 8 | using Result.Timers; 9 | 10 | namespace Result 11 | { 12 | public class Startup 13 | { 14 | public Startup(IConfiguration configuration) 15 | { 16 | Configuration = configuration; 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 24 | services.AddSignalR(); 25 | 26 | services.AddTransient() 27 | .AddSingleton(); 28 | } 29 | 30 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 31 | { 32 | if (env.IsDevelopment()) 33 | { 34 | app.UseDeveloperExceptionPage(); 35 | } 36 | else 37 | { 38 | app.UseExceptionHandler("/Error"); 39 | } 40 | 41 | app.UseStaticFiles(); 42 | app.UseSignalR(routes => 43 | { 44 | routes.MapHub("/resultsHub"); 45 | }); 46 | app.UseMvc(); 47 | 48 | var timer = app.ApplicationServices.GetService(); 49 | timer.Start(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /result/dotnet/Result/Timers/PublishResultsTimer.cs: -------------------------------------------------------------------------------- 1 | using System.Timers; 2 | using Microsoft.AspNetCore.SignalR; 3 | using Microsoft.Extensions.Configuration; 4 | using Result.Data; 5 | using Result.Hubs; 6 | 7 | namespace Result.Timers 8 | { 9 | public class PublishResultsTimer 10 | { 11 | private readonly IHubContext _hubContext; 12 | private readonly IResultData _resultData; 13 | private readonly Timer _timer; 14 | 15 | public PublishResultsTimer(IHubContext hubContext, IResultData resultData, IConfiguration configuration) 16 | { 17 | _hubContext = hubContext; 18 | _resultData = resultData; 19 | var publishMilliseconds = configuration.GetValue("ResultsTimer:PublishMilliseconds"); 20 | _timer = new Timer(publishMilliseconds) 21 | { 22 | Enabled = false 23 | }; 24 | _timer.Elapsed += PublishResults; 25 | } 26 | 27 | public void Start() 28 | { 29 | if (!_timer.Enabled) 30 | { 31 | _timer.Start(); 32 | } 33 | } 34 | 35 | private void PublishResults(object sender, ElapsedEventArgs e) 36 | { 37 | var model = _resultData.GetResults(); 38 | _hubContext.Clients.All.SendAsync("UpdateResults", model); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /result/dotnet/Result/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /result/dotnet/Result/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Voting": { 3 | "OptionA": "Cats", 4 | "OptionB": "Dogs" 5 | }, 6 | "ResultsTimer": { 7 | "PublishMilliseconds": 2500 8 | }, 9 | "ConnectionStrings": { 10 | "ResultData": "Server=mysql;Port=4000;Database=votes;User=root;SslMode=None" 11 | }, 12 | "Logging": { 13 | "LogLevel": { 14 | "Default": "Warning" 15 | } 16 | }, 17 | "AllowedHosts": "*" 18 | } 19 | -------------------------------------------------------------------------------- /result/dotnet/Result/libman.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "defaultProvider": "unpkg", 4 | "libraries": [ 5 | { 6 | "library": "@aspnet/signalr@1.0.3", 7 | "destination": "wwwroot/lib/signalr/", 8 | "files": [ 9 | "dist/browser/signalr.js", 10 | "dist/browser/signalr.min.js" 11 | ] 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /result/dotnet/Result/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, body { 8 | margin: 0; 9 | padding: 0; 10 | height: 100%; 11 | font-family: 'Open Sans'; 12 | } 13 | 14 | body { 15 | opacity: 0; 16 | transition: all 1s linear; 17 | } 18 | 19 | .divider { 20 | height: 150px; 21 | width: 2px; 22 | background-color: #C0C9CE; 23 | position: relative; 24 | top: 50%; 25 | float: left; 26 | transform: translateY(-50%); 27 | } 28 | 29 | #background-stats-1 { 30 | background-color: #2196f3; 31 | } 32 | 33 | #background-stats-2 { 34 | background-color: #00cbca; 35 | } 36 | 37 | #content-container { 38 | z-index: 2; 39 | position: relative; 40 | margin: 0 auto; 41 | display: table; 42 | padding: 10px; 43 | max-width: 940px; 44 | height: 100%; 45 | } 46 | 47 | #content-container-center { 48 | display: table-cell; 49 | text-align: center; 50 | vertical-align: middle; 51 | } 52 | 53 | #result { 54 | z-index: 3; 55 | position: absolute; 56 | bottom: 40px; 57 | right: 20px; 58 | color: #fff; 59 | opacity: 0.5; 60 | font-size: 45px; 61 | font-weight: 600; 62 | } 63 | 64 | #choice { 65 | transition: all 300ms linear; 66 | line-height: 1.3em; 67 | background: #fff; 68 | box-shadow: 10px 0 0 #fff, -10px 0 0 #fff; 69 | vertical-align: middle; 70 | font-size: 40px; 71 | font-weight: 600; 72 | width: 450px; 73 | height: 200px; 74 | } 75 | 76 | #choice a { 77 | text-decoration: none; 78 | } 79 | 80 | #choice a:hover, #choice a:focus { 81 | outline: 0; 82 | text-decoration: underline; 83 | } 84 | 85 | #choice .choice { 86 | width: 49%; 87 | position: relative; 88 | top: 50%; 89 | transform: translateY(-50%); 90 | text-align: left; 91 | padding-left: 50px; 92 | } 93 | 94 | #choice .choice .label { 95 | text-transform: uppercase; 96 | } 97 | 98 | #choice .choice.resultb { 99 | color: #00cbca; 100 | float: right; 101 | } 102 | 103 | #choice .choice.resulta { 104 | color: #2196f3; 105 | float: left; 106 | } 107 | 108 | #background-stats { 109 | z-index: 1; 110 | height: 100%; 111 | width: 100%; 112 | position: absolute; 113 | } 114 | 115 | #background-stats div { 116 | transition: width 400ms ease-in-out; 117 | display: inline-block; 118 | margin-bottom: -4px; 119 | width: 50%; 120 | height: 100%; 121 | } 122 | -------------------------------------------------------------------------------- /result/dotnet/Result/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}} -------------------------------------------------------------------------------- /result/dotnet/Result/wwwroot/js/results.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var connection = new signalR.HubConnectionBuilder().withUrl("/resultsHub").build(); 4 | 5 | connection.on("UpdateResults", function (results) { 6 | document.body.style.opacity=1; 7 | 8 | var a = parseInt(results.optionA || 0); 9 | var b = parseInt(results.optionB || 0); 10 | var percentages = getPercentages(a, b); 11 | 12 | document.getElementById("optionA").innerText = percentages.a + "%"; 13 | document.getElementById("optionB").innerText = percentages.b + "%"; 14 | var totalVotes = 'No votes yet'; 15 | if (results.voteCount > 0) { 16 | totalVotes = results.voteCount + (results.voteCount > 1 ? " votes" : " vote"); 17 | } 18 | document.getElementById("totalVotes").innerText = totalVotes; 19 | 20 | var bg1 = document.getElementById('background-stats-1'); 21 | var bg2 = document.getElementById('background-stats-2'); 22 | bg1.style.width = (percentages.a-0.2) + "%"; 23 | bg2.style.width = (percentages.b-0.2) + "%"; 24 | }); 25 | 26 | connection.start().catch(function (err) { 27 | return console.error(err.toString()); 28 | }); 29 | 30 | function getPercentages(a, b) { 31 | var result = {}; 32 | 33 | if (a + b > 0) { 34 | result.a = Math.round(a / (a + b) * 100); 35 | result.b = 100 - result.a; 36 | } else { 37 | result.a = result.b = 50; 38 | } 39 | 40 | return result; 41 | } -------------------------------------------------------------------------------- /result/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "result", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.7", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 10 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 11 | "requires": { 12 | "mime-types": "~2.1.24", 13 | "negotiator": "0.6.2" 14 | } 15 | }, 16 | "after": { 17 | "version": "0.8.2", 18 | "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", 19 | "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" 20 | }, 21 | "array-flatten": { 22 | "version": "1.1.1", 23 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 24 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 25 | }, 26 | "arraybuffer.slice": { 27 | "version": "0.0.7", 28 | "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", 29 | "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" 30 | }, 31 | "async": { 32 | "version": "3.1.0", 33 | "resolved": "https://registry.npmjs.org/async/-/async-3.1.0.tgz", 34 | "integrity": "sha512-4vx/aaY6j/j3Lw3fbCHNWP0pPaTCew3F6F3hYyl/tHs/ndmV1q7NW9T5yuJ2XAGwdQrP+6Wu20x06U4APo/iQQ==" 35 | }, 36 | "backo2": { 37 | "version": "1.0.2", 38 | "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", 39 | "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" 40 | }, 41 | "base64-arraybuffer": { 42 | "version": "0.1.4", 43 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", 44 | "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" 45 | }, 46 | "base64id": { 47 | "version": "2.0.0", 48 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", 49 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" 50 | }, 51 | "blob": { 52 | "version": "0.0.5", 53 | "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", 54 | "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" 55 | }, 56 | "body-parser": { 57 | "version": "1.19.0", 58 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 59 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 60 | "requires": { 61 | "bytes": "3.1.0", 62 | "content-type": "~1.0.4", 63 | "debug": "2.6.9", 64 | "depd": "~1.1.2", 65 | "http-errors": "1.7.2", 66 | "iconv-lite": "0.4.24", 67 | "on-finished": "~2.3.0", 68 | "qs": "6.7.0", 69 | "raw-body": "2.4.0", 70 | "type-is": "~1.6.17" 71 | } 72 | }, 73 | "buffer-writer": { 74 | "version": "2.0.0", 75 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", 76 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" 77 | }, 78 | "bytes": { 79 | "version": "3.1.0", 80 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 81 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 82 | }, 83 | "component-bind": { 84 | "version": "1.0.0", 85 | "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", 86 | "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" 87 | }, 88 | "component-emitter": { 89 | "version": "1.3.0", 90 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", 91 | "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" 92 | }, 93 | "component-inherit": { 94 | "version": "0.0.3", 95 | "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", 96 | "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" 97 | }, 98 | "content-disposition": { 99 | "version": "0.5.3", 100 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 101 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 102 | "requires": { 103 | "safe-buffer": "5.1.2" 104 | } 105 | }, 106 | "content-type": { 107 | "version": "1.0.4", 108 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 109 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 110 | }, 111 | "cookie": { 112 | "version": "0.3.1", 113 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 114 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 115 | }, 116 | "cookie-parser": { 117 | "version": "1.4.4", 118 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", 119 | "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", 120 | "requires": { 121 | "cookie": "0.3.1", 122 | "cookie-signature": "1.0.6" 123 | } 124 | }, 125 | "cookie-signature": { 126 | "version": "1.0.6", 127 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 128 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 129 | }, 130 | "debug": { 131 | "version": "2.6.9", 132 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 133 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 134 | "requires": { 135 | "ms": "2.0.0" 136 | } 137 | }, 138 | "depd": { 139 | "version": "1.1.2", 140 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 141 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 142 | }, 143 | "destroy": { 144 | "version": "1.0.4", 145 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 146 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 147 | }, 148 | "ee-first": { 149 | "version": "1.1.1", 150 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 151 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 152 | }, 153 | "encodeurl": { 154 | "version": "1.0.2", 155 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 156 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 157 | }, 158 | "engine.io": { 159 | "version": "3.5.0", 160 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", 161 | "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==", 162 | "requires": { 163 | "accepts": "~1.3.4", 164 | "base64id": "2.0.0", 165 | "cookie": "~0.4.1", 166 | "debug": "~4.1.0", 167 | "engine.io-parser": "~2.2.0", 168 | "ws": "~7.4.2" 169 | }, 170 | "dependencies": { 171 | "cookie": { 172 | "version": "0.4.1", 173 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", 174 | "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" 175 | }, 176 | "debug": { 177 | "version": "4.1.1", 178 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 179 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 180 | "requires": { 181 | "ms": "^2.1.1" 182 | } 183 | }, 184 | "ms": { 185 | "version": "2.1.3", 186 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 187 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 188 | } 189 | } 190 | }, 191 | "engine.io-client": { 192 | "version": "3.5.0", 193 | "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.0.tgz", 194 | "integrity": "sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==", 195 | "requires": { 196 | "component-emitter": "~1.3.0", 197 | "component-inherit": "0.0.3", 198 | "debug": "~3.1.0", 199 | "engine.io-parser": "~2.2.0", 200 | "has-cors": "1.1.0", 201 | "indexof": "0.0.1", 202 | "parseqs": "0.0.6", 203 | "parseuri": "0.0.6", 204 | "ws": "~7.4.2", 205 | "xmlhttprequest-ssl": "~1.5.4", 206 | "yeast": "0.1.2" 207 | }, 208 | "dependencies": { 209 | "debug": { 210 | "version": "3.1.0", 211 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 212 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 213 | "requires": { 214 | "ms": "2.0.0" 215 | } 216 | } 217 | } 218 | }, 219 | "engine.io-parser": { 220 | "version": "2.2.1", 221 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", 222 | "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", 223 | "requires": { 224 | "after": "0.8.2", 225 | "arraybuffer.slice": "~0.0.7", 226 | "base64-arraybuffer": "0.1.4", 227 | "blob": "0.0.5", 228 | "has-binary2": "~1.0.2" 229 | } 230 | }, 231 | "escape-html": { 232 | "version": "1.0.3", 233 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 234 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 235 | }, 236 | "etag": { 237 | "version": "1.8.1", 238 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 239 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 240 | }, 241 | "express": { 242 | "version": "4.17.1", 243 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 244 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 245 | "requires": { 246 | "accepts": "~1.3.7", 247 | "array-flatten": "1.1.1", 248 | "body-parser": "1.19.0", 249 | "content-disposition": "0.5.3", 250 | "content-type": "~1.0.4", 251 | "cookie": "0.4.0", 252 | "cookie-signature": "1.0.6", 253 | "debug": "2.6.9", 254 | "depd": "~1.1.2", 255 | "encodeurl": "~1.0.2", 256 | "escape-html": "~1.0.3", 257 | "etag": "~1.8.1", 258 | "finalhandler": "~1.1.2", 259 | "fresh": "0.5.2", 260 | "merge-descriptors": "1.0.1", 261 | "methods": "~1.1.2", 262 | "on-finished": "~2.3.0", 263 | "parseurl": "~1.3.3", 264 | "path-to-regexp": "0.1.7", 265 | "proxy-addr": "~2.0.5", 266 | "qs": "6.7.0", 267 | "range-parser": "~1.2.1", 268 | "safe-buffer": "5.1.2", 269 | "send": "0.17.1", 270 | "serve-static": "1.14.1", 271 | "setprototypeof": "1.1.1", 272 | "statuses": "~1.5.0", 273 | "type-is": "~1.6.18", 274 | "utils-merge": "1.0.1", 275 | "vary": "~1.1.2" 276 | }, 277 | "dependencies": { 278 | "cookie": { 279 | "version": "0.4.0", 280 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 281 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 282 | } 283 | } 284 | }, 285 | "finalhandler": { 286 | "version": "1.1.2", 287 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 288 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 289 | "requires": { 290 | "debug": "2.6.9", 291 | "encodeurl": "~1.0.2", 292 | "escape-html": "~1.0.3", 293 | "on-finished": "~2.3.0", 294 | "parseurl": "~1.3.3", 295 | "statuses": "~1.5.0", 296 | "unpipe": "~1.0.0" 297 | } 298 | }, 299 | "forwarded": { 300 | "version": "0.1.2", 301 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 302 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 303 | }, 304 | "fresh": { 305 | "version": "0.5.2", 306 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 307 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 308 | }, 309 | "has-binary2": { 310 | "version": "1.0.3", 311 | "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", 312 | "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", 313 | "requires": { 314 | "isarray": "2.0.1" 315 | } 316 | }, 317 | "has-cors": { 318 | "version": "1.1.0", 319 | "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", 320 | "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" 321 | }, 322 | "http-errors": { 323 | "version": "1.7.2", 324 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 325 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 326 | "requires": { 327 | "depd": "~1.1.2", 328 | "inherits": "2.0.3", 329 | "setprototypeof": "1.1.1", 330 | "statuses": ">= 1.5.0 < 2", 331 | "toidentifier": "1.0.0" 332 | } 333 | }, 334 | "iconv-lite": { 335 | "version": "0.4.24", 336 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 337 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 338 | "requires": { 339 | "safer-buffer": ">= 2.1.2 < 3" 340 | } 341 | }, 342 | "indexof": { 343 | "version": "0.0.1", 344 | "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", 345 | "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" 346 | }, 347 | "inherits": { 348 | "version": "2.0.3", 349 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 350 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 351 | }, 352 | "ipaddr.js": { 353 | "version": "1.9.0", 354 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", 355 | "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" 356 | }, 357 | "isarray": { 358 | "version": "2.0.1", 359 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", 360 | "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" 361 | }, 362 | "media-typer": { 363 | "version": "0.3.0", 364 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 365 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 366 | }, 367 | "merge-descriptors": { 368 | "version": "1.0.1", 369 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 370 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 371 | }, 372 | "method-override": { 373 | "version": "3.0.0", 374 | "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", 375 | "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", 376 | "requires": { 377 | "debug": "3.1.0", 378 | "methods": "~1.1.2", 379 | "parseurl": "~1.3.2", 380 | "vary": "~1.1.2" 381 | }, 382 | "dependencies": { 383 | "debug": { 384 | "version": "3.1.0", 385 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 386 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 387 | "requires": { 388 | "ms": "2.0.0" 389 | } 390 | } 391 | } 392 | }, 393 | "methods": { 394 | "version": "1.1.2", 395 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 396 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 397 | }, 398 | "mime": { 399 | "version": "1.6.0", 400 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 401 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 402 | }, 403 | "mime-db": { 404 | "version": "1.40.0", 405 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 406 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 407 | }, 408 | "mime-types": { 409 | "version": "2.1.24", 410 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 411 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 412 | "requires": { 413 | "mime-db": "1.40.0" 414 | } 415 | }, 416 | "ms": { 417 | "version": "2.0.0", 418 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 419 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 420 | }, 421 | "negotiator": { 422 | "version": "0.6.2", 423 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 424 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 425 | }, 426 | "on-finished": { 427 | "version": "2.3.0", 428 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 429 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 430 | "requires": { 431 | "ee-first": "1.1.1" 432 | } 433 | }, 434 | "packet-reader": { 435 | "version": "1.0.0", 436 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", 437 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" 438 | }, 439 | "parseqs": { 440 | "version": "0.0.6", 441 | "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", 442 | "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" 443 | }, 444 | "parseuri": { 445 | "version": "0.0.6", 446 | "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", 447 | "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" 448 | }, 449 | "parseurl": { 450 | "version": "1.3.3", 451 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 452 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 453 | }, 454 | "path-to-regexp": { 455 | "version": "0.1.7", 456 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 457 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 458 | }, 459 | "pg": { 460 | "version": "7.12.1", 461 | "resolved": "https://registry.npmjs.org/pg/-/pg-7.12.1.tgz", 462 | "integrity": "sha512-l1UuyfEvoswYfcUe6k+JaxiN+5vkOgYcVSbSuw3FvdLqDbaoa2RJo1zfJKfPsSYPFVERd4GHvX3s2PjG1asSDA==", 463 | "requires": { 464 | "buffer-writer": "2.0.0", 465 | "packet-reader": "1.0.0", 466 | "pg-connection-string": "0.1.3", 467 | "pg-pool": "^2.0.4", 468 | "pg-types": "^2.1.0", 469 | "pgpass": "1.x", 470 | "semver": "4.3.2" 471 | } 472 | }, 473 | "pg-connection-string": { 474 | "version": "0.1.3", 475 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz", 476 | "integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc=" 477 | }, 478 | "pg-int8": { 479 | "version": "1.0.1", 480 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", 481 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" 482 | }, 483 | "pg-pool": { 484 | "version": "2.0.7", 485 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-2.0.7.tgz", 486 | "integrity": "sha512-UiJyO5B9zZpu32GSlP0tXy8J2NsJ9EFGFfz5v6PSbdz/1hBLX1rNiiy5+mAm5iJJYwfCv4A0EBcQLGWwjbpzZw==" 487 | }, 488 | "pg-types": { 489 | "version": "2.2.0", 490 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", 491 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", 492 | "requires": { 493 | "pg-int8": "1.0.1", 494 | "postgres-array": "~2.0.0", 495 | "postgres-bytea": "~1.0.0", 496 | "postgres-date": "~1.0.4", 497 | "postgres-interval": "^1.1.0" 498 | } 499 | }, 500 | "pgpass": { 501 | "version": "1.0.2", 502 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz", 503 | "integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=", 504 | "requires": { 505 | "split": "^1.0.0" 506 | } 507 | }, 508 | "postgres-array": { 509 | "version": "2.0.0", 510 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", 511 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" 512 | }, 513 | "postgres-bytea": { 514 | "version": "1.0.0", 515 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 516 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" 517 | }, 518 | "postgres-date": { 519 | "version": "1.0.4", 520 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.4.tgz", 521 | "integrity": "sha512-bESRvKVuTrjoBluEcpv2346+6kgB7UlnqWZsnbnCccTNq/pqfj1j6oBaN5+b/NrDXepYUT/HKadqv3iS9lJuVA==" 522 | }, 523 | "postgres-interval": { 524 | "version": "1.2.0", 525 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", 526 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", 527 | "requires": { 528 | "xtend": "^4.0.0" 529 | } 530 | }, 531 | "proxy-addr": { 532 | "version": "2.0.5", 533 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", 534 | "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", 535 | "requires": { 536 | "forwarded": "~0.1.2", 537 | "ipaddr.js": "1.9.0" 538 | } 539 | }, 540 | "qs": { 541 | "version": "6.7.0", 542 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 543 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 544 | }, 545 | "range-parser": { 546 | "version": "1.2.1", 547 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 548 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 549 | }, 550 | "raw-body": { 551 | "version": "2.4.0", 552 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 553 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 554 | "requires": { 555 | "bytes": "3.1.0", 556 | "http-errors": "1.7.2", 557 | "iconv-lite": "0.4.24", 558 | "unpipe": "1.0.0" 559 | } 560 | }, 561 | "safe-buffer": { 562 | "version": "5.1.2", 563 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 564 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 565 | }, 566 | "safer-buffer": { 567 | "version": "2.1.2", 568 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 569 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 570 | }, 571 | "semver": { 572 | "version": "4.3.2", 573 | "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz", 574 | "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=" 575 | }, 576 | "send": { 577 | "version": "0.17.1", 578 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 579 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 580 | "requires": { 581 | "debug": "2.6.9", 582 | "depd": "~1.1.2", 583 | "destroy": "~1.0.4", 584 | "encodeurl": "~1.0.2", 585 | "escape-html": "~1.0.3", 586 | "etag": "~1.8.1", 587 | "fresh": "0.5.2", 588 | "http-errors": "~1.7.2", 589 | "mime": "1.6.0", 590 | "ms": "2.1.1", 591 | "on-finished": "~2.3.0", 592 | "range-parser": "~1.2.1", 593 | "statuses": "~1.5.0" 594 | }, 595 | "dependencies": { 596 | "ms": { 597 | "version": "2.1.1", 598 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 599 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 600 | } 601 | } 602 | }, 603 | "serve-static": { 604 | "version": "1.14.1", 605 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 606 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 607 | "requires": { 608 | "encodeurl": "~1.0.2", 609 | "escape-html": "~1.0.3", 610 | "parseurl": "~1.3.3", 611 | "send": "0.17.1" 612 | } 613 | }, 614 | "setprototypeof": { 615 | "version": "1.1.1", 616 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 617 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 618 | }, 619 | "socket.io": { 620 | "version": "2.4.0", 621 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.0.tgz", 622 | "integrity": "sha512-9UPJ1UTvKayuQfVv2IQ3k7tCQC/fboDyIK62i99dAQIyHKaBsNdTpwHLgKJ6guRWxRtC9H+138UwpaGuQO9uWQ==", 623 | "requires": { 624 | "debug": "~4.1.0", 625 | "engine.io": "~3.5.0", 626 | "has-binary2": "~1.0.2", 627 | "socket.io-adapter": "~1.1.0", 628 | "socket.io-client": "2.4.0", 629 | "socket.io-parser": "~3.4.0" 630 | }, 631 | "dependencies": { 632 | "debug": { 633 | "version": "4.1.1", 634 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 635 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 636 | "requires": { 637 | "ms": "^2.1.1" 638 | } 639 | }, 640 | "ms": { 641 | "version": "2.1.3", 642 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 643 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 644 | } 645 | } 646 | }, 647 | "socket.io-adapter": { 648 | "version": "1.1.2", 649 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", 650 | "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" 651 | }, 652 | "socket.io-client": { 653 | "version": "2.4.0", 654 | "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", 655 | "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", 656 | "requires": { 657 | "backo2": "1.0.2", 658 | "component-bind": "1.0.0", 659 | "component-emitter": "~1.3.0", 660 | "debug": "~3.1.0", 661 | "engine.io-client": "~3.5.0", 662 | "has-binary2": "~1.0.2", 663 | "indexof": "0.0.1", 664 | "parseqs": "0.0.6", 665 | "parseuri": "0.0.6", 666 | "socket.io-parser": "~3.3.0", 667 | "to-array": "0.1.4" 668 | }, 669 | "dependencies": { 670 | "debug": { 671 | "version": "3.1.0", 672 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 673 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 674 | "requires": { 675 | "ms": "2.0.0" 676 | } 677 | }, 678 | "socket.io-parser": { 679 | "version": "3.3.2", 680 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", 681 | "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", 682 | "requires": { 683 | "component-emitter": "~1.3.0", 684 | "debug": "~3.1.0", 685 | "isarray": "2.0.1" 686 | } 687 | } 688 | } 689 | }, 690 | "socket.io-parser": { 691 | "version": "3.4.1", 692 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", 693 | "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", 694 | "requires": { 695 | "component-emitter": "1.2.1", 696 | "debug": "~4.1.0", 697 | "isarray": "2.0.1" 698 | }, 699 | "dependencies": { 700 | "component-emitter": { 701 | "version": "1.2.1", 702 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", 703 | "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" 704 | }, 705 | "debug": { 706 | "version": "4.1.1", 707 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 708 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 709 | "requires": { 710 | "ms": "^2.1.1" 711 | } 712 | }, 713 | "ms": { 714 | "version": "2.1.3", 715 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 716 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 717 | } 718 | } 719 | }, 720 | "split": { 721 | "version": "1.0.1", 722 | "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", 723 | "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", 724 | "requires": { 725 | "through": "2" 726 | } 727 | }, 728 | "statuses": { 729 | "version": "1.5.0", 730 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 731 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 732 | }, 733 | "stoppable": { 734 | "version": "1.1.0", 735 | "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", 736 | "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==" 737 | }, 738 | "through": { 739 | "version": "2.3.8", 740 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 741 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 742 | }, 743 | "to-array": { 744 | "version": "0.1.4", 745 | "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", 746 | "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" 747 | }, 748 | "toidentifier": { 749 | "version": "1.0.0", 750 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 751 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 752 | }, 753 | "type-is": { 754 | "version": "1.6.18", 755 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 756 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 757 | "requires": { 758 | "media-typer": "0.3.0", 759 | "mime-types": "~2.1.24" 760 | } 761 | }, 762 | "unpipe": { 763 | "version": "1.0.0", 764 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 765 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 766 | }, 767 | "utils-merge": { 768 | "version": "1.0.1", 769 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 770 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 771 | }, 772 | "vary": { 773 | "version": "1.1.2", 774 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 775 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 776 | }, 777 | "ws": { 778 | "version": "7.4.2", 779 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", 780 | "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==" 781 | }, 782 | "xmlhttprequest-ssl": { 783 | "version": "1.5.5", 784 | "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", 785 | "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" 786 | }, 787 | "xtend": { 788 | "version": "4.0.2", 789 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 790 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 791 | }, 792 | "yeast": { 793 | "version": "0.1.2", 794 | "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", 795 | "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" 796 | } 797 | } 798 | } 799 | -------------------------------------------------------------------------------- /result/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "result", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "async": "^3.1.0", 13 | "body-parser": "^1.19.0", 14 | "cookie-parser": "^1.4.4", 15 | "express": "^4.17.1", 16 | "method-override": "^3.0.0", 17 | "pg": "^7.12.1", 18 | "socket.io": "^2.2.0", 19 | "stoppable": "^1.1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /result/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | async = require('async'), 3 | pg = require('pg'), 4 | { Pool } = require('pg'), 5 | path = require('path'), 6 | cookieParser = require('cookie-parser'), 7 | bodyParser = require('body-parser'), 8 | methodOverride = require('method-override'), 9 | app = express(), 10 | server = require('http').Server(app), 11 | io = require('socket.io')(server); 12 | 13 | io.set('transports', ['polling']); 14 | 15 | var port = process.env.PORT || 4000; 16 | 17 | io.sockets.on('connection', function (socket) { 18 | 19 | socket.emit('message', { text : 'Welcome!' }); 20 | 21 | socket.on('subscribe', function (data) { 22 | socket.join(data.channel); 23 | }); 24 | }); 25 | 26 | var pool = new pg.Pool({ 27 | connectionString: 'postgres://postgres:postgres@db/postgres' 28 | }); 29 | 30 | async.retry( 31 | {times: 1000, interval: 1000}, 32 | function(callback) { 33 | pool.connect(function(err, client, done) { 34 | if (err) { 35 | console.error("Waiting for db"); 36 | } 37 | callback(err, client); 38 | }); 39 | }, 40 | function(err, client) { 41 | if (err) { 42 | return console.error("Giving up"); 43 | } 44 | console.log("Connected to db"); 45 | getVotes(client); 46 | } 47 | ); 48 | 49 | function getVotes(client) { 50 | client.query('SELECT vote, COUNT(id) AS count FROM votes GROUP BY vote', [], function(err, result) { 51 | if (err) { 52 | console.error("Error performing query: " + err); 53 | } else { 54 | var votes = collectVotesFromResult(result); 55 | io.sockets.emit("scores", JSON.stringify(votes)); 56 | } 57 | 58 | setTimeout(function() {getVotes(client) }, 1000); 59 | }); 60 | } 61 | 62 | function collectVotesFromResult(result) { 63 | var votes = {a: 0, b: 0}; 64 | 65 | result.rows.forEach(function (row) { 66 | votes[row.vote] = parseInt(row.count); 67 | }); 68 | 69 | return votes; 70 | } 71 | 72 | app.use(cookieParser()); 73 | app.use(bodyParser()); 74 | app.use(methodOverride('X-HTTP-Method-Override')); 75 | app.use(function(req, res, next) { 76 | res.header("Access-Control-Allow-Origin", "*"); 77 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 78 | res.header("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS"); 79 | next(); 80 | }); 81 | 82 | app.use(express.static(__dirname + '/views')); 83 | 84 | app.get('/', function (req, res) { 85 | res.sendFile(path.resolve(__dirname + '/views/index.html')); 86 | }); 87 | 88 | server.listen(port, function () { 89 | var port = server.address().port; 90 | console.log('App running on port ' + port); 91 | }); 92 | -------------------------------------------------------------------------------- /result/tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.9-slim 2 | 3 | RUN apt-get update -qq && apt-get install -qy \ 4 | ca-certificates \ 5 | bzip2 \ 6 | curl \ 7 | libfontconfig \ 8 | --no-install-recommends 9 | RUN yarn global add phantomjs-prebuilt 10 | ADD . /app 11 | WORKDIR /app 12 | CMD ["/app/tests.sh"] 13 | -------------------------------------------------------------------------------- /result/tests/render.js: -------------------------------------------------------------------------------- 1 | var system = require('system'); 2 | var page = require('webpage').create(); 3 | var url = system.args[1]; 4 | 5 | page.onLoadFinished = function() { 6 | setTimeout(function(){ 7 | console.log(page.content); 8 | phantom.exit(); 9 | }, 1000); 10 | }; 11 | 12 | page.open(url, function() { 13 | page.evaluate(function() { 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /result/tests/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | while ! timeout 1 bash -c "echo > /dev/tcp/vote/80"; do 4 | sleep 1 5 | done 6 | 7 | curl -sS -X POST --data "vote=b" http://vote > /dev/null 8 | sleep 10 9 | 10 | if phantomjs render.js http://result | grep -q '1 vote'; then 11 | echo -e "\\e[42m------------" 12 | echo -e "\\e[92mTests passed" 13 | echo -e "\\e[42m------------" 14 | exit 0 15 | else 16 | echo -e "\\e[41m------------" 17 | echo -e "\\e[91mTests failed" 18 | echo -e "\\e[41m------------" 19 | exit 1 20 | fi 21 | -------------------------------------------------------------------------------- /result/views/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('catsvsdogs', []); 2 | var socket = io.connect({transports:['polling']}); 3 | 4 | var bg1 = document.getElementById('background-stats-1'); 5 | var bg2 = document.getElementById('background-stats-2'); 6 | 7 | app.controller('statsCtrl', function($scope){ 8 | $scope.aPercent = 50; 9 | $scope.bPercent = 50; 10 | 11 | var updateScores = function(){ 12 | socket.on('scores', function (json) { 13 | data = JSON.parse(json); 14 | var a = parseInt(data.a || 0); 15 | var b = parseInt(data.b || 0); 16 | 17 | var percentages = getPercentages(a, b); 18 | 19 | bg1.style.width = percentages.a + "%"; 20 | bg2.style.width = percentages.b + "%"; 21 | 22 | $scope.$apply(function () { 23 | $scope.aPercent = percentages.a; 24 | $scope.bPercent = percentages.b; 25 | $scope.total = a + b; 26 | }); 27 | }); 28 | }; 29 | 30 | var init = function(){ 31 | document.body.style.opacity=1; 32 | updateScores(); 33 | }; 34 | socket.on('message',function(data){ 35 | init(); 36 | }); 37 | }); 38 | 39 | function getPercentages(a, b) { 40 | var result = {}; 41 | 42 | if (a + b > 0) { 43 | result.a = Math.round(a / (a + b) * 100); 44 | result.b = 100 - result.a; 45 | } else { 46 | result.a = result.b = 50; 47 | } 48 | 49 | return result; 50 | } -------------------------------------------------------------------------------- /result/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cats vs Dogs -- Result 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
Cats
24 |
{{aPercent | number:1}}%
25 |
26 |
27 |
28 |
Dogs
29 |
{{bPercent | number:1}}%
30 |
31 |
32 |
33 |
34 |
35 | No votes yet 36 | {{total}} vote 37 | {{total}} votes 38 |
39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /result/views/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | *{ 4 | box-sizing:border-box; 5 | } 6 | html,body{ 7 | margin:0; 8 | padding:0; 9 | height:100%; 10 | font-family: 'Open Sans'; 11 | } 12 | body{ 13 | opacity:0; 14 | transition: all 1s linear; 15 | } 16 | 17 | .divider{ 18 | height: 150px; 19 | width:2px; 20 | background-color: #C0C9CE; 21 | position: relative; 22 | top: 50%; 23 | float: left; 24 | transform: translateY(-50%); 25 | } 26 | 27 | #background-stats-1{ 28 | background-color: #2196f3; 29 | } 30 | 31 | #background-stats-2{ 32 | background-color: #00cbca; 33 | } 34 | 35 | #content-container{ 36 | z-index:2; 37 | position:relative; 38 | margin:0 auto; 39 | display:table; 40 | padding:10px; 41 | max-width:940px; 42 | height:100%; 43 | } 44 | #content-container-center{ 45 | display:table-cell; 46 | text-align:center; 47 | vertical-align:middle; 48 | } 49 | #result{ 50 | z-index: 3; 51 | position: absolute; 52 | bottom: 40px; 53 | right: 20px; 54 | color: #fff; 55 | opacity: 0.5; 56 | font-size: 45px; 57 | font-weight: 600; 58 | } 59 | #choice{ 60 | transition: all 300ms linear; 61 | line-height:1.3em; 62 | background:#fff; 63 | box-shadow: 10px 0 0 #fff, -10px 0 0 #fff; 64 | vertical-align:middle; 65 | font-size:40px; 66 | font-weight: 600; 67 | width: 450px; 68 | height: 200px; 69 | } 70 | #choice a{ 71 | text-decoration:none; 72 | } 73 | #choice a:hover, #choice a:focus{ 74 | outline:0; 75 | text-decoration:underline; 76 | } 77 | 78 | #choice .choice{ 79 | width: 49%; 80 | position: relative; 81 | top: 50%; 82 | transform: translateY(-50%); 83 | text-align: left; 84 | padding-left: 50px; 85 | } 86 | 87 | #choice .choice .label{ 88 | text-transform: uppercase; 89 | } 90 | 91 | #choice .choice.dogs{ 92 | color: #00cbca; 93 | float: right; 94 | } 95 | 96 | #choice .choice.cats{ 97 | color: #2196f3; 98 | float: left; 99 | } 100 | #background-stats{ 101 | z-index:1; 102 | height:100%; 103 | width:100%; 104 | position:absolute; 105 | } 106 | #background-stats div{ 107 | transition: width 400ms ease-in-out; 108 | display:inline-block; 109 | margin-bottom:-4px; 110 | width:50%; 111 | height:100%; 112 | } 113 | -------------------------------------------------------------------------------- /seed-data/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | 3 | # add apache bench (ab) tool 4 | RUN apt-get update \ 5 | && apt-get install -y --no-install-recommends \ 6 | apache2-utils \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | WORKDIR /seed 10 | 11 | COPY . . 12 | 13 | # create POST data files with ab friendly formats 14 | RUN python make-data.py 15 | 16 | CMD /seed/generate-votes.sh -------------------------------------------------------------------------------- /seed-data/generate-votes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # create 3000 votes (2000 for option a, 1000 for option b) 4 | ab -n 1000 -c 50 -p posta -T "application/x-www-form-urlencoded" http://vote/ 5 | ab -n 1000 -c 50 -p postb -T "application/x-www-form-urlencoded" http://vote/ 6 | ab -n 1000 -c 50 -p posta -T "application/x-www-form-urlencoded" http://vote/ 7 | -------------------------------------------------------------------------------- /seed-data/make-data.py: -------------------------------------------------------------------------------- 1 | # this creates urlencode-friendly files without EOL 2 | import urllib.parse 3 | 4 | outfile = open('postb', 'w') 5 | params = ({ 'vote': 'b' }) 6 | encoded = urllib.parse.urlencode(params) 7 | outfile.write(encoded) 8 | outfile.close() 9 | outfile = open('posta', 'w') 10 | params = ({ 'vote': 'a' }) 11 | encoded = urllib.parse.urlencode(params) 12 | outfile.write(encoded) 13 | outfile.close() 14 | -------------------------------------------------------------------------------- /vote/Dockerfile: -------------------------------------------------------------------------------- 1 | # Using official python runtime base image 2 | FROM python:3.9-slim 3 | 4 | # add curl for healthcheck 5 | RUN apt-get update \ 6 | && apt-get install -y --no-install-recommends \ 7 | curl \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | # Set the application directory 11 | WORKDIR /app 12 | 13 | # Install our requirements.txt 14 | COPY requirements.txt /app/requirements.txt 15 | RUN pip install -r requirements.txt 16 | 17 | # Copy our code from the current folder to /app inside the container 18 | COPY . . 19 | 20 | # Make port 80 available for links and/or publish 21 | EXPOSE 80 22 | 23 | # Define our command to be run when launching the container 24 | CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--log-file", "-", "--access-logfile", "-", "--workers", "4", "--keep-alive", "0"] 25 | -------------------------------------------------------------------------------- /vote/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, make_response, g 2 | from redis import Redis 3 | import os 4 | import socket 5 | import random 6 | import json 7 | import logging 8 | 9 | option_a = os.getenv('OPTION_A', "Cats") 10 | option_b = os.getenv('OPTION_B', "Dogs") 11 | hostname = socket.gethostname() 12 | 13 | app = Flask(__name__) 14 | 15 | gunicorn_error_logger = logging.getLogger('gunicorn.error') 16 | app.logger.handlers.extend(gunicorn_error_logger.handlers) 17 | app.logger.setLevel(logging.INFO) 18 | 19 | def get_redis(): 20 | if not hasattr(g, 'redis'): 21 | g.redis = Redis(host="redis", db=0, socket_timeout=5) 22 | return g.redis 23 | 24 | @app.route("/", methods=['POST','GET']) 25 | def hello(): 26 | voter_id = request.cookies.get('voter_id') 27 | if not voter_id: 28 | voter_id = hex(random.getrandbits(64))[2:-1] 29 | 30 | vote = None 31 | 32 | if request.method == 'POST': 33 | redis = get_redis() 34 | vote = request.form['vote'] 35 | app.logger.info('Received vote for %s', vote) 36 | data = json.dumps({'voter_id': voter_id, 'vote': vote}) 37 | redis.rpush('votes', data) 38 | 39 | resp = make_response(render_template( 40 | 'index.html', 41 | option_a=option_a, 42 | option_b=option_b, 43 | hostname=hostname, 44 | vote=vote, 45 | )) 46 | resp.set_cookie('voter_id', voter_id) 47 | return resp 48 | 49 | 50 | if __name__ == "__main__": 51 | app.run(host='0.0.0.0', port=80, debug=True, threaded=True) 52 | -------------------------------------------------------------------------------- /vote/dotnet/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.1-sdk-nanoserver-sac2016 as builder 2 | 3 | WORKDIR /Vote 4 | COPY Vote/Vote.csproj . 5 | RUN dotnet restore 6 | 7 | COPY /Vote . 8 | RUN dotnet publish -c Release -o /out Vote.csproj 9 | 10 | # app image 11 | FROM microsoft/dotnet:2.1-aspnetcore-runtime-nanoserver-sac2016 12 | 13 | WORKDIR /app 14 | ENTRYPOINT ["dotnet", "Vote.dll"] 15 | 16 | COPY --from=builder /out . -------------------------------------------------------------------------------- /vote/dotnet/Dockerfile.1809: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 as builder 2 | 3 | WORKDIR /Vote 4 | COPY Vote/Vote.csproj . 5 | RUN dotnet restore 6 | 7 | COPY /Vote . 8 | RUN dotnet publish -c Release -o /out Vote.csproj 9 | 10 | # app image 11 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 12 | 13 | WORKDIR /app 14 | ENTRYPOINT ["dotnet", "Vote.dll"] 15 | 16 | COPY --from=builder /out . -------------------------------------------------------------------------------- /vote/dotnet/Vote/Messaging/IMessageQueue.cs: -------------------------------------------------------------------------------- 1 | using NATS.Client; 2 | using Vote.Messaging.Messages; 3 | 4 | namespace Vote.Messaging 5 | { 6 | public interface IMessageQueue 7 | { 8 | IConnection CreateConnection(); 9 | 10 | void Publish(TMessage message) where TMessage : Message; 11 | } 12 | } -------------------------------------------------------------------------------- /vote/dotnet/Vote/Messaging/MessageHelper.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Vote.Messaging.Messages; 3 | using System.Text; 4 | 5 | namespace Vote.Messaging 6 | { 7 | public class MessageHelper 8 | { 9 | public static byte[] ToData(TMessage message) 10 | where TMessage : Message 11 | { 12 | var json = JsonConvert.SerializeObject(message); 13 | return Encoding.Unicode.GetBytes(json); 14 | } 15 | 16 | public static TMessage FromData(byte[] data) 17 | where TMessage : Message 18 | { 19 | var json = Encoding.Unicode.GetString(data); 20 | return (TMessage)JsonConvert.DeserializeObject(json); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /vote/dotnet/Vote/Messaging/MessageQueue.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging; 3 | using NATS.Client; 4 | using Vote.Messaging.Messages; 5 | 6 | namespace Vote.Messaging 7 | { 8 | public class MessageQueue : IMessageQueue 9 | { 10 | protected readonly IConfiguration _configuration; 11 | protected readonly ILogger _logger; 12 | 13 | public MessageQueue(IConfiguration configuration, ILogger logger) 14 | { 15 | _configuration = configuration; 16 | _logger = logger; 17 | } 18 | 19 | public void Publish(TMessage message) 20 | where TMessage : Message 21 | { 22 | using (var connection = CreateConnection()) 23 | { 24 | var data = MessageHelper.ToData(message); 25 | connection.Publish(message.Subject, data); 26 | } 27 | } 28 | 29 | public IConnection CreateConnection() 30 | { 31 | var url = _configuration.GetValue("MessageQueue:Url"); 32 | return new ConnectionFactory().CreateConnection(url); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /vote/dotnet/Vote/Messaging/Messages/Events/VoteCastEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Vote.Messaging.Messages 4 | { 5 | public class VoteCastEvent : Message 6 | { 7 | public override string Subject { get { return MessageSubject; } } 8 | 9 | public string VoterId {get; set;} 10 | 11 | public string Vote {get; set; } 12 | 13 | public static string MessageSubject = "events.vote.votecast"; 14 | } 15 | } -------------------------------------------------------------------------------- /vote/dotnet/Vote/Messaging/Messages/Message.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Vote.Messaging.Messages 4 | { 5 | public abstract class Message 6 | { 7 | public string CorrelationId { get; set; } 8 | 9 | public abstract string Subject { get; } 10 | 11 | public Message() 12 | { 13 | CorrelationId = Guid.NewGuid().ToString(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vote/dotnet/Vote/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Vote.Pages.IndexModel 3 | 4 | 5 | 6 | 7 | 8 | @Model.OptionA vs @Model.OptionB! 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |

@Model.OptionA vs @Model.OptionB!

20 |
21 | 22 | 23 |
24 |
25 | (Tip: you can change your vote) 26 |
27 |
28 | Processed by container ID @System.Environment.MachineName 29 |
30 |
31 |
32 | 33 | @**@ 34 | 35 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /vote/dotnet/Vote/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.Logging; 6 | using Vote.Messaging; 7 | using Vote.Messaging.Messages; 8 | 9 | namespace Vote.Pages 10 | { 11 | public class IndexModel : PageModel 12 | { 13 | private string _optionA; 14 | private string _optionB; 15 | 16 | protected readonly IMessageQueue _messageQueue; 17 | protected readonly IConfiguration _configuration; 18 | protected readonly ILogger _logger; 19 | 20 | public IndexModel(IMessageQueue messageQueue, IConfiguration configuration, ILogger logger) 21 | { 22 | _messageQueue = messageQueue; 23 | _configuration = configuration; 24 | _logger = logger; 25 | 26 | _optionA = _configuration.GetValue("Voting:OptionA"); 27 | _optionB = _configuration.GetValue("Voting:OptionB"); 28 | } 29 | 30 | public string OptionA { get; private set; } 31 | 32 | public string OptionB { get; private set; } 33 | 34 | [BindProperty] 35 | public string Vote { get; private set; } 36 | 37 | private string _voterId 38 | { 39 | get { return TempData.Peek("VoterId") as string; } 40 | set { TempData["VoterId"] = value; } 41 | } 42 | 43 | public void OnGet() 44 | { 45 | OptionA = _optionA; 46 | OptionB = _optionB; 47 | } 48 | 49 | public IActionResult OnPost(string vote) 50 | { 51 | Vote = vote; 52 | OptionA = _optionA; 53 | OptionB = _optionB; 54 | if (_configuration.GetValue("MessageQueue:Enabled")) 55 | { 56 | PublishVote(vote); 57 | } 58 | return Page(); 59 | } 60 | 61 | private void PublishVote(string vote) 62 | { 63 | if (string.IsNullOrEmpty(_voterId)) 64 | { 65 | _voterId = Guid.NewGuid().ToString(); 66 | } 67 | var message = new VoteCastEvent 68 | { 69 | VoterId = _voterId, 70 | Vote = vote 71 | }; 72 | _messageQueue.Publish(message); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /vote/dotnet/Vote/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Vote 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | CreateWebHostBuilder(args).Build().Run(); 18 | } 19 | 20 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /vote/dotnet/Vote/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:6116", 7 | "sslPort": 44316 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Vote": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /vote/dotnet/Vote/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Vote.Messaging; 7 | 8 | namespace Vote 9 | { 10 | public class Startup 11 | { 12 | public Startup(IConfiguration configuration) 13 | { 14 | Configuration = configuration; 15 | } 16 | 17 | public IConfiguration Configuration { get; } 18 | 19 | public void ConfigureServices(IServiceCollection services) 20 | { 21 | services.AddMvc() 22 | .SetCompatibilityVersion(CompatibilityVersion.Version_2_1) 23 | .AddRazorPagesOptions(options => 24 | { 25 | options.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute()); 26 | }); 27 | services.AddTransient(); 28 | } 29 | 30 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 31 | { 32 | if (env.IsDevelopment()) 33 | { 34 | app.UseDeveloperExceptionPage(); 35 | } 36 | else 37 | { 38 | app.UseExceptionHandler("/Error"); 39 | } 40 | 41 | app.UseStaticFiles(); 42 | app.UseMvc(); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /vote/dotnet/Vote/Vote.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /vote/dotnet/Vote/Vote.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27703.2042 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vote", "Vote.csproj", "{DA159FEC-BE4D-4704-ACB2-E03FFA6F2D3B}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {DA159FEC-BE4D-4704-ACB2-E03FFA6F2D3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {DA159FEC-BE4D-4704-ACB2-E03FFA6F2D3B}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {DA159FEC-BE4D-4704-ACB2-E03FFA6F2D3B}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {DA159FEC-BE4D-4704-ACB2-E03FFA6F2D3B}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {BEABFEFC-9957-41E3-96A1-7F501F69411D} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /vote/dotnet/Vote/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "MessageQueue": { 3 | "Enabled": false 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Debug", 8 | "System": "Information", 9 | "Microsoft": "Information" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /vote/dotnet/Vote/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Voting": { 3 | "OptionA": "Cats", 4 | "OptionB": "Dogs" 5 | }, 6 | "MessageQueue": { 7 | "Enabled": true, 8 | "Url": "nats://message-queue:4222" 9 | }, 10 | "Logging": { 11 | "LogLevel": { 12 | "Default": "Information" 13 | } 14 | }, 15 | "AllowedHosts": "*" 16 | } 17 | -------------------------------------------------------------------------------- /vote/dotnet/Vote/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, body { 8 | margin: 0; 9 | padding: 0; 10 | background-color: #F7F8F9; 11 | height: 100vh; 12 | font-family: 'Open Sans'; 13 | } 14 | 15 | button { 16 | border-radius: 0; 17 | width: 100%; 18 | height: 50%; 19 | } 20 | 21 | button[type="submit"] { 22 | -webkit-appearance: none; 23 | -webkit-border-radius: 0; 24 | } 25 | 26 | button i { 27 | float: right; 28 | padding-right: 30px; 29 | margin-top: 3px; 30 | } 31 | 32 | button.a { 33 | background-color: #1aaaf8; 34 | } 35 | 36 | button.b { 37 | background-color: #00cbca; 38 | } 39 | 40 | #tip { 41 | text-align: left; 42 | color: #c0c9ce; 43 | font-size: 14px; 44 | } 45 | 46 | #hostname { 47 | position: absolute; 48 | bottom: 100px; 49 | right: 0; 50 | left: 0; 51 | color: #8f9ea8; 52 | font-size: 24px; 53 | } 54 | 55 | #content-container { 56 | z-index: 2; 57 | position: relative; 58 | margin: 0 auto; 59 | display: table; 60 | padding: 10px; 61 | max-width: 940px; 62 | height: 100%; 63 | } 64 | 65 | #content-container-center { 66 | display: table-cell; 67 | text-align: center; 68 | } 69 | 70 | #content-container-center h3 { 71 | color: #254356; 72 | } 73 | 74 | #choice { 75 | transition: all 300ms linear; 76 | line-height: 1.3em; 77 | display: inline; 78 | vertical-align: middle; 79 | font-size: 3em; 80 | } 81 | 82 | #choice a { 83 | text-decoration: none; 84 | } 85 | 86 | #choice a:hover, #choice a:focus { 87 | outline: 0; 88 | text-decoration: underline; 89 | } 90 | 91 | #choice button { 92 | display: block; 93 | height: 80px; 94 | width: 330px; 95 | border: none; 96 | color: white; 97 | text-transform: uppercase; 98 | font-size: 18px; 99 | font-weight: 700; 100 | margin-top: 10px; 101 | margin-bottom: 10px; 102 | text-align: left; 103 | padding-left: 50px; 104 | } 105 | 106 | #choice button.a:hover { 107 | background-color: #1488c6; 108 | } 109 | 110 | #choice button.b:hover { 111 | background-color: #00a2a1; 112 | } 113 | 114 | #choice button.a:focus { 115 | background-color: #1488c6; 116 | } 117 | 118 | #choice button.b:focus { 119 | background-color: #00a2a1; 120 | } 121 | 122 | #background-stats { 123 | z-index: 1; 124 | height: 100%; 125 | width: 100%; 126 | position: absolute; 127 | } 128 | 129 | #background-stats div { 130 | transition: width 400ms ease-in-out; 131 | display: inline-block; 132 | margin-bottom: -4px; 133 | width: 50%; 134 | height: 100%; 135 | } 136 | -------------------------------------------------------------------------------- /vote/dotnet/Vote/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}} -------------------------------------------------------------------------------- /vote/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Redis 3 | gunicorn 4 | -------------------------------------------------------------------------------- /vote/static/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,600); 2 | 3 | *{ 4 | box-sizing:border-box; 5 | } 6 | html,body{ 7 | margin: 0; 8 | padding: 0; 9 | background-color: #F7F8F9; 10 | height: 100vh; 11 | font-family: 'Open Sans'; 12 | } 13 | 14 | button{ 15 | border-radius: 0; 16 | width: 100%; 17 | height: 50%; 18 | } 19 | 20 | button[type="submit"] { 21 | -webkit-appearance:none; -webkit-border-radius:0; 22 | } 23 | 24 | button i{ 25 | float: right; 26 | padding-right: 30px; 27 | margin-top: 3px; 28 | } 29 | 30 | button.a{ 31 | background-color: #1aaaf8; 32 | } 33 | 34 | button.b{ 35 | background-color: #00cbca; 36 | } 37 | 38 | #tip{ 39 | text-align: left; 40 | color: #c0c9ce; 41 | font-size: 14px; 42 | } 43 | 44 | #hostname{ 45 | position: absolute; 46 | bottom: 100px; 47 | right: 0; 48 | left: 0; 49 | color: #8f9ea8; 50 | font-size: 24px; 51 | } 52 | 53 | #content-container{ 54 | z-index: 2; 55 | position: relative; 56 | margin: 0 auto; 57 | display: table; 58 | padding: 10px; 59 | max-width: 940px; 60 | height: 100%; 61 | } 62 | #content-container-center{ 63 | display: table-cell; 64 | text-align: center; 65 | } 66 | 67 | #content-container-center h3{ 68 | color: #254356; 69 | } 70 | 71 | #choice{ 72 | transition: all 300ms linear; 73 | line-height: 1.3em; 74 | display: inline; 75 | vertical-align: middle; 76 | font-size: 3em; 77 | } 78 | #choice a{ 79 | text-decoration:none; 80 | } 81 | #choice a:hover, #choice a:focus{ 82 | outline:0; 83 | text-decoration:underline; 84 | } 85 | 86 | #choice button{ 87 | display: block; 88 | height: 80px; 89 | width: 330px; 90 | border: none; 91 | color: white; 92 | text-transform: uppercase; 93 | font-size:18px; 94 | font-weight: 700; 95 | margin-top: 10px; 96 | margin-bottom: 10px; 97 | text-align: left; 98 | padding-left: 50px; 99 | } 100 | 101 | #choice button.a:hover{ 102 | background-color: #1488c6; 103 | } 104 | 105 | #choice button.b:hover{ 106 | background-color: #00a2a1; 107 | } 108 | 109 | #choice button.a:focus{ 110 | background-color: #1488c6; 111 | } 112 | 113 | #choice button.b:focus{ 114 | background-color: #00a2a1; 115 | } 116 | 117 | #background-stats{ 118 | z-index:1; 119 | height:100%; 120 | width:100%; 121 | position:absolute; 122 | } 123 | #background-stats div{ 124 | transition: width 400ms ease-in-out; 125 | display:inline-block; 126 | margin-bottom:-4px; 127 | width:50%; 128 | height:100%; 129 | } 130 | -------------------------------------------------------------------------------- /vote/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{option_a}} vs {{option_b}}! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

{{option_a}} vs {{option_b}}!

17 |
18 | 19 | 20 |
21 |
22 | (Tip: you can change your vote) 23 |
24 |
25 | Processed by container ID {{hostname}} 26 |
27 |
28 |
29 | 30 | 31 | 32 | {% if vote %} 33 | 47 | {% endif %} 48 | 49 | 50 | -------------------------------------------------------------------------------- /worker/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /worker/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | worker 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.m2e.core.maven2Nature 22 | 23 | 24 | -------------------------------------------------------------------------------- /worker/.settings/org.eclipse.jdt.apt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.apt.aptEnabled=false 3 | -------------------------------------------------------------------------------- /worker/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 3 | org.eclipse.jdt.core.compiler.compliance=1.7 4 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 5 | org.eclipse.jdt.core.compiler.processAnnotations=disabled 6 | org.eclipse.jdt.core.compiler.release=disabled 7 | org.eclipse.jdt.core.compiler.source=1.7 8 | -------------------------------------------------------------------------------- /worker/.settings/org.eclipse.m2e.core.prefs: -------------------------------------------------------------------------------- 1 | activeProfiles= 2 | eclipse.preferences.version=1 3 | resolveWorkspaceProjects=true 4 | version=1 5 | -------------------------------------------------------------------------------- /worker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 as builder 2 | 3 | WORKDIR /Worker 4 | COPY src/Worker/Worker.csproj . 5 | RUN dotnet restore 6 | 7 | COPY src/Worker/ . 8 | RUN dotnet publish -c Release -o /out Worker.csproj 9 | 10 | # app image 11 | FROM mcr.microsoft.com/dotnet/core/runtime:3.1 12 | 13 | WORKDIR /app 14 | ENTRYPOINT ["dotnet", "Worker.dll"] 15 | 16 | COPY --from=builder /out . -------------------------------------------------------------------------------- /worker/Dockerfile.j: -------------------------------------------------------------------------------- 1 | FROM maven:3.5-jdk-8-alpine AS build 2 | 3 | WORKDIR /code 4 | 5 | COPY pom.xml /code/pom.xml 6 | RUN ["mvn", "dependency:resolve"] 7 | RUN ["mvn", "verify"] 8 | 9 | # Adding source, compile and package into a fat jar 10 | COPY ["src/main", "/code/src/main"] 11 | RUN ["mvn", "package"] 12 | 13 | FROM openjdk:8-jre-alpine 14 | 15 | COPY --from=build /code/target/worker-jar-with-dependencies.jar / 16 | 17 | CMD ["java", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-jar", "/worker-jar-with-dependencies.jar"] 18 | -------------------------------------------------------------------------------- /worker/Dockerfile.net: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:1.1.1-sdk 2 | 3 | WORKDIR /code 4 | 5 | ADD src/Worker /code/src/Worker 6 | 7 | RUN dotnet restore -v minimal src/Worker \ 8 | && dotnet publish -c Release -o "./" "src/Worker/" 9 | 10 | CMD dotnet src/Worker/Worker.dll 11 | -------------------------------------------------------------------------------- /worker/dotnet/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.1-sdk-nanoserver-sac2016 as builder 2 | 3 | WORKDIR /Worker 4 | COPY Worker/Worker.csproj . 5 | RUN dotnet restore 6 | 7 | COPY /Worker . 8 | RUN dotnet publish -c Release -o /out Worker.csproj 9 | 10 | # app image 11 | FROM microsoft/dotnet:2.1-runtime-nanoserver-sac2016 12 | 13 | WORKDIR /app 14 | ENTRYPOINT ["dotnet", "Worker.dll"] 15 | 16 | COPY --from=builder /out . -------------------------------------------------------------------------------- /worker/dotnet/Dockerfile.1809: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 as builder 2 | 3 | WORKDIR /Worker 4 | COPY Worker/Worker.csproj . 5 | RUN dotnet restore 6 | 7 | COPY /Worker . 8 | RUN dotnet publish -c Release -o /out Worker.csproj 9 | 10 | # app image 11 | FROM mcr.microsoft.com/dotnet/core/runtime:3.1 12 | 13 | WORKDIR /app 14 | ENTRYPOINT ["dotnet", "Worker.dll"] 15 | 16 | COPY --from=builder /out . -------------------------------------------------------------------------------- /worker/dotnet/Worker/Data/IVoteData.cs: -------------------------------------------------------------------------------- 1 | namespace Worker.Data 2 | { 3 | public interface IVoteData 4 | { 5 | void Set(string voterId, string vote); 6 | } 7 | } -------------------------------------------------------------------------------- /worker/dotnet/Worker/Data/MySqlVoteData.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Worker.Entities; 3 | 4 | namespace Worker.Data 5 | { 6 | public class MySqlVoteData : IVoteData 7 | { 8 | private readonly VoteContext _context; 9 | private readonly ILogger _logger; 10 | 11 | public MySqlVoteData(VoteContext context, ILogger logger) 12 | { 13 | _context = context; 14 | _logger = logger; 15 | } 16 | 17 | public void Set(string voterId, string vote) 18 | { 19 | var currentVote = _context.Votes.Find(voterId); 20 | if (currentVote == null) 21 | { 22 | _context.Votes.Add(new Vote 23 | { 24 | VoterId = voterId, 25 | VoteOption = vote 26 | }); 27 | } 28 | else if (currentVote.VoteOption != vote) 29 | { 30 | currentVote.VoteOption = vote; 31 | } 32 | _context.SaveChanges(); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /worker/dotnet/Worker/Entities/Vote.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace Worker.Entities 5 | { 6 | [Table("votes")] 7 | public class Vote 8 | { 9 | [Column("id")] 10 | [Key] 11 | public string VoterId { get; set; } 12 | 13 | [Column("vote")] 14 | public string VoteOption { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /worker/dotnet/Worker/Entities/VoteContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Worker.Entities 4 | { 5 | public class VoteContext : DbContext 6 | { 7 | private static bool _EnsureCreated; 8 | public VoteContext(DbContextOptions options) : base(options) 9 | { 10 | if (!_EnsureCreated) 11 | { 12 | Database.EnsureCreated(); 13 | _EnsureCreated = true; 14 | } 15 | } 16 | 17 | public DbSet Votes { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /worker/dotnet/Worker/Messaging/IMessageQueue.cs: -------------------------------------------------------------------------------- 1 | using NATS.Client; 2 | using Worker.Messaging.Messages; 3 | 4 | namespace Worker.Messaging 5 | { 6 | public interface IMessageQueue 7 | { 8 | IConnection CreateConnection(); 9 | 10 | void Publish(TMessage message) where TMessage : Message; 11 | } 12 | } -------------------------------------------------------------------------------- /worker/dotnet/Worker/Messaging/MessageHelper.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Worker.Messaging.Messages; 3 | using System.Text; 4 | 5 | namespace Worker.Messaging 6 | { 7 | public class MessageHelper 8 | { 9 | public static byte[] ToData(TMessage message) 10 | where TMessage : Message 11 | { 12 | var json = JsonConvert.SerializeObject(message); 13 | return Encoding.Unicode.GetBytes(json); 14 | } 15 | 16 | public static TMessage FromData(byte[] data) 17 | where TMessage : Message 18 | { 19 | var json = Encoding.Unicode.GetString(data); 20 | return (TMessage)JsonConvert.DeserializeObject(json); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /worker/dotnet/Worker/Messaging/MessageQueue.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging; 3 | using NATS.Client; 4 | using Worker.Messaging.Messages; 5 | 6 | namespace Worker.Messaging 7 | { 8 | public class MessageQueue : IMessageQueue 9 | { 10 | protected readonly IConfiguration _configuration; 11 | protected readonly ILogger _logger; 12 | 13 | public MessageQueue(IConfiguration configuration, ILogger logger) 14 | { 15 | _configuration = configuration; 16 | _logger = logger; 17 | } 18 | 19 | public void Publish(TMessage message) 20 | where TMessage : Message 21 | { 22 | using (var connection = CreateConnection()) 23 | { 24 | var data = MessageHelper.ToData(message); 25 | connection.Publish(message.Subject, data); 26 | } 27 | } 28 | 29 | public IConnection CreateConnection() 30 | { 31 | var url = _configuration.GetValue("MessageQueue:Url"); 32 | return new ConnectionFactory().CreateConnection(url); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /worker/dotnet/Worker/Messaging/Messages/Events/VoteCastEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Worker.Messaging.Messages 4 | { 5 | public class VoteCastEvent : Message 6 | { 7 | public override string Subject { get { return MessageSubject; } } 8 | 9 | public string VoterId {get; set;} 10 | 11 | public string Vote {get; set; } 12 | 13 | public static string MessageSubject = "events.vote.votecast"; 14 | } 15 | } -------------------------------------------------------------------------------- /worker/dotnet/Worker/Messaging/Messages/Message.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Worker.Messaging.Messages 4 | { 5 | public abstract class Message 6 | { 7 | public string CorrelationId { get; set; } 8 | 9 | public abstract string Subject { get; } 10 | 11 | public Message() 12 | { 13 | CorrelationId = Guid.NewGuid().ToString(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /worker/dotnet/Worker/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using Worker.Data; 7 | using Worker.Entities; 8 | using Worker.Messaging; 9 | using Worker.Workers; 10 | 11 | namespace Worker 12 | { 13 | class Program 14 | { 15 | static void Main(string[] args) 16 | { 17 | var config = new ConfigurationBuilder() 18 | .AddJsonFile("appsettings.json") 19 | .AddEnvironmentVariables() 20 | .Build(); 21 | 22 | var loggerFactory = new LoggerFactory() 23 | .AddConsole(); 24 | 25 | var services = new ServiceCollection() 26 | .AddSingleton(loggerFactory) 27 | .AddLogging() 28 | .AddSingleton(config) 29 | .AddTransient() 30 | .AddTransient() 31 | .AddSingleton() 32 | .AddDbContext(builder => builder.UseMySQL(config.GetConnectionString("VoteData"))); 33 | 34 | var provider = services.BuildServiceProvider(); 35 | var worker = provider.GetService(); 36 | worker.Start(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /worker/dotnet/Worker/Worker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | PreserveNewest 15 | PreserveNewest 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /worker/dotnet/Worker/Workers/QueueWorker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.Logging; 5 | using NATS.Client; 6 | using Worker.Data; 7 | using Worker.Messaging; 8 | using Worker.Messaging.Messages; 9 | 10 | namespace Worker.Workers 11 | { 12 | public class QueueWorker 13 | { 14 | private static ManualResetEvent _ResetEvent = new ManualResetEvent(false); 15 | private const string QUEUE_GROUP = "save-handler"; 16 | 17 | private readonly IMessageQueue _messageQueue; 18 | private readonly IConfiguration _config; 19 | private readonly IVoteData _data; 20 | protected readonly ILogger _logger; 21 | 22 | public QueueWorker(IMessageQueue messageQueue, IVoteData data, IConfiguration config, ILogger logger) 23 | { 24 | _messageQueue = messageQueue; 25 | _data = data; 26 | _config = config; 27 | _logger = logger; 28 | } 29 | 30 | public void Start() 31 | { 32 | _logger.LogInformation($"Connecting to message queue url: {_config.GetValue("MessageQueue:Url")}"); 33 | using (var connection = _messageQueue.CreateConnection()) 34 | { 35 | var subscription = connection.SubscribeAsync(VoteCastEvent.MessageSubject, QUEUE_GROUP); 36 | subscription.MessageHandler += SaveVote; 37 | subscription.Start(); 38 | _logger.LogInformation($"Listening on subject: {VoteCastEvent.MessageSubject}, queue: {QUEUE_GROUP}"); 39 | 40 | _ResetEvent.WaitOne(); 41 | connection.Close(); 42 | } 43 | } 44 | 45 | private void SaveVote(object sender, MsgHandlerEventArgs e) 46 | { 47 | _logger.LogDebug($"Received message, subject: {e.Message.Subject}"); 48 | var voteMessage = MessageHelper.FromData(e.Message.Data); 49 | _logger.LogInformation($"Processing vote for '{voteMessage.Vote}' by '{voteMessage.VoterId}'"); 50 | try 51 | { 52 | _data.Set(voteMessage.VoterId, voteMessage.Vote); 53 | _logger.LogDebug($"Succesffuly processed vote by '{voteMessage.VoterId}'"); 54 | } 55 | catch (Exception ex) 56 | { 57 | _logger.LogError($"Vote processing FAILED for '{voteMessage.VoterId}', exception: {ex}"); 58 | } 59 | 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /worker/dotnet/Worker/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "MessageQueue": { 3 | "Url": "nats://message-queue:4222" 4 | }, 5 | "ConnectionStrings": { 6 | "VoteData": "Server=mysql;Port=4000;Database=votes;User=root;SslMode=None" 7 | }, 8 | "Logging": { 9 | "LogLevel": { 10 | "Default": "Information" 11 | } 12 | }, 13 | "AllowedHosts": "*" 14 | } 15 | -------------------------------------------------------------------------------- /worker/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | worker 7 | worker 8 | 1.0-SNAPSHOT 9 | 10 | 11 | 12 | 13 | org.json 14 | json 15 | 20140107 16 | 17 | 18 | 19 | redis.clients 20 | jedis 21 | 2.7.2 22 | jar 23 | compile 24 | 25 | 26 | 27 | org.postgresql 28 | postgresql 29 | 9.4-1200-jdbc41 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.apache.maven.plugins 37 | maven-jar-plugin 38 | 2.4 39 | 40 | worker 41 | 42 | 43 | true 44 | worker.Worker 45 | dependency-jars/ 46 | 47 | 48 | 49 | 50 | 51 | org.apache.maven.plugins 52 | maven-compiler-plugin 53 | 3.1 54 | 55 | 1.7 56 | 1.7 57 | 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-assembly-plugin 62 | 63 | 64 | 65 | attached 66 | 67 | package 68 | 69 | worker 70 | 71 | jar-with-dependencies 72 | 73 | 74 | 75 | worker.Worker 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /worker/src/Worker/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using System.Threading; 7 | using Newtonsoft.Json; 8 | using Npgsql; 9 | using StackExchange.Redis; 10 | 11 | namespace Worker 12 | { 13 | public class Program 14 | { 15 | public static int Main(string[] args) 16 | { 17 | try 18 | { 19 | var pgsql = OpenDbConnection("Server=db;Username=postgres;Password=postgres;"); 20 | var redisConn = OpenRedisConnection("redis"); 21 | var redis = redisConn.GetDatabase(); 22 | 23 | // Keep alive is not implemented in Npgsql yet. This workaround was recommended: 24 | // https://github.com/npgsql/npgsql/issues/1214#issuecomment-235828359 25 | var keepAliveCommand = pgsql.CreateCommand(); 26 | keepAliveCommand.CommandText = "SELECT 1"; 27 | 28 | var definition = new { vote = "", voter_id = "" }; 29 | while (true) 30 | { 31 | // Slow down to prevent CPU spike, only query each 100ms 32 | Thread.Sleep(100); 33 | 34 | // Reconnect redis if down 35 | if (redisConn == null || !redisConn.IsConnected) { 36 | Console.WriteLine("Reconnecting Redis"); 37 | redisConn = OpenRedisConnection("redis"); 38 | redis = redisConn.GetDatabase(); 39 | } 40 | string json = redis.ListLeftPopAsync("votes").Result; 41 | if (json != null) 42 | { 43 | var vote = JsonConvert.DeserializeAnonymousType(json, definition); 44 | Console.WriteLine($"Processing vote for '{vote.vote}' by '{vote.voter_id}'"); 45 | // Reconnect DB if down 46 | if (!pgsql.State.Equals(System.Data.ConnectionState.Open)) 47 | { 48 | Console.WriteLine("Reconnecting DB"); 49 | pgsql = OpenDbConnection("Server=db;Username=postgres;Password=postgres;"); 50 | } 51 | else 52 | { // Normal +1 vote requested 53 | UpdateVote(pgsql, vote.voter_id, vote.vote); 54 | } 55 | } 56 | else 57 | { 58 | keepAliveCommand.ExecuteNonQuery(); 59 | } 60 | } 61 | } 62 | catch (Exception ex) 63 | { 64 | Console.Error.WriteLine(ex.ToString()); 65 | return 1; 66 | } 67 | } 68 | 69 | private static NpgsqlConnection OpenDbConnection(string connectionString) 70 | { 71 | NpgsqlConnection connection; 72 | 73 | while (true) 74 | { 75 | try 76 | { 77 | connection = new NpgsqlConnection(connectionString); 78 | connection.Open(); 79 | break; 80 | } 81 | catch (SocketException) 82 | { 83 | Console.Error.WriteLine("Waiting for db"); 84 | Thread.Sleep(1000); 85 | } 86 | catch (DbException) 87 | { 88 | Console.Error.WriteLine("Waiting for db"); 89 | Thread.Sleep(1000); 90 | } 91 | } 92 | 93 | Console.Error.WriteLine("Connected to db"); 94 | 95 | var command = connection.CreateCommand(); 96 | command.CommandText = @"CREATE TABLE IF NOT EXISTS votes ( 97 | id VARCHAR(255) NOT NULL UNIQUE, 98 | vote VARCHAR(255) NOT NULL 99 | )"; 100 | command.ExecuteNonQuery(); 101 | 102 | return connection; 103 | } 104 | 105 | private static ConnectionMultiplexer OpenRedisConnection(string hostname) 106 | { 107 | // Use IP address to workaround https://github.com/StackExchange/StackExchange.Redis/issues/410 108 | var ipAddress = GetIp(hostname); 109 | Console.WriteLine($"Found redis at {ipAddress}"); 110 | 111 | while (true) 112 | { 113 | try 114 | { 115 | Console.Error.WriteLine("Connecting to redis"); 116 | return ConnectionMultiplexer.Connect(ipAddress); 117 | } 118 | catch (RedisConnectionException) 119 | { 120 | Console.Error.WriteLine("Waiting for redis"); 121 | Thread.Sleep(1000); 122 | } 123 | } 124 | } 125 | 126 | private static string GetIp(string hostname) 127 | => Dns.GetHostEntryAsync(hostname) 128 | .Result 129 | .AddressList 130 | .First(a => a.AddressFamily == AddressFamily.InterNetwork) 131 | .ToString(); 132 | 133 | private static void UpdateVote(NpgsqlConnection connection, string voterId, string vote) 134 | { 135 | var command = connection.CreateCommand(); 136 | try 137 | { 138 | command.CommandText = "INSERT INTO votes (id, vote) VALUES (@id, @vote)"; 139 | command.Parameters.AddWithValue("@id", voterId); 140 | command.Parameters.AddWithValue("@vote", vote); 141 | command.ExecuteNonQuery(); 142 | } 143 | catch (DbException) 144 | { 145 | command.CommandText = "UPDATE votes SET vote = @vote WHERE id = @id"; 146 | command.ExecuteNonQuery(); 147 | } 148 | finally 149 | { 150 | command.Dispose(); 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /worker/src/Worker/Worker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /worker/src/main/java/worker/Worker.java: -------------------------------------------------------------------------------- 1 | package worker; 2 | 3 | import redis.clients.jedis.Jedis; 4 | import redis.clients.jedis.exceptions.JedisConnectionException; 5 | import java.sql.*; 6 | import org.json.JSONObject; 7 | 8 | class Worker { 9 | public static void main(String[] args) { 10 | try { 11 | Jedis redis = connectToRedis("redis"); 12 | Connection dbConn = connectToDB("db"); 13 | 14 | System.err.println("Watching vote queue"); 15 | 16 | while (true) { 17 | String voteJSON = redis.blpop(0, "votes").get(1); 18 | JSONObject voteData = new JSONObject(voteJSON); 19 | String voterID = voteData.getString("voter_id"); 20 | String vote = voteData.getString("vote"); 21 | 22 | System.err.printf("Processing vote for '%s' by '%s'\n", vote, voterID); 23 | updateVote(dbConn, voterID, vote); 24 | } 25 | } catch (SQLException e) { 26 | e.printStackTrace(); 27 | System.exit(1); 28 | } 29 | } 30 | 31 | static void updateVote(Connection dbConn, String voterID, String vote) throws SQLException { 32 | PreparedStatement insert = dbConn.prepareStatement( 33 | "INSERT INTO votes (id, vote) VALUES (?, ?)"); 34 | insert.setString(1, voterID); 35 | insert.setString(2, vote); 36 | 37 | try { 38 | insert.executeUpdate(); 39 | } catch (SQLException e) { 40 | PreparedStatement update = dbConn.prepareStatement( 41 | "UPDATE votes SET vote = ? WHERE id = ?"); 42 | update.setString(1, vote); 43 | update.setString(2, voterID); 44 | update.executeUpdate(); 45 | } 46 | } 47 | 48 | static Jedis connectToRedis(String host) { 49 | Jedis conn = new Jedis(host); 50 | 51 | while (true) { 52 | try { 53 | conn.keys("*"); 54 | break; 55 | } catch (JedisConnectionException e) { 56 | System.err.println("Waiting for redis"); 57 | sleep(1000); 58 | } 59 | } 60 | 61 | System.err.println("Connected to redis"); 62 | return conn; 63 | } 64 | 65 | static Connection connectToDB(String host) throws SQLException { 66 | Connection conn = null; 67 | 68 | try { 69 | 70 | Class.forName("org.postgresql.Driver"); 71 | String url = "jdbc:postgresql://" + host + "/postgres"; 72 | 73 | while (conn == null) { 74 | try { 75 | conn = DriverManager.getConnection(url, "postgres", "postgres"); 76 | } catch (SQLException e) { 77 | System.err.println("Waiting for db"); 78 | sleep(1000); 79 | } 80 | } 81 | 82 | PreparedStatement st = conn.prepareStatement( 83 | "CREATE TABLE IF NOT EXISTS votes (id VARCHAR(255) NOT NULL UNIQUE, vote VARCHAR(255) NOT NULL)"); 84 | st.executeUpdate(); 85 | 86 | } catch (ClassNotFoundException e) { 87 | e.printStackTrace(); 88 | System.exit(1); 89 | } 90 | 91 | System.err.println("Connected to db"); 92 | return conn; 93 | } 94 | 95 | static void sleep(long duration) { 96 | try { 97 | Thread.sleep(duration); 98 | } catch (InterruptedException e) { 99 | System.exit(1); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /worker/target/classes/worker/Worker.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmumshad/example-voting-app/38826ff7c00c398203f3074c18c9fcee41e1a512/worker/target/classes/worker/Worker.class --------------------------------------------------------------------------------