Repository: smancke/guble Branch: master Commit: 83e654d595ec Files: 190 Total size: 597.1 KB Directory structure: gitextract_j4dqg380/ ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── api/ │ └── swagger.yaml ├── client/ │ ├── client.go │ ├── client_test.go │ └── mocks_client_gen_test.go ├── guble-cli/ │ ├── README.md │ ├── main.go │ └── main_test.go ├── logformatter/ │ ├── logstash_formatter.go │ └── logstash_formatter_test.go ├── main.go ├── protocol/ │ ├── cmd.go │ ├── cmd_test.go │ ├── log.go │ ├── log_test.go │ ├── message.go │ ├── message_test.go │ └── path.go ├── restclient/ │ ├── guble_sender.go │ ├── guble_sender_test.go │ ├── logger.go │ └── sender.go ├── scripts/ │ ├── Dockerfile-cluster │ ├── compose.cluster.test.yml │ ├── compose.postgres.test.yml │ ├── cov.sh │ ├── dependencies_graph.sh │ ├── file-hex.sh │ ├── generate_coverage.sh │ └── generate_mocks.sh ├── server/ │ ├── apns/ │ │ ├── apns.go │ │ ├── apns_metrics.go │ │ ├── apns_pusher.go │ │ ├── apns_sender.go │ │ ├── apns_sender_test.go │ │ ├── apns_test.go │ │ ├── logger.go │ │ ├── mocks_connector_gen_test.go │ │ ├── mocks_kvstore_gen_test.go │ │ ├── mocks_pusher_gen_test.go │ │ └── mocks_router_gen_test.go │ ├── auth/ │ │ ├── accessmanager.go │ │ ├── accessmanager_test.go │ │ ├── allow_all_accessmanager.go │ │ ├── logger.go │ │ ├── mocks_auth_gen_test.go │ │ └── rest_accessmanager.go │ ├── benchmarking_apns_test.go │ ├── benchmarking_common_test.go │ ├── benchmarking_fcm_test.go │ ├── benchmarking_fetch_test.go │ ├── benchmarking_test.go │ ├── cluster/ │ │ ├── cluster.go │ │ ├── cluster_benchmarking_test.go │ │ ├── cluster_conflict.go │ │ ├── cluster_delegate.go │ │ ├── cluster_event_delegate.go │ │ ├── cluster_test.go │ │ ├── codec.go │ │ ├── codec_test.go │ │ ├── logger.go │ │ └── synchronizer.go │ ├── cluster_integration_test.go │ ├── config.go │ ├── config_test.go │ ├── connector/ │ │ ├── connector.go │ │ ├── connector_test.go │ │ ├── logger.go │ │ ├── manager.go │ │ ├── manager_test.go │ │ ├── mocks_connector_gen_test.go │ │ ├── mocks_kvstore_gen_test.go │ │ ├── mocks_router_gen_test.go │ │ ├── queue.go │ │ ├── request.go │ │ ├── subscriber.go │ │ └── substitution.go │ ├── fcm/ │ │ ├── fcm.go │ │ ├── fcm_metrics.go │ │ ├── fcm_sender.go │ │ ├── fcm_test.go │ │ ├── json_error.go │ │ ├── logger.go │ │ ├── mocks_gcm_gen_test.go │ │ ├── mocks_kvstore_gen_test.go │ │ ├── mocks_router_gen_test.go │ │ ├── mocks_store_gen_test.go │ │ └── testutil.go │ ├── fcm_integration_test.go │ ├── gubled.go │ ├── gubled_test.go │ ├── integration_test.go │ ├── kvstore/ │ │ ├── common_test.go │ │ ├── gorm.go │ │ ├── kvstore.go │ │ ├── memory.go │ │ ├── memory_test.go │ │ ├── postgres.go │ │ ├── postgres_config.go │ │ ├── postgres_config_test.go │ │ ├── postgres_test.go │ │ ├── sqlite.go │ │ └── sqlite_test.go │ ├── logger.go │ ├── metrics/ │ │ ├── average.go │ │ ├── average_test.go │ │ ├── disabled.go │ │ ├── enabled.go │ │ ├── enabled_test.go │ │ ├── int.go │ │ ├── map.go │ │ ├── metrics.go │ │ ├── metrics_test.go │ │ ├── ns.go │ │ ├── rate.go │ │ ├── rate_test.go │ │ ├── time.go │ │ └── zero.go │ ├── mocks_apns_pusher_gen_test.go │ ├── mocks_auth_gen_test.go │ ├── mocks_router_gen_test.go │ ├── mocks_store_gen_test.go │ ├── redundancy_test.go │ ├── rest/ │ │ ├── mocks_router_gen_test.go │ │ ├── rest_message_api.go │ │ └── rest_message_api_test.go │ ├── router/ │ │ ├── errors.go │ │ ├── logger.go │ │ ├── message_queue.go │ │ ├── mocks_auth_gen_test.go │ │ ├── mocks_checker_gen_test.go │ │ ├── mocks_kvstore_gen_test.go │ │ ├── mocks_router_gen_test.go │ │ ├── mocks_store_gen_test.go │ │ ├── route.go │ │ ├── route_config.go │ │ ├── route_config_test.go │ │ ├── route_params.go │ │ ├── route_test.go │ │ ├── router.go │ │ ├── router_metrics.go │ │ └── router_test.go │ ├── service/ │ │ ├── logger.go │ │ ├── mocks_checker_gen_test.go │ │ ├── mocks_router_gen_test.go │ │ ├── module.go │ │ ├── service.go │ │ └── service_test.go │ ├── sms/ │ │ ├── logger.go │ │ ├── mocks_router_gen_test.go │ │ ├── mocks_sender_gen_test.go │ │ ├── mocks_store_gen_test.go │ │ ├── nexmo_sms.go │ │ ├── nexmo_sms_sender.go │ │ ├── nexmo_sms_sender_test.go │ │ ├── sms_gateway.go │ │ ├── sms_gateway_test.go │ │ └── sms_metrics.go │ ├── store/ │ │ ├── dummystore/ │ │ │ ├── dummy_message_store.go │ │ │ └── dummy_message_store_test.go │ │ ├── fetch_request.go │ │ ├── filestore/ │ │ │ ├── cache.go │ │ │ ├── index_list.go │ │ │ ├── index_list_test.go │ │ │ ├── logger.go │ │ │ ├── message_partition.go │ │ │ ├── message_partition_robustness_test.go │ │ │ ├── message_partition_test.go │ │ │ ├── message_store.go │ │ │ └── message_store_test.go │ │ └── store.go │ ├── utils_test.go │ ├── webserver/ │ │ ├── logger.go │ │ ├── web_server.go │ │ └── web_server_test.go │ └── websocket/ │ ├── logger.go │ ├── mocks_auth_gen_test.go │ ├── mocks_router_gen_test.go │ ├── mocks_store_gen_test.go │ ├── mocks_websocket_gen_test.go │ ├── receiver.go │ ├── receiver_test.go │ ├── websocket_connector.go │ └── websocket_connector_test.go ├── test.sh └── testutil/ └── testutil.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ cover.out .goxc.json /.idea *.iml guble guble-cli/guble-cli ================================================ FILE: .travis.yml ================================================ language: go go: - tip sudo: required services: - docker - postgresql before_script: - psql -c 'create database guble;' -U postgres before_install: - go get github.com/wadey/gocovmerge - go get github.com/mattn/goveralls - go get golang.org/x/tools/cmd/cover script: - GO_TEST_DISABLED=true go test -v ./... after_success: - scripts/generate_coverage.sh - goveralls -coverprofile=full_cov.out -service=travis-ci - if [ "$TRAVIS_BRANCH" == "master" ]; then GOOS=linux go build -a --ldflags '-linkmode external -extldflags "-static"' . ; GOOS=linux go build -a --ldflags '-linkmode external -extldflags "-static"' -o ./guble-cli/guble-cli ./guble-cli ; docker build -t smancke/guble . ; docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" ; docker push smancke/guble ; fi env: global: - secure: V9+UswYO6l0EuekA5YBviUdz0OcWfT3QsY1Bgoml8lmWP3/Rdq0fpxGh1hHUWt1pyAl3Aymw5Sc9DU/STmb5k6YjimS649Hu3jZ2AJfjLxh8ZA+vTgFiQc4mN4FqDAFhnPVB/aOSQhGyRlWalxikNy3nhcJrN+uWOpzRqzg0icNOdfTKpSH1cRJO0Ja34f4AEmLuNvGUAyZVpLuZJFL5mE9sJ1G1baqgFf/kTQ67jF+Ezg+1AY+NYaYwd5PUGFgIKf/qVT5Wqtrff26Yxzr/hECEBypAvNmCdLSoV/qyzZvzUTgYZTPmUnDks0uUEup9YzEQZ9XxwIQyHSXZ9D6h2vZxyr0TlZvBtdzWNiLHjBSISF8ZzOthI7NIi/e4YRYlqCF3apZuRo6o2fneHqzonza0OpJQdCKACXgycFe0ZTXk1o7SdT1d1JgeFckmL0kS8H2N4E/DaIAPq8zaC4bOlaYaUYt6vXNwEKK99q0X97gLJFdrBBY7lzKs9bbVa7b2Dhkh67PUt6WhoHUjLSN+9jTn+oda8VEKtXxyaWM6AsCRHgBiy0VaxuHbU2k1mpSCLdBfJGbrDITA4+nyPopv/oky4xHX1FGSMGFw73Ejafu9Xo0cpvIpVcNjeagUugQ5ThPQMSua9hxSZJx6alIUhptDUesiYHJAWUVPQi4N/3A= - secure: SHIH8wBBTWslUnXeIPa3XpPugTX2IgKu3CB0OAbEE9e1nkAop1bbbas5chJgSA276xseBriH7aBSPe25XB4q9JM0YDelC7pK7dmSiLQMiAYvBb8SiGpfTAArBen4hiJiYaJo9hAE51Q4tjZ03vlIvTCYFjJ0rsBoTnbk9W6iVNEQfKzo9KfVBshYcS4BswwBgPSGtck/V3I3oASTmWpPdCGhhDipuOA9UG4hbnyWyeDqi1Mf0Dukggya4Qg+Z2o3WFI5qKGN/L6Qulgse9Rszrlikas5g2iDP11e9eO/tn/2nipIGZd/0xgCcG4tfcoqVn0PzOIOLE33vgqDrUvaaIsmVL/h0nQvC+EhVjgtrNcV/c3gDFH/3GaFX/J2wtT7396CpNCJbje/5fo9pFKS/QXjyqeIRrjq5Rux59RkZNoZIYyXbgM2UW3F8ebHFgaLd4+3Ec67zelxvixJWP1s2iDkZ2C4M7eHSBSvwpM0leebXPDOXeInCPspD5AWkhmo29m7X6J6fT7lwkfbSTyvCAQCKMzuRIsxMaAdxMCco5eVMam8CaAqZoAL/8RbnC+G9BiknyNDxx/W3qLfbnTpXlljKIBapNRYiut0RglrcPjpGAhHwbefXNwjb8AHxzx/GnU3GIHzjkQXCGDGMLJ6cPm/Iik2tVZD/eqgRxGqWNA= - secure: BNjkm6Hb8go2xem0JLsSkFhACUgwxhBhqgsEfcpzJG1+gIC0ZZtvK5ARBusOgEytmT1tsyDbT99FT1MJ3LsucNB4EixLU/8UEoY80r/QD67eK4dKzUIiQdsPmJTUMJnzfTqgyQF2byilu6tHSHWL+MwFVmaQh04R1T1Zo0LyZMFhWjWIGx2lNhHbsLQWjb7KFLLlYx9lg4POf4eTTnrhhdJHTpUOmoty57+jf+Sen+hPOanGGsSajo6GTqN2SMmsLOCykwytsSUA0ZZ/QuEsL+1htm0vpQXqsfUxQ3KAIbyDUVTrSQsAPvULM1ymlEyIeFEeABTCVNUQb8sMpc/5VbKTNd/jEhM6oidZfakLnx3RV6kZCtrHMbkHh6ta8KcxTpt7TcnyGjTnMD5jCVjgAM99j8x7QMfAd+boRr05intzHB8GFv0IDYq9tZ93/umQHyqX8ctN+kNpmy0kSshusd3QPZ+FeZrMgWhfKvYkrjEZ1Pd/wWaqb4Pv0DUfqvlwYKshvCtH7u7TCP63Nbnt2rY+CNgfXWBDfPkxIDoF8UrXIHEZXY2C5JOGfEtS27AjUin46vHFQKr/oaYYMiUXVu25mbTkNNR67Q/6yxD0f7VqVFmWAmT1pWdDd6Gc0uT4fsP8H8Le1PAciPjMGvUFIegQK7W9TnnvDA1w0IsADwE= # DOCKER_PASSWORD ================================================ FILE: Dockerfile ================================================ FROM alpine COPY ./guble ./guble-cli/guble-cli /usr/local/bin/ RUN mkdir -p /var/lib/guble VOLUME ["/var/lib/guble"] ENTRYPOINT ["/usr/local/bin/guble"] EXPOSE 8080 ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Sebastian Mancke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Guble Messaging Server Guble is a simple user-facing messaging and data replication server written in Go. [![Codacy Badge](https://api.codacy.com/project/badge/Grade/f3b9a351201b416db4fe6df8faea363b)](https://www.codacy.com/app/cosminrentea/guble?utm_source=github.com&utm_medium=referral&utm_content=smancke/guble&utm_campaign=badger) [![Release](https://img.shields.io/github/release/smancke/guble.svg)](https://github.com/smancke/guble/releases/latest) [![Docker](https://img.shields.io/docker/pulls/smancke/guble.svg)](https://hub.docker.com/r/smancke/guble/) [![Build Status](https://api.travis-ci.org/smancke/guble.svg?branch=master)](https://travis-ci.org/smancke/guble) [![Go Report Card](https://goreportcard.com/badge/github.com/smancke/guble)](https://goreportcard.com/report/github.com/smancke/guble) [![codebeat badge](https://codebeat.co/badges/7f317892-0a7b-4e31-97f4-a530cf779889)](https://codebeat.co/projects/github-com-smancke-guble) [![Coverage Status](https://coveralls.io/repos/smancke/guble/badge.svg?branch=master&service=github)](https://coveralls.io/github/smancke/guble?branch=master) [![GoDoc](https://godoc.org/github.com/smancke/guble?status.svg)](https://godoc.org/github.com/smancke/guble) [![Awesome-Go](https://camo.githubusercontent.com/13c4e50d88df7178ae1882a203ed57b641674f94/68747470733a2f2f63646e2e7261776769742e636f6d2f73696e647265736f726875732f617765736f6d652f643733303566333864323966656437386661383536353265336136336531353464643865383832392f6d656469612f62616467652e737667)](https://awesome-go.com) # Overview Guble is in an early state (release 0.4). It is already working well and is very useful, but the protocol, API and storage formats may still change (until reaching 0.7). If you intend to use guble, please get in contact with us. The goal of guble is to be a simple and fast message bus for user interaction and replication of data between multiple devices: * Very easy consumption of messages with web and mobile clients * Fast realtime messaging, as well as playback of messages from a persistent commit log * Reliable and scalable over multiple nodes * User-aware semantics to easily support messaging scenarios between people using multiple devices * Batteries included: usable as front-facing server, without the need of a proxy layer * Self-contained: no mandatory dependencies to other services ## Working Features (0.4) * Publishing and subscription of messages to topics and subtopics * Persistent message store with transparent live and offline fetching * WebSocket and REST APIs for message publishing * Commandline client and Go client library * Firebase Cloud Messaging (FCM) adapter: delivery of messages as FCM push notifications * Docker images for server and client * Simple Authentication and Access-Management * Clean shutdown * Improved logging using [logrus](https://github.com/Sirupsen/logrus) and logstash formatter * Health-Check with Endpoint * Collection of Basic Metrics, with Endpoint * Added Postgresql as KV Backend * Load testing with 5000 messages per instance * Support for Apple Push Notification services (a new connector alongside Firebase) * Upgrade, cleanup, abstraction, documentation, and test coverage of the Firebase connector * GET list of subscribers / list of topics per subscriber (userID , deviceID) * Support for SMS-sending using Nexmo (a new connector alongside Firebase) ## Throughput Measured on an old notebook with i5-2520M, dual core and SSD. Message payload was 'Hello Word'. Load driver and server were set up on the same machine, so 50% of the cpu was allocated to the load driver. * End-2-End: Delivery of ~35.000 persistent messages per second * Fetching: Receive of ~70.000 persistent messages per second During the tests, the memory consumption of the server was around ~25 MB. ## Table of Contents - [Roadmap](#roadmap) - [Roadmap Release 0.5](#roadmap-release-05) - [Roadmap Release 0.6](#roadmap-release-06) - [Roadmap Release 0.7](#roadmap-release-07) - [Guble Docker Image](#guble-docker-image) - [Start the Guble Server](#start-the-guble-server) - [Connecting with the Guble Client](#connecting-with-the-guble-client) - [Build and Run](#build-and-run) - [Build and Start the Server](#build-and-start-the-server) - [Configuration](#configuration) - [Run All Tests](#run-all-tests) - [Clients](#clients) - [Protocol Reference](#protocol-reference) - [REST API](#rest-api) - [Headers](#headers) - [WebSocket Protocol](#websocket-protocol) - [Message Format](#message-format) - [Client Commands](#client-commands) - [Server Status Messages](#server-status-messages) - [Topics](#topics) - [Subtopics](#subtopics) # Roadmap This is the current (and fast changing) roadmap and todo list: ## Roadmap Release 0.5 * Replication across multiple servers (in a Guble cluster) * Acknowledgement of message delivery for connectors * Storing the sequence-Id of topics in KV store, if we turn off persistence * Filtering of messages in guble server (e.g. sent by the REST client) according to URL parameters: UserID, DeviceID, Connector name * Updating README to show subscribe/unsubscribe/get/posting, health/metrics ## Roadmap Release 0.6 * Make notification messages optional by client configuration * Correct behaviour of receive command with `maxCount` on subtopics * Cancel of fetch in the message store and multiple concurrent fetch commands for the same topic * Configuration of different persistence strategies for topics * Delivery semantics: user must read on one device / deliver only to one device / notify if not connected, etc. * User-specific persistent subscriptions across all clients of the user * Client: (re-)setup of subscriptions after client reconnect * Message size limit configurable by the client with fetching by URL ## Roadmap Release 0.7 * HTTPS support in the service * Minimal example: chat application * Stable JavaScript client: https://github.com/smancke/guble-js * (TBD) Improved authentication and access-management * (TBD) Add Consul as KV Backend * (TBD) Index-based search of messages using [GoLucene](https://github.com/balzaczyy/golucene) # Guble Docker Image We are providing Docker images of the server and client for your convenience. ## Start the Guble Server There is an automated Docker build for the master at the Docker Hub. To start the server with Docker simply type: ``` docker run -p 8080:8080 smancke/guble ``` To see available configuration options: ``` docker run smancke/guble --help ``` All options can be supplied on the commandline or by a corresponding environment variable with the prefix `GUBLE_`. So to let guble be more verbose, you can either use: ``` docker run smancke/guble --log=info ``` or ``` docker run -e GUBLE_LOG=info smancke/guble ``` The Docker image has a volume mount point at `/var/lib/guble`, so if you want to bind-mount the persistent storage from your host you should use: ``` docker run -p 8080:8080 -v /host/storage/path:/var/lib/guble smancke/guble ``` ## Connecting with the Guble Client The Docker image includes the guble commandline client `guble-cli`. You can execute it within a running guble container and connect to the server: ``` docker run -d --name guble smancke/guble docker exec -it guble /usr/local/bin/guble-cli ``` Visit the [`guble-cli` documentation](https://github.com/smancke/guble/tree/master/guble-cli) for more details. # Build and Run Since Go makes it very easy to build from source, you can compile guble using a single command. A prerequisite is having an installed Go environment and an empty directory: ``` sudo apt-get install golang mkdir guble && cd guble export GOPATH=`pwd` ``` ## Build and Start the Server Build and start guble with the following commands (assuming that directory `/var/lib/guble` is already created with read-write rights for the current user): ``` go get github.com/smancke/guble bin/guble --log=info ``` ### Configuration |CLI Option|Env Variable|Values|Default|Description| |--- |--- |--- |--- |--- | |`--env`|GUBLE_ENV|development | integration | preproduction | production|development|Name of the environment on which the application is running. Used mainly for logging| |`--health-endpoint`|GUBLE_HEALTH_ENDPOINT|resource/path/to/healthendpoint|/admin/healthcheck|The health endpoint to be used by the HTTP server.Can be disabled by setting the value to ""| |`--http`|GUBLE_HTTP_LISTEN|format: [host]:port||The address to for the HTTP server to listen on| |`--kvs`|GUBLE_KVS|memory | file | postgres|file|The storage backend for the key-value store to use| |`--log`|GUBLE_LOG|panic | fatal | error | warn | info | debug|error|The log level in which the process logs| |`--metrics-endpoint`|GUBLE_METRICS_ENDPOINT|resource/path/to/metricsendpoint|/admin/metrics|The metrics endpoint to be used by the HTTP server.Can be disabled by setting the value to ""| |`--ms`|GUBLE_MS|memory | file|file|The message storage backend| |`--profile`|GUBLE_PROFILE|cpu | mem | block||The profiler to be used| |`--storage-path`|GUBLE_STORAGE_PATH|path/to/storage|/var/lib/guble|The path for storing messages and key-value data like subscriptions if defined.The path must exists!| #### APNS |CLI Option|Env Variable|Values|Default|Description| |--- |--- |--- |--- |--- | |`--apns`|GUBLE_APNS|true | false|false|Enable the APNS module in general as well as the connector to the development endpoint| |`--apns-production`|GUBLE_APNS_PRODUCTION|true | false|false|Enables the connector to the apns production endpoint, requires the apns option to be set| |`--apns-cert-file`|GUBLE_APNS_CERT_FILE|path/to/cert/file||The APNS certificate file name, use this as an alternative to the certificate bytes option| |`--apns-cert-bytes`|GUBLE_APNS_CERT_BYTES|cert-bytes-as-hex-string||The APNS certificate bytes, use this as an alternative to the certificate file option| |`--apns-cert-password`|GUBLE_APNS_CERT_PASSWORD|password||The APNS certificate password| |`--apns-app-topic`|GUBLE_APNS_APP_TOPIC|topic||The APNS topic (as used by the mobile application)| |`--apns-prefix`|GUBLE_APNS_PREFIX|prefix|/apns/|The APNS prefix / endpoint| |`--apns-workers`|GUBLE_APNS_WORKERS|number of workers|Number of CPUs|The number of workers handling traffic with APNS (default: number of CPUs)| #### SMS |CLI Option|Env Variable|Values|Default |Description| |--- |--- |--- |--- |--- | |`sms`|GUBLE_SMS|true | false|false |Enable the SMS gateway| |`sms_api_key`|GUBLE_SMS_API_KEY|api key||The Nexmo API Key for Sending sms| |`sms_api_secret`|GUBLE_SMS_API_SECRET|api secret||The Nexmo API Secret for Sending sms| |`sms_topic`|GUBLE_SMS_TOPIC|topic|/sms|The topic for sms route| |`sms_workers`|GUBLE_SMS_WORKERS|number of workers|Number of CPUs|The number of workers handling traffic with Nexmo sms endpoint| #### FCM |CLI Option|Env Variable|Values|Default|Description| |--- |--- |--- |--- |--- | |`--fcm|GUBLE_FCM`|true | false|false|Enable the Google Firebase Cloud Messaging connector| |`--fcm-api-key`|GUBLE_FCM_API_KEY|api key||The Google API Key for Google Firebase Cloud Messaging| |`--fcm-workers`|GUBLE_FCM_WORKERS|number of workers|Number of CPUs|The number of workers handling traffic with Firebase Cloud Messaging| |`--fcm-endpoint`|GUBLE_FCM_ENDPOINT|format: url-schema|https://fcm.googleapis.com/fcm/send|The Google Firebase Cloud Messaging endpoint| |`--fcm-prefix`|GUBLE_FCM_PREFIX|prefix|/fcm/|The FCM prefix / endpoint| #### Postgres |CLI Option|Env Variable|Values|Default|Description| |--- |--- |--- |--- |--- | |`--pg-host`|GUBLE_PG_HOST|hostname|localhost|The PostgreSQL hostname| |`--pg-port`|GUBLE_PG_PORT|port|5432|The PostgreSQL port| |`--pg-user`|GUBLE_PG_USER|user|guble|The PostgreSQL user| |`--pg-password`|GUBLE_PG_PASSWORD|password|guble|The PostgreSQL password| |`--pg-dbname`|GUBLE_PG_DBNAME|database|guble|The PostgreSQL database name| ## Run All Tests ``` go get -t github.com/smancke/guble/... go test github.com/smancke/guble/... ``` # Clients The following clients are available: * __Commandline Client__: https://github.com/smancke/guble/tree/master/guble-cli * __Go client library__: https://github.com/smancke/guble/tree/master/client * __JavaScript library__: (in early stage) https://github.com/smancke/guble-js # Protocol Reference ## REST API Currently there is a minimalistic REST API, just for publishing messages. ``` POST /api/message/ ``` URL parameters: * __userId__: The PublisherUserId * __messageId__: The PublisherMessageId ### Headers You can set fields in the header JSON of the message by providing the corresponding HTTP headers with the prefix `X-Guble-`. Curl example with the resulting message: ``` curl -X POST -H "x-Guble-Key: Value" --data Hello 'http://127.0.0.1:8080/api/message/foo?userId=marvin&messageId=42' ``` Results in: ``` 16,/foo,marvin,VoAdxGO3DBEn8vv8,42,1451236804 {"Key":"Value"} Hello ``` ## WebSocket Protocol The communication with the guble server is done by ordinary WebSockets, using a binary encoding. ### Message Format All payload messages sent from the server to the client are using the following format: ``` ,,,,,\n []\n example 1: /foo/bar,42,user01,phone1,id123,1420110000 {"Content-Type": "text/plain", "Correlation-Id": "7sdks723ksgqn"} Hello World example 2: /foo/bar,42,user01,54sdcj8sd7,id123,1420110000 anyByteData ``` * All text formats are assumed to be UTF-8 encoded. * Message `sequenceId`s are `int64`, and distinct within a topic. The message `sequenceId`s are strictly monotonically increasing depending on the message age, but there is no guarantee for the right order while transmitting. ### Client Commands The client can send the following commands. #### Send Publish a message to a topic: ``` > []\n [
\n].. \n example: > /foo Hello World ``` #### Subscribe/Receive Receive messages from a path (e.g. a topic or subtopic). This command can be used to subscribe for incoming messages on a topic, as well as for replaying the message history. ``` + [[,]] ``` * `path`: the topic to receive the messages from * `startId`: the message id to start the replay ** If no `startId` is given, only future messages will be received (simple subscribe). ** If the `startId` is negative, it is interpreted as relative count of last messages in the history. * `maxCount`: the maximum number of messages to replay __Note__: Currently, the fetching of stored messages does not recognize subtopics. Examples: ``` + /foo # Subscribe to all future messages matching /foo + /foo/bar # Subscribe to all future messages matching /foo/bar + /foo 0 # Receive all message from the topic and subscribe for further incoming messages. + /foo 42 # Receive all message with message ids >= 42 # from the topic and subscribe for further incoming messages. + /foo 0 20 # Receive the first (oldest) 20 messages within the topic and stop. # (If the topic has less messages, it will stop after receiving all existing ones.) + /foo -20 # Receive the last (newest) 20 messages from the topic and then # subscribe for further incoming messages. + /foo -20 20 # Receive the last (newest) 20 messages within the topic and stop. # (If the topic has less messages, it will stop after receiving all existing ones.) ``` #### Unsubscribe/Cancel Cancel further receiving of messages from a path (e.g. a topic or subtopic). ``` - example: - /foo - /foo/bar ``` ### Server Status Messages The server sends status messages to the client. All positive status messages start with `>`. Status messages reporting an error start with `!`. Status messages are in the following format. ``` '#' \n ``` #### Connection Message ``` #ok-connected You are connected to the server.\n {"ApplicationId": "the app id", "UserId": "the user id", "Time": "the server time as unix timestamp "} ``` Example: ``` #connected You are connected to the server. {"ApplicationId": "phone1", "UserId": "user01", "Time": "1420110000"} ``` #### Send Success Notification This notification confirms, that the messaging system has successfully received the message and now starts transmitting it to the subscribers: ``` #send {"sequenceId": "sequence id", "path": "/foo", "publisherMessageId": "publishers message id", "messagePublishingTime": "unix-timestamp"} ``` #### Receive Success Notification Depending on the type of `+` (receive) command, up to three different notification messages will be sent back. Be aware, that a server may send more receive notifications that you would have expected in first place, e.g. when: * Additional messages are stored, while the first fetching is in progress * The server decides to meanwhile stop the online subscription and change to fetching, because your client is too slow to read all incoming messages. 1. When the fetch operation starts: ``` #fetch-start ``` * `path`: the topic path * `count`: the number of messages that will be returned 2. When the fetch operation is done: ``` #fetch-done ``` * `path`: the topic path 3. When the subscription to new messages was taken: ``` #subscribed-to ``` * `path`: the topic path #### Unsubscribe Success Notification An unsubscribe/cancel operation is confirmed by the following notification: ``` #canceled ``` #### Send Error Notification This message indicates, that the message could not be delivered. ``` !error-send {"sequenceId": "sequence id", "path": "/foo", "publisherMessageId": "publishers message id", "messagePublishingTime": "unix-timestamp"} ``` #### Bad Request This notification has the same meaning as the http 400 Bad Request. ``` !error-bad-request unknown command 'sdcsd' ``` #### Internal Server Error This notification has the same meaning as the http 500 Internal Server Error. ``` !error-server-internal this computing node has problems ``` ## Topics Messages can be hierarchically routed by topics, so they are represented by a path, separated by `/`. The server takes care, that a message only gets delivered once, even if it is matched by multiple subscription paths. ### Subtopics The path delimiter gives the semantic of subtopics. With this, a subscription to a parent topic (e.g. `/foo`) also results in receiving all messages of the subtopics (e.g. `/foo/bar`). ================================================ FILE: api/swagger.yaml ================================================ swagger: '2.0' info: version: "0.0.1" title: Guble API schemes: - http paths: /api/subscribers/{topic}: get: produces: - application/json tags: - REST - APNS - FCM description: | Get subscribers registered for a topic parameters: - name: topic in: path type: string required: true description: name of the subscribtion topic responses: 200: description: successful response schema: type: array items: $ref: '#/definitions/Subscriber' 500: description: unknown error /api/message/{topic}: post: consumes: - application/json tags: - REST - SMS - APNS - FCM description: | Send message to a connector parameters: - name: topic in: path type: string required: true description: | Name of the subscribtion topic. 'sms' is a special topic to send a sms message and must not be used neither as APNS nor as FCM topic. - name: message in: body required: true description: a json message in the format expected by the connector schema: type: object - name: userId in: header required: false type: string - name: x-guble in: header required: false type: string description: x-guble- is a generic header prefix - name: filterConnector in: query description: | Specifies a connector which should handle message. As the message is in the connector specific format, the parameter should be treated as mandatory. required: false type: string enum: - apns - fcm - name: filterUserID in: query description: Specifies a subscribed user which should received the notification. required: false type: string - in: query name: filterDeviceToken description: Specifies a device token which should received the notification. required: false type: string responses: 200: description: successful response 400: description: malformed request 500: description: unknown error /apns/{device_token}/{user_id}/{topic}: post: tags: - APNS description: | Create APN subscription parameters: - name: device_token in: path type: string required: true description: device token which mobile device received from APNS - name: user_id in: path type: string required: true description: customer uuid or 'anonymous' - name: topic in: path type: string required: true description: name of the subscribtion topic responses: 200: description: successful response 500: description: unknown error delete: tags: - APNS description: | Delete APN subscription parameters: - name: device_token in: path type: string required: true description: device token which mobile device received from APNS - name: user_id in: path type: string required: true description: customer uuid or 'anonymous' - name: topic in: path type: string required: true description: name of the subscribtion topic responses: 200: description: successful response 404: description: subscription not found 500: description: unknown error /apns/: get: tags: - APNS description: | Return the list of APNS subscriptions parameters: - in: query name: device_token description: device token which mobile device received from APNS required: false type: string - in: query name: user_id description: device token required: false type: string responses: 200: description: list of topics schema: type: array items: { type: string } 400: description: missing filters 500: description: unknown error /apns/substitute/: post: tags: - APNS description: | Substitutes field value of the APNS subscriber object. Provided old value must match the current value stored in the object for operation to be succcessful parameters: - name: body in: body required: true schema: $ref: '#/definitions/SubstitutionRequest' responses: 200: description: successful response schema: $ref: '#/definitions/SubstitutionResponse' 400: description: invalid substitution request schema: $ref: '#/definitions/ErrorResponse' 500: description: unknown error schema: $ref: '#/definitions/ErrorResponse' /fcm/{device_token}/{user_id}/{topic}: post: tags: - FCM description: | Create FCM subscription parameters: - name: device_token in: path type: string required: true description: device token which mobile device received from FCM - name: user_id in: path type: string required: true description: customer uuid or 'anonymous' - name: topic in: path type: string required: true description: name of the subscribtion topic responses: 200: description: successful response 500: description: unknown error delete: tags: - FCM description: | Delete FCM subscription parameters: - name: device_token in: path type: string required: true description: device token which mobile device received from FCM - name: user_id in: path type: string required: true description: customer uuid or 'anonymous' - name: topic in: path type: string required: true description: name of the subscribtion topic responses: 200: description: successful response 404: description: subscription not found 500: description: unknown error /fcm/: get: tags: - FCM description: | Return the list of subscriptions parameters: - in: query name: device_token description: device token which mobile device received from FCM required: false type: string - in: query name: user_id description: device token required: false type: string responses: 200: description: list of topics schema: type: array items: { type: string } 400: description: missing filters 500: description: unknown error /fcm/substitute/: post: tags: - FCM description: | Substitutes field value of the FCMC subscriber object. Provided old value must match the current value stored in the object for operation to be succcessful parameters: - name: body in: body required: true schema: $ref: '#/definitions/SubstitutionRequest' responses: 200: description: successful response schema: $ref: '#/definitions/SubstitutionResponse' 400: description: invalid substitution request schema: $ref: '#/definitions/ErrorResponse' 500: description: unknown error schema: $ref: '#/definitions/ErrorResponse' /admin/healtcheck: get: produces: - application/json tags: - ADMIN description: Application health check responses: 200: description: successful response 500: description: unknown error /admin/metrics: get: produces: - application/json tags: - ADMIN description: Application metrics responses: 200: description: successful response schema: type: object 500: description: unknown error /stream/: get: tags: - WEBSOCKET description: Web socket interface responses: 201: description: Response code is 101 after protocol was switched definitions: ErrorResponse: type: object properties: error: description: error message type: string Subscriber: type: object required: - connector - device_token - user_id properties: connector: description: name of the connector type: string enum: - apns - fcm device_token: description: device token type: string user_id: description: customer uuid or 'anonymous' type: string SubstitutionRequest: type: object required: - field - old_value - new_value properties: field: description: field name type: string enum: - device_token - user_id old_value: description: old value type: string new_value: description: new value type: string SubstitutionResponse: type: object properties: modified: description: number of modified entries type: integer ================================================ FILE: client/client.go ================================================ package client import ( "github.com/smancke/guble/protocol" log "github.com/Sirupsen/logrus" "github.com/gorilla/websocket" "net/http" "sync" "time" ) var logger = log.WithFields(log.Fields{ "module": "client", }) type WSConnection interface { WriteMessage(messageType int, data []byte) error ReadMessage() (messageType int, p []byte, err error) Close() error } func DefaultConnectionFactory(url string, origin string) (WSConnection, error) { logger.WithField("url", url).Info("Connecting to") header := http.Header{"Origin": []string{origin}} conn, _, err := websocket.DefaultDialer.Dial(url, header) if err != nil { return nil, err } logger.WithField("url", url).Info("Connected to") return conn, nil } type WSConnectionFactory func(url string, origin string) (WSConnection, error) type Client interface { Start() error Close() Subscribe(path string) error Unsubscribe(path string) error Send(path string, body string, header string) error SendBytes(path string, body []byte, header string) error WriteRawMessage(message []byte) error Messages() chan *protocol.Message StatusMessages() chan *protocol.NotificationMessage Errors() chan *protocol.NotificationMessage SetWSConnectionFactory(WSConnectionFactory) IsConnected() bool } type client struct { mu sync.RWMutex ws WSConnection messages chan *protocol.Message statusMessages chan *protocol.NotificationMessage errors chan *protocol.NotificationMessage url string origin string shouldStopChan chan bool shouldStopFlag bool autoReconnect bool wSConnectionFactory func(url string, origin string) (WSConnection, error) // flag, to indicate if the client is connected connected bool } // Open is a shortcut for New() and Start() func Open(url, origin string, channelSize int, autoReconnect bool) (Client, error) { c := New(url, origin, channelSize, autoReconnect) c.SetWSConnectionFactory(DefaultConnectionFactory) return c, c.Start() } // New creates a new client, without starting the connection func New(url, origin string, channelSize int, autoReconnect bool) Client { return &client{ messages: make(chan *protocol.Message, channelSize), statusMessages: make(chan *protocol.NotificationMessage, channelSize), errors: make(chan *protocol.NotificationMessage, channelSize), url: url, origin: origin, shouldStopChan: make(chan bool, 1), autoReconnect: autoReconnect, } } func (c *client) SetWSConnectionFactory(connection WSConnectionFactory) { c.wSConnectionFactory = connection } func (c *client) IsConnected() bool { c.mu.RLock() defer c.mu.RUnlock() return c.connected } func (c *client) setIsConnected(connected bool) { c.mu.Lock() defer c.mu.Unlock() c.connected = connected } // Connect and start the read go routine. // If an error occurs on first connect, it will be returned. // Further connection errors will only be logged. func (c *client) Start() error { var err error c.ws, err = c.wSConnectionFactory(c.url, c.origin) c.setIsConnected(err == nil) if c.IsConnected() { go c.readLoop() } else if c.autoReconnect { go c.startWithReconnect() } return err } func (c *client) startWithReconnect() { for { if c.IsConnected() { err := c.readLoop() if err == nil { return } } if c.shouldStop() { return } var err error c.ws, err = c.wSConnectionFactory(c.url, c.origin) if err != nil { c.setIsConnected(false) logger.WithError(err).Error("Error on connect, retry in 50 ms") time.Sleep(time.Millisecond * 50) } else { c.setIsConnected(true) logger.Warn("Reconnected again") } } } func (c *client) readLoop() error { for { _, msg, err := c.ws.ReadMessage() if err != nil { c.setIsConnected(false) if c.shouldStop() { return nil } logger.WithError(err).Error("Error when reading from websocket") c.errors <- clientErrorMessage(err.Error()) return err } logger.WithField("msg", string(msg)).Debug("Raw >") c.handleIncomingMessage(msg) } } func (c *client) shouldStop() bool { if c.shouldStopFlag { return true } select { case <-c.shouldStopChan: c.shouldStopFlag = true return true default: return false } } func (c *client) handleIncomingMessage(msg []byte) { parsed, err := protocol.Decode(msg) if err != nil { logger.WithError(err).Error("Error on parsing of incoming message") c.errors <- clientErrorMessage(err.Error()) return } switch message := parsed.(type) { case *protocol.Message: c.messages <- message case *protocol.NotificationMessage: if message.IsError { select { case c.errors <- message: default: } } else { select { case c.statusMessages <- message: default: } } } } func (c *client) Subscribe(path string) error { cmd := &protocol.Cmd{ Name: protocol.CmdReceive, Arg: path, } err := c.ws.WriteMessage(websocket.BinaryMessage, cmd.Bytes()) return err } func (c *client) Unsubscribe(path string) error { cmd := &protocol.Cmd{ Name: protocol.CmdCancel, Arg: path, } err := c.ws.WriteMessage(websocket.BinaryMessage, cmd.Bytes()) return err } func (c *client) Send(path string, body string, header string) error { return c.SendBytes(path, []byte(body), header) } func (c *client) SendBytes(path string, body []byte, header string) error { cmd := &protocol.Cmd{ Name: protocol.CmdSend, Arg: path, Body: body, HeaderJSON: header, } return c.WriteRawMessage(cmd.Bytes()) } func (c *client) WriteRawMessage(message []byte) error { return c.ws.WriteMessage(websocket.BinaryMessage, message) } func (c *client) Messages() chan *protocol.Message { return c.messages } func (c *client) StatusMessages() chan *protocol.NotificationMessage { return c.statusMessages } func (c *client) Errors() chan *protocol.NotificationMessage { return c.errors } func (c *client) Close() { c.shouldStopChan <- true c.ws.Close() } func clientErrorMessage(message string) *protocol.NotificationMessage { return &protocol.NotificationMessage{ IsError: true, Name: "clientError", Arg: message, } } ================================================ FILE: client/client_test.go ================================================ package client import ( "github.com/smancke/guble/testutil" "fmt" "strings" "testing" "time" "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" ) var aNormalMessage = `/foo/bar,42,user01,phone01,{},1420110000,0 Hello World` var aSendNotification = "#send" var anErrorNotification = "!error-send" func MockConnectionFactory(connectionMock *MockWSConnection) func(string, string) (WSConnection, error) { return func(url string, origin string) (WSConnection, error) { return connectionMock, nil } } func TestConnectErrorWithoutReconnection(t *testing.T) { a := assert.New(t) // given a client c := New("url", "origin", 1, false) // which raises an error on connect callCounter := 0 c.SetWSConnectionFactory(func(url string, origin string) (WSConnection, error) { a.Equal("url", url) a.Equal("origin", origin) callCounter++ return nil, fmt.Errorf("emulate connection error") }) // when we start err := c.Start() // then a.Error(err) a.Equal(1, callCounter) } func TestConnectErrorWithoutReconnectionUsingOpen(t *testing.T) { a := assert.New(t) c, err := Open("url", "origin", 1, false) // which raises an error on connect callCounter := 0 c.SetWSConnectionFactory(func(url string, origin string) (WSConnection, error) { a.Equal("url", url) a.Equal("origin", origin) callCounter++ return nil, fmt.Errorf("emulate connection error") }) a.Error(err) } func TestConnectErrorWithReconnection(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) // given a client c := New("url", "origin", 1, true) // which raises an error twice and then allows to connect callCounter := 0 connMock := NewMockWSConnection(ctrl) connMock.EXPECT().ReadMessage().Do(func() { time.Sleep(time.Second) }) c.SetWSConnectionFactory(func(url string, origin string) (WSConnection, error) { a.Equal("url", url) a.Equal("origin", origin) if callCounter <= 2 { callCounter++ return nil, fmt.Errorf("emulate connection error") } return connMock, nil }) // when we start err := c.Start() // then we get an error, first a.Error(err) a.False(c.IsConnected()) // when we wait for two iterations and 10ms buffer time to connect time.Sleep(time.Millisecond * 110) // then we got connected a.True(c.IsConnected()) a.Equal(3, callCounter) } func TestStopableClient(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) // given a client c := New("url", "origin", 1, true) // with a closeable connection connMock := NewMockWSConnection(ctrl) close := make(chan bool, 1) connMock.EXPECT().ReadMessage(). Do(func() { <-close }). Return(0, []byte{}, fmt.Errorf("expected close error")) connMock.EXPECT().Close().Do(func() { close <- true }) c.SetWSConnectionFactory(MockConnectionFactory(connMock)) // when we start err := c.Start() // than we are connected a.NoError(err) a.True(c.IsConnected()) // when we clode c.Close() time.Sleep(time.Millisecond * 1) // than the client returns a.False(c.IsConnected()) } func TestReceiveAMessage(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) // given a client c := New("url", "origin", 10, false) // with a closeable connection connMock := NewMockWSConnection(ctrl) close := make(chan bool, 1) // normal message call1 := connMock.EXPECT().ReadMessage(). Return(4, []byte(aNormalMessage), nil) call2 := connMock.EXPECT().ReadMessage(). Return(4, []byte(aSendNotification), nil) call3 := connMock.EXPECT().ReadMessage(). Return(4, []byte("---"), nil) call4 := connMock.EXPECT().ReadMessage(). Return(4, []byte(anErrorNotification), nil) call5 := connMock.EXPECT().ReadMessage(). Do(func() { <-close }). Return(0, []byte{}, fmt.Errorf("expected close error")). AnyTimes() call5.After(call4) call4.After(call3) call3.After(call2) call2.After(call1) c.SetWSConnectionFactory(MockConnectionFactory(connMock)) connMock.EXPECT().Close().Do(func() { close <- true }) // when we start err := c.Start() a.NoError(err) a.True(c.IsConnected()) // than we receive the expected message select { case m := <-c.Messages(): a.Equal(aNormalMessage, string(m.Bytes())) case <-time.After(time.Millisecond * 10): a.Fail("timeout while waiting for message") } // and we receive the notification select { case m := <-c.StatusMessages(): a.Equal(aSendNotification, string(m.Bytes())) case <-time.After(time.Millisecond * 10): a.Fail("timeout while waiting for message") } // parse error select { case m := <-c.Errors(): a.True(strings.HasPrefix(string(m.Bytes()), "!clientError ")) case <-time.After(time.Millisecond * 10): a.Fail("timeout while waiting for message") } // and we receive the error notification select { case m := <-c.Errors(): a.Equal(anErrorNotification, string(m.Bytes())) case <-time.After(time.Millisecond * 10): a.Fail("timeout while waiting for message") } c.Close() } func TestSendAMessage(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() // a := assert.New(t) // given a client c := New("url", "origin", 1, true) // when expects a message connMock := NewMockWSConnection(ctrl) connMock.EXPECT().WriteMessage(websocket.BinaryMessage, []byte("> /foo\n{}\nTest")) connMock.EXPECT(). ReadMessage(). Return(websocket.BinaryMessage, []byte(aNormalMessage), nil). Do(func() { time.Sleep(time.Millisecond * 50) }). AnyTimes() c.SetWSConnectionFactory(MockConnectionFactory(connMock)) c.Start() // then the expectation is meet by sending it c.Send("/foo", "Test", "{}") // stop client after 200ms time.AfterFunc(time.Millisecond*200, func() { c.Close() }) } func TestSendSubscribeMessage(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() // given a client c := New("url", "origin", 1, true) // when expects a message connMock := NewMockWSConnection(ctrl) connMock.EXPECT().WriteMessage(websocket.BinaryMessage, []byte("+ /foo")) connMock.EXPECT(). ReadMessage(). Return(websocket.BinaryMessage, []byte(aNormalMessage), nil). Do(func() { time.Sleep(time.Millisecond * 50) }). AnyTimes() c.SetWSConnectionFactory(MockConnectionFactory(connMock)) c.Start() c.Subscribe("/foo") // stop client after 200ms time.AfterFunc(time.Millisecond*200, func() { c.Close() }) } func TestSendUnSubscribeMessage(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() // given a client c := New("url", "origin", 1, true) // when expects a message connMock := NewMockWSConnection(ctrl) connMock.EXPECT().WriteMessage(websocket.BinaryMessage, []byte("- /foo")) connMock.EXPECT(). ReadMessage(). Return(websocket.BinaryMessage, []byte(aNormalMessage), nil). Do(func() { time.Sleep(time.Millisecond * 50) }). AnyTimes() c.SetWSConnectionFactory(MockConnectionFactory(connMock)) c.Start() c.Unsubscribe("/foo") // stop client after 200ms time.AfterFunc(time.Millisecond*200, func() { c.Close() }) } ================================================ FILE: client/mocks_client_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/client (interfaces: WSConnection,Client) package client import ( "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" ) // Mock of WSConnection interface type MockWSConnection struct { ctrl *gomock.Controller recorder *_MockWSConnectionRecorder } // Recorder for MockWSConnection (not exported) type _MockWSConnectionRecorder struct { mock *MockWSConnection } func NewMockWSConnection(ctrl *gomock.Controller) *MockWSConnection { mock := &MockWSConnection{ctrl: ctrl} mock.recorder = &_MockWSConnectionRecorder{mock} return mock } func (_m *MockWSConnection) EXPECT() *_MockWSConnectionRecorder { return _m.recorder } func (_m *MockWSConnection) Close() error { ret := _m.ctrl.Call(_m, "Close") ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockWSConnectionRecorder) Close() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Close") } func (_m *MockWSConnection) ReadMessage() (int, []byte, error) { ret := _m.ctrl.Call(_m, "ReadMessage") ret0, _ := ret[0].(int) ret1, _ := ret[1].([]byte) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } func (_mr *_MockWSConnectionRecorder) ReadMessage() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "ReadMessage") } func (_m *MockWSConnection) WriteMessage(_param0 int, _param1 []byte) error { ret := _m.ctrl.Call(_m, "WriteMessage", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockWSConnectionRecorder) WriteMessage(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "WriteMessage", arg0, arg1) } // Mock of Client interface type MockClient struct { ctrl *gomock.Controller recorder *_MockClientRecorder } // Recorder for MockClient (not exported) type _MockClientRecorder struct { mock *MockClient } func NewMockClient(ctrl *gomock.Controller) *MockClient { mock := &MockClient{ctrl: ctrl} mock.recorder = &_MockClientRecorder{mock} return mock } func (_m *MockClient) EXPECT() *_MockClientRecorder { return _m.recorder } func (_m *MockClient) Close() { _m.ctrl.Call(_m, "Close") } func (_mr *_MockClientRecorder) Close() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Close") } func (_m *MockClient) Errors() chan *protocol.NotificationMessage { ret := _m.ctrl.Call(_m, "Errors") ret0, _ := ret[0].(chan *protocol.NotificationMessage) return ret0 } func (_mr *_MockClientRecorder) Errors() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Errors") } func (_m *MockClient) IsConnected() bool { ret := _m.ctrl.Call(_m, "IsConnected") ret0, _ := ret[0].(bool) return ret0 } func (_mr *_MockClientRecorder) IsConnected() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "IsConnected") } func (_m *MockClient) Messages() chan *protocol.Message { ret := _m.ctrl.Call(_m, "Messages") ret0, _ := ret[0].(chan *protocol.Message) return ret0 } func (_mr *_MockClientRecorder) Messages() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Messages") } func (_m *MockClient) Send(_param0 string, _param1 string, _param2 string) error { ret := _m.ctrl.Call(_m, "Send", _param0, _param1, _param2) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockClientRecorder) Send(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Send", arg0, arg1, arg2) } func (_m *MockClient) SendBytes(_param0 string, _param1 []byte, _param2 string) error { ret := _m.ctrl.Call(_m, "SendBytes", _param0, _param1, _param2) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockClientRecorder) SendBytes(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "SendBytes", arg0, arg1, arg2) } func (_m *MockClient) SetWSConnectionFactory(_param0 WSConnectionFactory) { _m.ctrl.Call(_m, "SetWSConnectionFactory", _param0) } func (_mr *_MockClientRecorder) SetWSConnectionFactory(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "SetWSConnectionFactory", arg0) } func (_m *MockClient) Start() error { ret := _m.ctrl.Call(_m, "Start") ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockClientRecorder) Start() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Start") } func (_m *MockClient) StatusMessages() chan *protocol.NotificationMessage { ret := _m.ctrl.Call(_m, "StatusMessages") ret0, _ := ret[0].(chan *protocol.NotificationMessage) return ret0 } func (_mr *_MockClientRecorder) StatusMessages() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "StatusMessages") } func (_m *MockClient) Subscribe(_param0 string) error { ret := _m.ctrl.Call(_m, "Subscribe", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockClientRecorder) Subscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Subscribe", arg0) } func (_m *MockClient) Unsubscribe(_param0 string) error { ret := _m.ctrl.Call(_m, "Unsubscribe", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockClientRecorder) Unsubscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Unsubscribe", arg0) } func (_m *MockClient) WriteRawMessage(_param0 []byte) error { ret := _m.ctrl.Call(_m, "WriteRawMessage", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockClientRecorder) WriteRawMessage(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "WriteRawMessage", arg0) } ================================================ FILE: guble-cli/README.md ================================================ # The guble command line client This is the command line client for the guble messaging server. It is intended for demonstration and debugging use. [![Build Status](https://api.travis-ci.org/smancke/guble.svg)](https://travis-ci.org/smancke/guble) ## Starting the client with docker The guble docker image has the command line client included. You can execute it within a running golang container and connect to the server. ``` docker run -d --name guble smancke/guble docker exec -it guble /go/bin/guble-cli ``` ## Building from source ``` go get github.com/smancke/guble/guble-cli bin/guble-cli ``` ## Start options ``` usage: guble-cli [--exit] [--verbose] [--url URL] [--user USER] [--log-info] [--log-debug] [COMMANDS [COMMANDS ...]] positional arguments: commands options: --exit, -x Exit after sending the commands --verbose, -v Display verbose server communication --url URL The websocket url to connect (ws://localhost:8080/stream/) --user USER The user name to connect with (guble-cli) --log-info Log on INFO level (false) --log-debug Log on DEBUG level (false) ``` ## Commands in the client In the running client, you can use the commands from the websocket api, e.g: ``` ? # prints some usage info + /foo/bar # subscribe to the topic /foo/bar + /foo 0 # read from message 0 and subscribe to the topic /foo + /foo 0 5 # read messages 0-5 from /foo + /foo -5 # read the last 5 messages and subscribe to the topic /foo - /foo # cancel the subscription for /foo > /foo # send a message to /foo > /foo/bar 42 # send a message to /foo/bar with publisherid 42 ``` ================================================ FILE: guble-cli/main.go ================================================ package main import ( "bufio" "fmt" "os" "os/signal" "strings" "syscall" log "github.com/Sirupsen/logrus" "github.com/smancke/guble/client" "github.com/smancke/guble/protocol" "gopkg.in/alecthomas/kingpin.v2" ) var ( exit = kingpin.Flag("exit", "Exit after sending the commands").Short('x').Bool() commands = kingpin.Arg("commands", "The commands to send after startup").Strings() verbose = kingpin.Flag("verbose", "Display verbose server communication").Short('v').Bool() url = kingpin.Flag("url", "The websocket url to connect to").Default("ws://localhost:8080/stream/").String() user = kingpin.Flag("user", "The user name to connect with (guble-cli)").Short('u').Default("guble-cli").String() logLevel = kingpin.Flag("log", "Log level"). Short('l'). Default(log.ErrorLevel.String()). Envar("GUBLE_LOG"). Enum(logLevels()...) logger = log.WithField("app", "guble-cli") ) func logLevels() (levels []string) { for _, level := range log.AllLevels { levels = append(levels, level.String()) } return } // This is a minimal commandline client to connect through a websocket func main() { kingpin.Parse() // set log level level, err := log.ParseLevel(*logLevel) if err != nil { logger.WithField("error", err).Fatal("Invalid log level") } log.SetLevel(level) origin := "http://localhost/" url := fmt.Sprintf("%v/user/%v", removeTrailingSlash(*url), *user) client, err := client.Open(url, origin, 100, true) if err != nil { log.Fatal(err) } go writeLoop(client) go readLoop(client) for _, cmd := range *commands { client.WriteRawMessage([]byte(cmd)) } if *exit { return } waitForTermination(func() {}) } func readLoop(client client.Client) { for { select { case incomingMessage := <-client.Messages(): if *verbose { fmt.Println(string(incomingMessage.Bytes())) } else { fmt.Printf("%v: %v\n", incomingMessage.UserID, incomingMessage.BodyAsString()) } case e := <-client.Errors(): fmt.Println("ERROR: " + string(e.Bytes())) case status := <-client.StatusMessages(): fmt.Println(string(status.Bytes())) fmt.Println() } } } func writeLoop(client client.Client) { shouldStop := false for !shouldStop { func() { defer protocol.PanicLogger() reader := bufio.NewReader(os.Stdin) text, err := reader.ReadString('\n') if err != nil { return } if strings.TrimSpace(text) == "" { return } if strings.TrimSpace(text) == "?" || strings.TrimSpace(text) == "help" { printHelp() return } if strings.HasPrefix(text, ">") { fmt.Print("header: ") header, err := reader.ReadString('\n') if err != nil { return } text += header fmt.Print("body: ") body, err := reader.ReadString('\n') if err != nil { return } text += strings.TrimSpace(body) } if *verbose { log.Printf("Sending: %v\n", text) } if err := client.WriteRawMessage([]byte(text)); err != nil { shouldStop = true logger.WithError(err).Error("Error on Writing message") } }() } } func waitForTermination(callback func()) { sigc := make(chan os.Signal) signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM) log.Printf("%q", <-sigc) callback() os.Exit(0) } func printHelp() { fmt.Println(` ## Commands ? # print this info + /foo/bar # subscribe to the topic /foo/bar + /foo 0 # read from message 0 and subscribe to the topic /foo + /foo 0 5 # read messages 0-5 from /foo + /foo -5 # read the last 5 messages and subscribe to the topic /foo - /foo # cancel the subscription for /foo > /foo # send a message to /foo > /foo/bar 42 # send a message to /foo/bar with publisherid 42 `) } func removeTrailingSlash(path string) string { if len(path) > 1 && path[len(path)-1] == '/' { return path[:len(path)-1] } return path } ================================================ FILE: guble-cli/main_test.go ================================================ package main import ( "fmt" "github.com/stretchr/testify/assert" "io/ioutil" "os" "testing" ) func Test_PrintHelp(t *testing.T) { expectedHelpMessage := ` ## Commands ? # print this info + /foo/bar # subscribe to the topic /foo/bar + /foo 0 # read from message 0 and subscribe to the topic /foo + /foo 0 5 # read messages 0-5 from /foo + /foo -5 # read the last 5 messages and subscribe to the topic /foo - /foo # cancel the subscription for /foo > /foo # send a message to /foo > /foo/bar 42 # send a message to /foo/bar with publisherid 42 ` + "\n" rescueStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w printHelp() w.Close() out, _ := ioutil.ReadAll(r) os.Stdout = rescueStdout resultMessage := fmt.Sprintf("%s", out) assert.Equal(t, expectedHelpMessage, resultMessage) } func Test_removeTrailingSlash(t *testing.T) { cases := []struct { expected, path string }{ {"/foo/user/marvin", "/foo/user/marvin"}, {"/foo/user/marvin", "/foo/user/marvin/"}, {"/", "/"}, } for i, c := range cases { assert.Equal(t, c.expected, removeTrailingSlash(c.path), fmt.Sprintf("Failed at case no=%d", i)) } } ================================================ FILE: logformatter/logstash_formatter.go ================================================ package logformatter import ( "encoding/json" "fmt" "github.com/Sirupsen/logrus" "os" "time" ) const ( defaultServiceName = "guble" defaultLogType = "application" defaultApplicationType = "service" ) // LogstashFormatter generates json in logstash format. // Logstash site: http://logstash.net/ type LogstashFormatter struct { //Type of the fields Type string // if not empty use for logstash type field. //Env is the environment on which the application is running Env string //ServiceName will be by default guble ServiceName string //ApplicationType will be by default "service". Other values could be "service", "system", "appserver", "webserver" ApplicationType string //LogType will be by default application. Other possible values "access", "error", "application", "system" LogType string //TimestampFormat sets the format used for timestamps. TimestampFormat string } // Format the logrus entry to a byte slice, or return an error. func (f *LogstashFormatter) Format(entry *logrus.Entry) ([]byte, error) { fields := make(logrus.Fields) for k, v := range entry.Data { switch v := v.(type) { case error: // Otherwise errors are ignored by `encoding/json` // https://github.com/Sirupsen/logrus/issues/137 // https://github.com/sirupsen/logrus/issues/377 fields[k] = v.Error() default: fields[k] = v } } if f.Env != "" { fields["environment"] = f.Env } timeStampFormat := f.TimestampFormat if timeStampFormat == "" { timeStampFormat = time.RFC3339 } fields["@timestamp"] = entry.Time.Format(timeStampFormat) if f.ServiceName != "" { fields["service"] = f.ServiceName } else { fields["service"] = defaultServiceName } if f.ApplicationType != "" { fields["application_type"] = f.ServiceName } else { fields["application_type"] = defaultApplicationType } if f.LogType != "" { fields["log_type"] = f.LogType } else { fields["log_type"] = defaultLogType } // set level field, prefixing fields clashes if v, ok := entry.Data["loglevel"]; ok { fields["fields.loglevel"] = v } fields["loglevel"] = entry.Level.String() //set host field, prefixing fields clashes if v, ok := entry.Data["host"]; ok { fields["fields.host"] = v } if hostname, err := os.Hostname(); err == nil { fields["host"] = hostname } // set type field if f.Type != "" { if v, ok := entry.Data["type"]; ok { fields["fields.type"] = v } fields["type"] = f.Type } // set message field, prefixing fields clashes if v, ok := entry.Data["msg"]; ok { fields["fields.msg"] = v } fields["msg"] = entry.Message serialized, err := json.Marshal(fields) if err != nil { return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) } return append(serialized, '\n'), nil } ================================================ FILE: logformatter/logstash_formatter_test.go ================================================ package logformatter import ( "bytes" "encoding/json" "testing" "github.com/Sirupsen/logrus" "github.com/stretchr/testify/assert" ) func TestLogstashFormatter_Format(t *testing.T) { a := assert.New(t) lf := LogstashFormatter{Type: "abc", ServiceName: "guble", Env: "prod"} fields := logrus.Fields{ "msg": "def", "level": "ijk", "type": "lmn", "one": 1, "pi": 3.14, "bool": true, } entry := logrus.WithFields(fields) entry.Message = "msg" entry.Level = logrus.InfoLevel b, _ := lf.Format(entry) var data map[string]interface{} dec := json.NewDecoder(bytes.NewReader(b)) dec.UseNumber() dec.Decode(&data) // base fields a.Equal("application", data["log_type"]) a.Equal("service", data["application_type"]) a.Equal("guble", data["service"]) a.Equal("prod", data["environment"]) a.NotEmpty(data["@timestamp"]) a.NotEmpty(data["host"]) a.Equal("abc", data["type"]) a.Equal("msg", data["msg"]) a.Equal("info", data["loglevel"]) // substituted fields a.Equal("def", data["fields.msg"]) a.Equal("lmn", data["fields.type"]) // formats a.Equal(json.Number("1"), data["one"]) a.Equal(json.Number("3.14"), data["pi"]) a.Equal(true, data["bool"]) } ================================================ FILE: main.go ================================================ package main import ( "github.com/smancke/guble/server" ) func main() { server.Main() } ================================================ FILE: protocol/cmd.go ================================================ package protocol import ( "bytes" "fmt" "strings" ) // Valid command names const ( CmdSend = ">" CmdReceive = "+" CmdCancel = "-" ) // Cmd is a representation of a command, which the client sends to the server type Cmd struct { // The name of the command Name string // The argument line, following the commandName Arg string // The header line, if the command has one HeaderJSON string // The command payload, if the command has such Body []byte } // ParseCmd parses a slice of bytes and return a *Cmd func ParseCmd(message []byte) (*Cmd, error) { msg := &Cmd{} if len(message) == 0 { return nil, fmt.Errorf("empty command") } parts := strings.SplitN(string(message), "\n", 3) firstLine := strings.SplitN(parts[0], " ", 2) msg.Name = firstLine[0] if len(firstLine) > 1 { msg.Arg = firstLine[1] } if len(parts) > 1 { msg.HeaderJSON = parts[1] } if len(parts) > 2 { msg.Body = []byte(parts[2]) } return msg, nil } // Bytes serializes the the command into a byte slice func (cmd *Cmd) Bytes() []byte { buff := &bytes.Buffer{} buff.WriteString(cmd.Name) buff.WriteString(" ") buff.WriteString(cmd.Arg) if len(cmd.HeaderJSON) > 0 || len(cmd.Body) > 0 { buff.WriteString("\n") } if len(cmd.HeaderJSON) > 0 { buff.WriteString(cmd.HeaderJSON) } if len(cmd.Body) > 0 { buff.WriteString("\n") buff.Write(cmd.Body) } return buff.Bytes() } ================================================ FILE: protocol/cmd_test.go ================================================ package protocol import ( assert "github.com/stretchr/testify/assert" "testing" ) var aSendCommand = `> /foo {"meta": "data"} Hello World` var aSubscribeCommand = "+ /foo/bar" func TestParsingASendCommand(t *testing.T) { assert := assert.New(t) cmd, err := ParseCmd([]byte(aSendCommand)) assert.NoError(err) assert.Equal(CmdSend, cmd.Name) assert.Equal("/foo", cmd.Arg) assert.Equal(`{"meta": "data"}`, cmd.HeaderJSON) assert.Equal("Hello World", string(cmd.Body)) } func TestSerializeASendCommand(t *testing.T) { cmd := &Cmd{ Name: CmdSend, Arg: "/foo", HeaderJSON: `{"meta": "data"}`, Body: []byte("Hello World"), } assert.Equal(t, aSendCommand, string(cmd.Bytes())) } func Test_Cmd_EmptyCommand_Error(t *testing.T) { assert := assert.New(t) _, err := ParseCmd([]byte{}) assert.Error(err) } func TestParsingASubscribeCommand(t *testing.T) { assert := assert.New(t) cmd, err := ParseCmd([]byte(aSubscribeCommand)) assert.NoError(err) assert.Equal(CmdReceive, cmd.Name) assert.Equal("/foo/bar", cmd.Arg) assert.Equal("", cmd.HeaderJSON) assert.Nil(cmd.Body) } func TestSerializeASubscribeCommand(t *testing.T) { cmd := &Cmd{ Name: CmdReceive, Arg: "/foo/bar", } assert.Equal(t, aSubscribeCommand, string(cmd.Bytes())) } ================================================ FILE: protocol/log.go ================================================ package protocol import ( "bytes" "fmt" log "github.com/Sirupsen/logrus" "runtime" "strings" ) func PanicLogger() { if r := recover(); r != nil { log.Printf("PANIC (%v): %v", identifyLogOrigin(), r) log.Printf(getStackTraceMessage(fmt.Sprintf("%v", r))) } } func identifyLogOrigin() string { var name, file string var line int var pc [16]uintptr n := runtime.Callers(3, pc[:]) for _, pc := range pc[:n] { fn := runtime.FuncForPC(pc) if fn == nil { continue } file, line = fn.FileLine(pc) name = fn.Name() if !strings.HasPrefix(name, "runtime.") { break } } switch { case name != "": return fmt.Sprintf("%v:%v", name, line) case file != "": return fmt.Sprintf("%v:%v", file, line) } return fmt.Sprintf("pc:%x", pc) } func getStackTraceMessage(msg string) string { var name, file string var line int var pc [16]uintptr n := runtime.Callers(3, pc[:]) buff := &bytes.Buffer{} buff.WriteString(msg) buff.WriteString("\n") for _, pc := range pc[:n] { fn := runtime.FuncForPC(pc) if fn == nil { continue } file, line = fn.FileLine(pc) name = fn.Name() switch { case name != "": buff.WriteString(fmt.Sprintf("! %v:%v\n", name, line)) case file != "": buff.WriteString(fmt.Sprintf("! %v:%v\n", file, line)) } } return string(buff.Bytes()) } ================================================ FILE: protocol/log_test.go ================================================ package protocol import ( "bytes" log "github.com/Sirupsen/logrus" "github.com/stretchr/testify/assert" "os" "testing" ) func Test_log_functions_panic_logger(t *testing.T) { a := assert.New(t) w := bytes.NewBuffer([]byte{}) log.SetOutput(w) defer log.SetOutput(os.Stderr) raisePanic() a.Contains(w.String(), "PANIC") a.Contains(w.String(), "raisePanic") a.Contains(w.String(), "Don't panic!") } func raisePanic() { defer PanicLogger() panic("Don't panic!") } ================================================ FILE: protocol/message.go ================================================ package protocol import ( "bytes" "encoding/json" "fmt" "strconv" "strings" log "github.com/Sirupsen/logrus" ) // Message is a struct that represents a message in the guble protocol, as the server sends it to the client. type Message struct { // The sequenceId of the message, which is given by the // server an is strictly monotonically increasing at least within a root topic. ID uint64 // The topic path Path Path // The user id of the message sender UserID string // The id of the sending application ApplicationID string // Filters applied to this message. The message will be sent only to the // routes that match the filters Filters map[string]string // The time of publishing, as Unix Timestamp date Time int64 // The header line of the message (optional). If set, then it has to be a valid JSON object structure. HeaderJSON string // The message payload Body []byte // Used in cluster mode to identify a guble node NodeID uint8 } type MessageDeliveryCallback func(*Message) // Metadata returns the first line of a serialized message, without the newline func (msg *Message) Metadata() string { buff := &bytes.Buffer{} msg.writeMetadata(buff) return string(buff.Bytes()) } func (msg *Message) String() string { return fmt.Sprintf("%d", msg.ID) } func (msg *Message) BodyAsString() string { return string(msg.Body) } // Bytes serializes the message into a byte slice func (msg *Message) Bytes() []byte { buff := &bytes.Buffer{} msg.writeMetadata(buff) if len(msg.HeaderJSON) > 0 || len(msg.Body) > 0 { buff.WriteString("\n") } if len(msg.HeaderJSON) > 0 { buff.WriteString(msg.HeaderJSON) } if len(msg.Body) > 0 { buff.WriteString("\n") buff.Write(msg.Body) } return buff.Bytes() } func (msg *Message) writeMetadata(buff *bytes.Buffer) { buff.WriteString(string(msg.Path)) buff.WriteString(",") buff.WriteString(strconv.FormatUint(msg.ID, 10)) buff.WriteString(",") buff.WriteString(msg.UserID) buff.WriteString(",") buff.WriteString(msg.ApplicationID) buff.WriteString(",") buff.Write(msg.encodeFilters()) buff.WriteString(",") buff.WriteString(strconv.FormatInt(msg.Time, 10)) buff.WriteString(",") buff.WriteString(strconv.FormatUint(uint64(msg.NodeID), 10)) } func (msg *Message) encodeFilters() []byte { if msg.Filters == nil { return []byte{} } data, err := json.Marshal(msg.Filters) if err != nil { log.WithError(err).WithField("filters", msg.Filters).Error("Error encoding filters") return []byte{} } return data } func (msg *Message) decodeFilters(data []byte) { if len(data) == 0 { return } msg.Filters = make(map[string]string) err := json.Unmarshal(data, &msg.Filters) if err != nil { log.WithError(err).WithField("data", string(data)).Error("Error decoding filters") } } func (msg *Message) SetFilter(key, value string) { if msg.Filters == nil { msg.Filters = make(map[string]string, 1) } msg.Filters[key] = value } // Valid constants for the NotificationMessage.Name const ( SUCCESS_CONNECTED = "connected" SUCCESS_SEND = "send" SUCCESS_FETCH_START = "fetch-start" SUCCESS_FETCH_END = "fetch-end" SUCCESS_SUBSCRIBED_TO = "subscribed-to" SUCCESS_CANCELED = "canceled" ERROR_SUBSCRIBED_TO = "error-subscribed-to" ERROR_BAD_REQUEST = "error-bad-request" ERROR_INTERNAL_SERVER = "error-server-internal" ) // NotificationMessage is a representation of a status messages or error message, sent from the server type NotificationMessage struct { // The name of the message Name string // The argument line, following the messageName Arg string // The optional json data supplied with the message Json string // Flag which indicates, if the notification is an error IsError bool } // Bytes serializes the notification message into a byte slice func (msg *NotificationMessage) Bytes() []byte { buff := &bytes.Buffer{} if msg.IsError { buff.WriteString("!") } else { buff.WriteString("#") } buff.WriteString(msg.Name) if len(msg.Arg) > 0 { buff.WriteString(" ") buff.WriteString(msg.Arg) } if len(msg.Json) > 0 { buff.WriteString("\n") buff.WriteString(msg.Json) } return buff.Bytes() } // Decode decodes a message, sent from the server to the client. // The decoded messages can have one of the types: *Message or *NotificationMessage func Decode(message []byte) (interface{}, error) { if len(message) >= 1 && (message[0] == '#' || message[0] == '!') { return parseNotificationMessage(message) } return ParseMessage(message) } func ParseMessage(message []byte) (*Message, error) { parts := strings.SplitN(string(message), "\n", 3) if len(message) == 0 { return nil, fmt.Errorf("empty message") } meta := strings.Split(parts[0], ",") if len(meta) != 7 { return nil, fmt.Errorf("message metadata has to have 7 fields, but was %v", parts[0]) } if len(meta[0]) == 0 || meta[0][0] != '/' { return nil, fmt.Errorf("message has invalid topic, got %v", meta[0]) } id, err := strconv.ParseUint(meta[1], 10, 0) if err != nil { return nil, fmt.Errorf("message metadata to have an integer (message-id) as second field, but was %v", meta[1]) } publishingTime, err := strconv.ParseInt(meta[5], 10, 64) if err != nil { return nil, fmt.Errorf("message metadata to have an integer (publishing time) as sixth field, but was %v", meta[5]) } nodeID, err := strconv.ParseUint(meta[6], 10, 8) if err != nil { return nil, fmt.Errorf("message metadata to have an integer (nodeID) as seventh field, but was %v", meta[6]) } msg := &Message{ ID: id, Path: Path(meta[0]), UserID: meta[2], ApplicationID: meta[3], Time: publishingTime, NodeID: uint8(nodeID), } msg.decodeFilters([]byte(meta[4])) if len(parts) >= 2 { msg.HeaderJSON = parts[1] } if len(parts) == 3 { msg.Body = []byte(parts[2]) } return msg, nil } func parseNotificationMessage(message []byte) (*NotificationMessage, error) { msg := &NotificationMessage{} if len(message) < 2 || (message[0] != '#' && message[0] != '!') { return nil, fmt.Errorf("message has to start with '#' or '!' and a name, but got '%v'", message) } msg.IsError = message[0] == '!' parts := strings.SplitN(string(message)[1:], "\n", 2) firstLine := strings.SplitN(parts[0], " ", 2) msg.Name = firstLine[0] if len(firstLine) > 1 { msg.Arg = firstLine[1] } if len(parts) > 1 { msg.Json = parts[1] } return msg, nil } ================================================ FILE: protocol/message_test.go ================================================ package protocol import ( "strings" "testing" "time" "github.com/stretchr/testify/assert" ) var aNormalMessage = `/foo/bar,42,user01,phone01,{"user":"user01"},1420110000,1 {"Content-Type": "text/plain", "Correlation-Id": "7sdks723ksgqn"} Hello World` var aMinimalMessage = "/,42,,,,1420110000,0" var aConnectedNotification = `#connected You are connected to the server. {"ApplicationId": "phone1", "UserId": "user01", "Time": "1420110000"}` // 2015-01-01T12:00:00+01:00 is equal to 1420110000 var unixTime, _ = time.Parse(time.RFC3339, "2015-01-01T12:00:00+01:00") func TestParsingANormalMessage(t *testing.T) { assert := assert.New(t) msgI, err := Decode([]byte(aNormalMessage)) assert.NoError(err) assert.IsType(&Message{}, msgI) msg := msgI.(*Message) assert.Equal(uint64(42), msg.ID) assert.Equal(Path("/foo/bar"), msg.Path) assert.Equal("user01", msg.UserID) assert.Equal("phone01", msg.ApplicationID) assert.Equal(map[string]string{"user": "user01"}, msg.Filters) assert.Equal(unixTime.Unix(), msg.Time) assert.Equal(uint8(1), msg.NodeID) assert.Equal(`{"Content-Type": "text/plain", "Correlation-Id": "7sdks723ksgqn"}`, msg.HeaderJSON) assert.Equal("Hello World", string(msg.Body)) } func TestSerializeANormalMessage(t *testing.T) { // given: a message msg := &Message{ ID: uint64(42), Path: Path("/foo/bar"), UserID: "user01", ApplicationID: "phone01", Filters: map[string]string{"user": "user01"}, Time: unixTime.Unix(), NodeID: 1, HeaderJSON: `{"Content-Type": "text/plain", "Correlation-Id": "7sdks723ksgqn"}`, Body: []byte("Hello World"), } // then: the serialisation is as expected assert.Equal(t, aNormalMessage, string(msg.Bytes())) assert.Equal(t, "Hello World", msg.BodyAsString()) // and: the first line is as expected assert.Equal(t, strings.SplitN(aNormalMessage, "\n", 2)[0], msg.Metadata()) } func TestSerializeAMinimalMessage(t *testing.T) { msg := &Message{ ID: uint64(42), Path: Path("/"), Time: unixTime.Unix(), } assert.Equal(t, aMinimalMessage, string(msg.Bytes())) } func TestSerializeAMinimalMessageWithBody(t *testing.T) { msg := &Message{ ID: uint64(42), Path: Path("/"), Time: unixTime.Unix(), Body: []byte("Hello World"), } assert.Equal(t, aMinimalMessage+"\n\nHello World", string(msg.Bytes())) } func TestParsingAMinimalMessage(t *testing.T) { assert := assert.New(t) msgI, err := Decode([]byte(aMinimalMessage)) assert.NoError(err) assert.IsType(&Message{}, msgI) msg := msgI.(*Message) assert.Equal(uint64(42), msg.ID) assert.Equal(Path("/"), msg.Path) assert.Equal("", msg.UserID) assert.Equal("", msg.ApplicationID) assert.Nil(msg.Filters) assert.Equal(unixTime.Unix(), msg.Time) assert.Equal("", msg.HeaderJSON) assert.Equal("", string(msg.Body)) } func TestErrorsOnParsingMessages(t *testing.T) { assert := assert.New(t) var err error _, err = Decode([]byte("")) assert.Error(err) // missing meta field _, err = Decode([]byte("42,/foo/bar,user01,phone1,id123\n{}\nBla")) assert.Error(err) // id not an integer _, err = Decode([]byte("xy42,/foo/bar,user01,phone1,id123,1420110000\n")) assert.Error(err) // path is empty _, err = Decode([]byte("42,,user01,phone1,id123,1420110000\n")) assert.Error(err) // Error Message without Name _, err = Decode([]byte("!")) assert.Error(err) } func TestParsingNotificationMessage(t *testing.T) { assert := assert.New(t) msgI, err := Decode([]byte(aConnectedNotification)) assert.NoError(err) assert.IsType(&NotificationMessage{}, msgI) msg := msgI.(*NotificationMessage) assert.Equal(SUCCESS_CONNECTED, msg.Name) assert.Equal("You are connected to the server.", msg.Arg) assert.Equal(`{"ApplicationId": "phone1", "UserId": "user01", "Time": "1420110000"}`, msg.Json) assert.Equal(false, msg.IsError) } func TestSerializeANotificationMessage(t *testing.T) { msg := &NotificationMessage{ Name: SUCCESS_CONNECTED, Arg: "You are connected to the server.", Json: `{"ApplicationId": "phone1", "UserId": "user01", "Time": "1420110000"}`, IsError: false, } assert.Equal(t, aConnectedNotification, string(msg.Bytes())) } func TestSerializeAnErrorMessage(t *testing.T) { msg := &NotificationMessage{ Name: ERROR_BAD_REQUEST, Arg: "you are so bad.", IsError: true, } assert.Equal(t, "!"+ERROR_BAD_REQUEST+" "+"you are so bad.", string(msg.Bytes())) } func TestSerializeANotificationMessageWithEmptyArg(t *testing.T) { msg := &NotificationMessage{ Name: SUCCESS_SEND, Arg: "", IsError: false, } assert.Equal(t, "#"+SUCCESS_SEND, string(msg.Bytes())) } func TestParsingErrorNotificationMessage(t *testing.T) { assert := assert.New(t) raw := "!bad-request unknown command 'sdcsd'" msgI, err := Decode([]byte(raw)) assert.NoError(err) assert.IsType(&NotificationMessage{}, msgI) msg := msgI.(*NotificationMessage) assert.Equal("bad-request", msg.Name) assert.Equal("unknown command 'sdcsd'", msg.Arg) assert.Equal("", msg.Json) assert.Equal(true, msg.IsError) } func Test_Message_getPartitionFromTopic(t *testing.T) { a := assert.New(t) a.Equal("foo", Path("/foo/bar/bazz").Partition()) a.Equal("foo", Path("/foo").Partition()) a.Equal("", Path("/").Partition()) a.Equal("", Path("").Partition()) } func TestMessage_Filters(t *testing.T) { a := assert.New(t) msg := &Message{} msg.SetFilter("user", "user01") msg.SetFilter("device_id", "ID_DEVICE") a.NotNil(msg.Filters) a.Equal(msg.Filters["user"], "user01") a.Equal(msg.Filters["device_id"], "ID_DEVICE") a.JSONEq(`{"user": "user01","device_id":"ID_DEVICE"}`, string(msg.encodeFilters())) } func TestMessage_decodeFilters(t *testing.T) { a := assert.New(t) msg := &Message{} filters := []byte(`{"user": "user01","device_id":"ID_DEVICE"}`) msg.decodeFilters(filters) a.NotNil(msg.Filters) a.Contains(msg.Filters, "user") a.Contains(msg.Filters, "device_id") a.Equal(msg.Filters["user"], "user01") a.Equal(msg.Filters["device_id"], "ID_DEVICE") } ================================================ FILE: protocol/path.go ================================================ package protocol import "strings" // Path is the path of a topic type Path string // Partition returns the parsed partition from the path. func (path Path) Partition() string { if len(path) > 0 && path[0] == '/' { path = path[1:] } return strings.SplitN(string(path), "/", 2)[0] } func (path Path) RemovePrefixSlash() string { return strings.TrimPrefix(string(path), "/") } ================================================ FILE: restclient/guble_sender.go ================================================ package restclient import ( "bytes" "fmt" "net/http" "strings" "io/ioutil" "net/url" log "github.com/Sirupsen/logrus" ) type gubleSender struct { Endpoint string httpClient *http.Client } // New returns a new Sender. func New(endpoint string) Sender { return &gubleSender{ Endpoint: endpoint, httpClient: &http.Client{}, } } func (gs gubleSender) GetSubscribers(topic string) ([]byte, error) { logger.WithField("topic", topic).Info("GetSubscribers called") body := make([]byte, 0) request, err := http.NewRequest( http.MethodGet, fmt.Sprintf("%s/subscribers/%s", gs.Endpoint, trimPrefixSlash(topic)), bytes.NewReader(body), ) logger.WithField("url", fmt.Sprintf("%s/subscribers/%s", gs.Endpoint, topic)) if err != nil { return nil, err } response, err := gs.httpClient.Do(request) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode != http.StatusOK { logger.WithFields(log.Fields{ "header": response.Header, "code": response.StatusCode, "status": response.Status, }).Error("Guble response error") return nil, fmt.Errorf("Error code returned from guble: %d", response.StatusCode) } content, err := ioutil.ReadAll(response.Body) if err != nil { return nil, err } logger.WithFields(log.Fields{ "header": response.Header, "code": response.StatusCode, "body": string(content), }).Debug("Guble response") return content, nil } func (gs gubleSender) Check() bool { request, err := http.NewRequest(http.MethodHead, gs.Endpoint, nil) if err != nil { logger.WithError(err).Error("error creating request url") return false } response, err := gs.httpClient.Do(request) if err != nil { logger.WithError(err).Error("error reaching guble server endpoint") return false } defer response.Body.Close() return response.StatusCode == http.StatusOK } func (gs gubleSender) Send(topic string, body []byte, userID string, params map[string]string) error { logger.WithFields(log.Fields{ "topic": topic, "body": body, "userID": userID, "params": params, }).Debug("Sending guble message") request, err := http.NewRequest(http.MethodPost, getURL(gs.Endpoint, topic, userID, params), bytes.NewReader(body)) if err != nil { return err } response, err := gs.httpClient.Do(request) if err != nil { return err } defer response.Body.Close() if response.StatusCode != http.StatusOK { logger.WithFields(log.Fields{ "header": response.Header, "code": response.StatusCode, "status": response.Status, }).Error("Guble response error") return fmt.Errorf("Error code returned from guble: %d", response.StatusCode) } return nil } func getURL(endpoint, topic, userID string, params map[string]string) string { uv := url.Values{} uv.Add("userId", userID) if params != nil { for k, v := range params { if k != "" { uv.Add(k, v) } } } return fmt.Sprintf("%s/%s?%s", endpoint, topic, uv.Encode()) } func trimPrefixSlash(topic string) string { if strings.HasPrefix(topic, "/") { return strings.TrimPrefix(topic, "/") } return topic } ================================================ FILE: restclient/guble_sender_test.go ================================================ package restclient import ( "github.com/stretchr/testify/assert" "net/url" "testing" ) func TestGetURL(t *testing.T) { a := assert.New(t) testcases := map[string]struct { endpoint string topic string userID string params map[string]string // expected result expected string }{ "endpoint only, no topic, no user, no params": { endpoint: "http://localhost:8080/api", expected: "http://localhost:8080/api/?userId=", }, "endpoint, valid topic, no user, no params": { endpoint: "http://localhost:8080/api", topic: "topic", expected: "http://localhost:8080/api/topic?userId=", }, "endpoint, valid topic, valid user, no params": { endpoint: "http://localhost:8080/api", topic: "topic", userID: "user", expected: "http://localhost:8080/api/topic?userId=user", }, "endpoint, valid topic, valid user, empty params": { endpoint: "http://localhost:8080/api", topic: "topic", userID: "user", params: map[string]string{}, expected: "http://localhost:8080/api/topic?userId=user", }, "endpoint, valid topic, valid user, one valid param": { endpoint: "http://localhost:8080/api", topic: "topic", userID: "user", params: map[string]string{"filterCriteria1": "value1"}, expected: "http://localhost:8080/api/topic?filterCriteria1=value1&userId=user", }, "endpoint, valid topic, valid user, more valid params": { endpoint: "http://localhost:8080/api", topic: "topic", userID: "user", params: map[string]string{ "filterCriteria1": "value1", "filterCriteria2": "value2", }, expected: "http://localhost:8080/api/topic?filterCriteria1=value1&filterCriteria2=value2&userId=user", }, "endpoint, valid topic, valid user, one param value invalid inside URL": { endpoint: "http://localhost:8080/api", topic: "topic", userID: "user", params: map[string]string{"filterCriteria1": "?"}, expected: "http://localhost:8080/api/topic?filterCriteria1=%3F&userId=user", }, "endpoint, valid topic, valid user, one param key empty": { endpoint: "http://localhost:8080/api", topic: "topic", userID: "user", params: map[string]string{"": "value"}, expected: "http://localhost:8080/api/topic?userId=user", }, } var err error for name, c := range testcases { _, err = url.Parse(c.expected) a.NoError(err) a.Equal(c.expected, getURL(c.endpoint, c.topic, c.userID, c.params), "Failed check for case: "+name) } } ================================================ FILE: restclient/logger.go ================================================ package restclient import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithFields(log.Fields{ "module": "restclient", }) ================================================ FILE: restclient/sender.go ================================================ package restclient // Sender is an interface used to send a message to the guble server. type Sender interface { // Send a a message(body) to the guble Server, to the given topic, with the given userID. Send(topic string, body []byte, userID string, params map[string]string) error // Check returns `true` if the guble server endpoint is reachable, or `false` otherwise. Check() bool // GetSubscribers returns a binary encoded JSON of all subscribers of 'topic' or an error otherwise GetSubscribers(topic string) ([]byte, error) } ================================================ FILE: scripts/Dockerfile-cluster ================================================ # this Dockerfile requires u to build the app locally with the name `guble` FROM phusion/baseimage RUN mkdir -p /var/lib/guble COPY guble /go/bin/app EXPOSE 10000 8080 VOLUME /var/lib/guble ENTRYPOINT ['/go/bin/app'] ================================================ FILE: scripts/compose.cluster.test.yml ================================================ version: '2' services: cluster_1: build: context: .. dockerfile: scripts/Dockerfile-cluster entrypoint: - /go/bin/app environment: - GUBLE_NODE_ID=1 - GUBLE_LOG=debug - GUBLE_REMOTES=localhost:10000 localhost:10001 ports: - "8080:8080" - "10000:10000" cluster_2: build: context: .. dockerfile: scripts/Dockerfile-cluster entrypoint: - /go/bin/app environment: - GUBLE_NODE_ID=2 - GUBLE_LOG=debug - GUBLE_REMOTES=localhost:10000 localhost:10001 ports: - "8080:8080" - "10001:10000" ================================================ FILE: scripts/compose.postgres.test.yml ================================================ # docker-compose file to run test(s) using dockerized Postgresql # Start Postgres from root of project with following command: # sudo docker-compose -f scripts/compose.postgres.test.yml up -d # Stop Postgres from root of project with following command: # sudo docker-compose -f scripts/compose.postgres.test.yml down version: '2' services: postgres: image: postgres:9 environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD= - POSTGRES_DB=guble volumes: - /tmp/guble_test_postgres:/var/lib/postgresql/data ports: - "5432:5432" ================================================ FILE: scripts/cov.sh ================================================ #!/bin/bash -e # Run from parent directory via: # ./scripts/cov.sh # Requires installation of: # go get golang.org/x/tools/cmd/cover # go get github.com/wadey/gocovmerge source scripts/generate_coverage.sh # If we have an arg, assume travis run and push to coveralls. Otherwise launch browser results go tool cover -html=full_cov.out rm -f full_cov.out ================================================ FILE: scripts/dependencies_graph.sh ================================================ #!/bin/bash -e # Requires installation of package: graphviz (echo "digraph G {" go list -f '{{range .Imports}}{{printf "\t%q -> %q;\n" $.ImportPath .}}{{end}}' $(go list -f '{{join .Deps " "}}' github.com/smancke/guble ) github.com/smancke/guble echo "}" ) | dot -Tsvg -o dependencies_graph.svg ================================================ FILE: scripts/file-hex.sh ================================================ #!/usr/bin/env bash xxd -p $1 | tr -d '\n' ================================================ FILE: scripts/generate_coverage.sh ================================================ #!/bin/bash -e # Requires local installation of: `github.com/wadey/gocovmerge` cd $GOPATH/src/github.com/smancke/guble rm -rf ./cov mkdir cov i=0 for dir in $(find . -maxdepth 10 -not -path './.git*' -not -path '*/_test.go' -type d); do if ls ${dir}/*.go &> /dev/null; then GO_TEST_DISABLED=true go test -v -covermode=atomic -coverprofile=./cov/$i.out ./${dir} i=$((i+1)) fi done gocovmerge ./cov/*.out > full_cov.out rm -rf ./cov ================================================ FILE: scripts/generate_mocks.sh ================================================ #!/bin/bash -xe # Prerequisites: mockgen should be installed # go get github.com/golang/mock/mockgen if [ -z "$GOPATH" ]; then echo "Missing $GOPATH!"; exit 1 fi # replace in file if last operation was successful function replace { FILE=$1; shift; while [ -n "$1" ]; do echo "Replacing: $1" sed -i "s/$1//g" $FILE shift done } MOCKGEN=$GOPATH/bin/mockgen # server/service mocks $MOCKGEN -self_package service -package service \ -destination server/service/mocks_router_gen_test.go \ github.com/smancke/guble/server/router \ Router & $MOCKGEN -self_package service -package service \ -destination server/service/mocks_checker_gen_test.go \ github.com/docker/distribution/health \ Checker & # server/router mocks $MOCKGEN -self_package router -package router \ -destination server/router/mocks_router_gen_test.go \ github.com/smancke/guble/server/router \ Router replace "server/router/mocks_router_gen_test.go" "router \"github.com\/smancke\/guble\/server\/router\"" "router\." $MOCKGEN -self_package router -package router \ -destination server/router/mocks_store_gen_test.go \ github.com/smancke/guble/server/store \ MessageStore & $MOCKGEN -self_package router -package router \ -destination server/router/mocks_kvstore_gen_test.go \ github.com/smancke/guble/server/kvstore \ KVStore & $MOCKGEN -self_package router -package router \ -destination server/router/mocks_auth_gen_test.go \ github.com/smancke/guble/server/auth \ AccessManager & $MOCKGEN -self_package router -package router \ -destination server/router/mocks_checker_gen_test.go \ github.com/docker/distribution/health \ Checker & # client mocks $MOCKGEN -self_package client -package client \ -destination client/mocks_client_gen_test.go \ github.com/smancke/guble/client \ WSConnection,Client replace "client/mocks_client_gen_test.go" "client \"github.com\/smancke\/guble\/client\"" "client\." # server/apns mocks $MOCKGEN -package apns \ -destination server/apns/mocks_router_gen_test.go \ github.com/smancke/guble/server/router \ Router & $MOCKGEN -package apns \ -destination server/apns/mocks_kvstore_gen_test.go \ github.com/smancke/guble/server/kvstore \ KVStore & $MOCKGEN -package apns \ -destination server/apns/mocks_connector_gen_test.go \ github.com/smancke/guble/server/connector \ Sender,Request,Subscriber & $MOCKGEN -package apns \ -destination server/apns/mocks_pusher_gen_test.go \ github.com/smancke/guble/server/apns \ Pusher & # server/fcm mocks $MOCKGEN -package fcm \ -destination server/fcm/mocks_router_gen_test.go \ github.com/smancke/guble/server/router \ Router & $MOCKGEN -self_package fcm -package fcm \ -destination server/fcm/mocks_kvstore_gen_test.go \ github.com/smancke/guble/server/kvstore \ KVStore & $MOCKGEN -self_package fcm -package fcm \ -destination server/fcm/mocks_store_gen_test.go \ github.com/smancke/guble/server/store \ MessageStore & $MOCKGEN -self_package fcm -package fcm \ -destination server/fcm/mocks_gcm_gen_test.go \ github.com/Bogh/gcm \ Sender & # server mocks $MOCKGEN -package server \ -destination server/mocks_router_gen_test.go \ github.com/smancke/guble/server/router \ Router & $MOCKGEN -self_package server -package server \ -destination server/mocks_auth_gen_test.go \ github.com/smancke/guble/server/auth \ AccessManager & $MOCKGEN -self_package server -package server \ -destination server/mocks_store_gen_test.go \ github.com/smancke/guble/server/store \ MessageStore & $MOCKGEN -package server \ -destination server/mocks_apns_pusher_gen_test.go \ github.com/smancke/guble/server/apns \ Pusher & # server/auth mocks $MOCKGEN -self_package auth -package auth \ -destination server/auth/mocks_auth_gen_test.go \ github.com/smancke/guble/server/auth \ AccessManager replace "server/auth/mocks_auth_gen_test.go" \ "auth \"github.com\/smancke\/guble\/server\/auth\"" \ "auth\." # server/connector mocks $MOCKGEN -self_package connector -package connector \ -destination server/connector/mocks_connector_gen_test.go \ github.com/smancke/guble/server/connector \ Connector,Sender,ResponseHandler,Manager,Queue,Request,Subscriber replace "server/connector/mocks_connector_gen_test.go" \ "connector \"github.com\/smancke\/guble\/server\/connector\"" \ "connector\." $MOCKGEN -self_package connector -package connector \ -destination server/connector/mocks_router_gen_test.go \ github.com/smancke/guble/server/router \ Router & $MOCKGEN -self_package connector -package connector \ -destination server/connector/mocks_kvstore_gen_test.go \ github.com/smancke/guble/server/kvstore \ KVStore & # server/websocket mocks $MOCKGEN -self_package websocket -package websocket \ -destination server/websocket/mocks_websocket_gen_test.go \ github.com/smancke/guble/server/websocket \ WSConnection replace "server/websocket/mocks_websocket_gen_test.go" \ "websocket \"github.com\/smancke\/server\/websocket\"" \ "websocket\." $MOCKGEN -self_package websocket -package websocket \ -destination server/websocket/mocks_router_gen_test.go \ github.com/smancke/guble/server/router \ Router & $MOCKGEN -self_package websocket -package websocket \ -destination server/websocket/mocks_store_gen_test.go \ github.com/smancke/guble/server/store \ MessageStore & $MOCKGEN -self_package websocket -package websocket \ -destination server/websocket/mocks_auth_gen_test.go \ github.com/smancke/guble/server/auth \ AccessManager & # server/rest Mocks $MOCKGEN -package rest \ -destination server/rest/mocks_router_gen_test.go \ github.com/smancke/guble/server/router \ Router & # server/sms Mocks $MOCKGEN -package sms \ -destination server/sms/mocks_sender_gen_test.go \ github.com/smancke/guble/server/sms \ Sender & $MOCKGEN -package sms \ -destination server/sms/mocks_router_gen_test.go \ github.com/smancke/guble/server/router \ Router & $MOCKGEN -self_package router -package sms \ -destination server/sms/mocks_store_gen_test.go \ github.com/smancke/guble/server/store \ MessageStore & wait ================================================ FILE: server/apns/apns.go ================================================ package apns import ( "errors" "fmt" "github.com/sideshow/apns2" "github.com/smancke/guble/server/connector" "github.com/smancke/guble/server/metrics" "github.com/smancke/guble/server/router" "time" ) const ( // schema is the default database schema for APNS schema = "apns_registration" ) var ( errSenderNotRecreated = errors.New("APNS Sender could not be recreated.") ) // Config is used for configuring the APNS module. type Config struct { Enabled *bool Production *bool CertificateFileName *string CertificateBytes *[]byte CertificatePassword *string AppTopic *string Workers *int Prefix *string IntervalMetrics *bool } // apns is the private struct for handling the communication with APNS type apns struct { Config connector.Connector } // New creates a new connector.ResponsiveConnector without starting it func New(router router.Router, sender connector.Sender, config Config) (connector.ResponsiveConnector, error) { baseConn, err := connector.NewConnector( router, sender, connector.Config{ Name: "apns", Schema: schema, Prefix: *config.Prefix, URLPattern: fmt.Sprintf("/{%s}/{%s}/{%s:.*}", deviceIDKey, userIDKey, connector.TopicParam), Workers: *config.Workers, }, ) if err != nil { logger.WithError(err).Error("Base connector error") return nil, err } a := &apns{ Config: config, Connector: baseConn, } a.SetResponseHandler(a) return a, nil } func (a *apns) Start() error { err := a.Connector.Start() if err == nil { a.startMetrics() } return err } func (a *apns) startMetrics() { mTotalSentMessages.Set(0) mTotalSendErrors.Set(0) mTotalResponseErrors.Set(0) mTotalResponseInternalErrors.Set(0) mTotalResponseRegistrationErrors.Set(0) mTotalResponseOtherErrors.Set(0) mTotalSendNetworkErrors.Set(0) mTotalSendRetryCloseTLS.Set(0) mTotalSendRetryUnrecoverable.Set(0) if *a.IntervalMetrics { a.startIntervalMetric(mMinute, time.Minute) a.startIntervalMetric(mHour, time.Hour) a.startIntervalMetric(mDay, time.Hour*24) } } func (a *apns) startIntervalMetric(m metrics.Map, td time.Duration) { metrics.RegisterInterval(a.Context(), m, td, resetIntervalMetrics, processAndResetIntervalMetrics) } func (a *apns) HandleResponse(request connector.Request, responseIface interface{}, metadata *connector.Metadata, errSend error) error { logger.Info("Handle APNS response") if errSend != nil { logger.WithField("error", errSend.Error()).WithField("error_type", errSend).Error("error when trying to send APNS notification") mTotalSendErrors.Add(1) if *a.IntervalMetrics && metadata != nil { addToLatenciesAndCountsMaps(currentTotalErrorsLatenciesKey, currentTotalErrorsKey, metadata.Latency) } return errSend } r, ok := responseIface.(*apns2.Response) if !ok { mTotalResponseErrors.Add(1) return fmt.Errorf("Response could not be converted to an APNS Response") } messageID := request.Message().ID subscriber := request.Subscriber() subscriber.SetLastID(messageID) if err := a.Manager().Update(subscriber); err != nil { logger.WithField("error", err.Error()).Error("Manager could not update subscription") mTotalResponseInternalErrors.Add(1) return err } if r.Sent() { logger.WithField("id", r.ApnsID).Info("APNS notification was successfully sent") mTotalSentMessages.Add(1) if *a.IntervalMetrics && metadata != nil { addToLatenciesAndCountsMaps(currentTotalMessagesLatenciesKey, currentTotalMessagesKey, metadata.Latency) } return nil } logger.Error("APNS notification was not sent") logger.WithField("id", r.ApnsID).WithField("reason", r.Reason).Info("APNS notification was not sent - details") switch r.Reason { case apns2.ReasonMissingDeviceToken, apns2.ReasonBadDeviceToken, apns2.ReasonDeviceTokenNotForTopic, apns2.ReasonUnregistered: logger.WithField("id", r.ApnsID).Info("trying to remove subscriber because a relevant error was received from APNS") mTotalResponseRegistrationErrors.Add(1) err := a.Manager().Remove(subscriber) if err != nil { logger.WithField("id", r.ApnsID).Error("could not remove subscriber") } default: logger.Error("handling other APNS errors") mTotalResponseOtherErrors.Add(1) } return nil } ================================================ FILE: server/apns/apns_metrics.go ================================================ package apns import ( "github.com/smancke/guble/server/metrics" "time" ) var ( ns = metrics.NS("apns") mTotalSentMessages = ns.NewInt("total_sent_messages") mTotalSendErrors = ns.NewInt("total_sent_message_errors") mTotalResponseErrors = ns.NewInt("total_response_errors") mTotalResponseInternalErrors = ns.NewInt("total_response_internal_errors") mTotalResponseRegistrationErrors = ns.NewInt("total_response_registration_errors") mTotalResponseOtherErrors = ns.NewInt("total_response_other_errors") mTotalSendNetworkErrors = ns.NewInt("total_send_network_errors") mTotalSendRetryCloseTLS = ns.NewInt("total_send_retry_close_tls") mTotalSendRetryUnrecoverable = ns.NewInt("total_send_retry_unrecoverable") mMinute = ns.NewMap("minute") mHour = ns.NewMap("hour") mDay = ns.NewMap("day") ) const ( currentTotalMessagesLatenciesKey = "current_messages_total_latencies_nanos" currentTotalMessagesKey = "current_messages_count" currentTotalErrorsLatenciesKey = "current_errors_total_latencies_nanos" currentTotalErrorsKey = "current_errors_count" ) func processAndResetIntervalMetrics(m metrics.Map, td time.Duration, t time.Time) { msgLatenciesValue := m.Get(currentTotalMessagesLatenciesKey) msgNumberValue := m.Get(currentTotalMessagesKey) errLatenciesValue := m.Get(currentTotalErrorsLatenciesKey) errNumberValue := m.Get(currentTotalErrorsKey) m.Init() resetIntervalMetrics(m, t) metrics.SetRate(m, "last_messages_rate_sec", msgNumberValue, td, time.Second) metrics.SetRate(m, "last_errors_rate_sec", errNumberValue, td, time.Second) metrics.SetAverage(m, "last_messages_average_latency_msec", msgLatenciesValue, msgNumberValue, metrics.MilliPerNano, metrics.DefaultAverageLatencyJSONValue) metrics.SetAverage(m, "last_errors_average_latency_msec", errLatenciesValue, errNumberValue, metrics.MilliPerNano, metrics.DefaultAverageLatencyJSONValue) } func resetIntervalMetrics(m metrics.Map, t time.Time) { m.Set("current_interval_start", metrics.NewTime(t)) metrics.AddToMaps(currentTotalMessagesLatenciesKey, 0, m) metrics.AddToMaps(currentTotalMessagesKey, 0, m) metrics.AddToMaps(currentTotalErrorsLatenciesKey, 0, m) metrics.AddToMaps(currentTotalErrorsKey, 0, m) } func addToLatenciesAndCountsMaps(latenciesKey string, countKey string, latency time.Duration) { metrics.AddToMaps(latenciesKey, int64(latency), mMinute, mHour, mDay) metrics.AddToMaps(countKey, 1, mMinute, mHour, mDay) } ================================================ FILE: server/apns/apns_pusher.go ================================================ package apns import ( "crypto/tls" "github.com/sideshow/apns2" "github.com/sideshow/apns2/certificate" "golang.org/x/net/http2" "net" "net/http" "sync" "time" ) const ( //see https://github.com/sideshow/apns2/issues/24 and https://github.com/sideshow/apns2/issues/20 tlsDialTimeout = 20 * time.Second httpClientTimeout = 30 * time.Second ) type Pusher interface { Push(*apns2.Notification) (*apns2.Response, error) } type closable interface { CloseTLS() } func newPusher(c Config) (Pusher, error) { logger.Info("creating new apns pusher") var ( cert tls.Certificate errCert error ) if c.CertificateFileName != nil && *c.CertificateFileName != "" { cert, errCert = certificate.FromP12File(*c.CertificateFileName, *c.CertificatePassword) } else { cert, errCert = certificate.FromP12Bytes(*c.CertificateBytes, *c.CertificatePassword) } if errCert != nil { return nil, errCert } var clientFactory func(certificate tls.Certificate) *apns2Client if *c.Production { clientFactory = newProductionClient } else { clientFactory = newDevelopmentClient } apns2.TLSDialTimeout = tlsDialTimeout apns2.HTTPClientTimeout = httpClientTimeout logger.Info("created new apns pusher") return clientFactory(cert), nil } func newProductionClient(certificate tls.Certificate) *apns2Client { logger.Info("APNS Pusher in Production mode") c := newApns2Client(certificate) c.Production() logger.WithField("apns_url", c.Host).Info("APNS Pusher in Production mode url") return c } func newDevelopmentClient(certificate tls.Certificate) *apns2Client { logger.Info("APNS Pusher in Development mode") c := newApns2Client(certificate) c.Development() logger.WithField("apns_url", c.Host).Info("APNS Pusher in Development mode url") return c } type apns2Client struct { *apns2.Client tlsConn net.Conn mu sync.Mutex } func newApns2Client(certificate tls.Certificate) *apns2Client { logger.Info("creating new apns2client") c := &apns2Client{} tlsConfig := &tls.Config{ Certificates: []tls.Certificate{certificate}, } if len(certificate.Certificate) > 0 { tlsConfig.BuildNameToCertificate() } transport := &http2.Transport{ TLSClientConfig: tlsConfig, DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { conn, err := tls.DialWithDialer(&net.Dialer{Timeout: tlsDialTimeout, KeepAlive: 2 * time.Second}, network, addr, cfg) c.mu.Lock() defer c.mu.Unlock() if err == nil { c.tlsConn = conn } else { c.tlsConn = nil } return conn, err }, } client := &apns2.Client{ HTTPClient: &http.Client{ Transport: transport, Timeout: httpClientTimeout, }, Certificate: certificate, Host: apns2.DefaultHost, } c.Client = client logger.Info("created new apns2client") return c } // interface closable used used by apns_sender func (c *apns2Client) CloseTLS() { c.mu.Lock() defer c.mu.Unlock() if c.tlsConn != nil { logger.Info("Trying to close TLS connection") c.tlsConn.Close() logger.Info("Closed TLS connection") c.tlsConn = nil } } ================================================ FILE: server/apns/apns_sender.go ================================================ package apns import ( "errors" "github.com/jpillora/backoff" "github.com/sideshow/apns2" "github.com/smancke/guble/server/connector" "net" "time" ) const ( // deviceIDKey is the key name set on the route params to identify the application deviceIDKey = "device_token" userIDKey = "user_id" ) var ( errPusherInvalidParams = errors.New("Invalid parameters of APNS Pusher") ErrRetryFailed = errors.New("Retry failed") ) type sender struct { client Pusher appTopic string } func NewSender(config Config) (connector.Sender, error) { pusher, err := newPusher(config) if err != nil { logger.WithField("error", err.Error()).Error("APNS Pusher creation error") return nil, err } return NewSenderUsingPusher(pusher, *config.AppTopic) } func NewSenderUsingPusher(pusher Pusher, appTopic string) (connector.Sender, error) { if pusher == nil || appTopic == "" { return nil, errPusherInvalidParams } return &sender{ client: pusher, appTopic: appTopic, }, nil } func (s sender) Send(request connector.Request) (interface{}, error) { deviceToken := request.Subscriber().Route().Get(deviceIDKey) logger.WithField("deviceToken", deviceToken).Info("Trying to push a message to APNS") push := func() (interface{}, error) { return s.client.Push(&apns2.Notification{ Priority: apns2.PriorityHigh, Topic: s.appTopic, DeviceToken: deviceToken, Payload: request.Message().Body, }) } withRetry := &retryable{ Backoff: backoff.Backoff{ Min: 1 * time.Second, Max: 10 * time.Second, Factor: 2, Jitter: true, }, maxTries: 3, } result, err := withRetry.execute(push) if err != nil && err == ErrRetryFailed { if closable, ok := s.client.(closable); ok { logger.Warn("Close TLS and retry again") mTotalSendRetryCloseTLS.Add(1) closable.CloseTLS() return push() } else { mTotalSendRetryUnrecoverable.Add(1) logger.Error("Cannot Close TLS. Unrecoverable state") } } return result, err } type retryable struct { backoff.Backoff maxTries int } func (r *retryable) execute(op func() (interface{}, error)) (interface{}, error) { tryCounter := 0 for { tryCounter++ result, opError := op() // retry on network errors if _, ok := opError.(net.Error); ok { mTotalSendNetworkErrors.Add(1) if tryCounter >= r.maxTries { return "", ErrRetryFailed } d := r.Duration() logger.WithField("error", opError.Error()).Warn("Retry in ", d) time.Sleep(d) continue } else { return result, opError } } } ================================================ FILE: server/apns/apns_sender_test.go ================================================ package apns import ( "errors" "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/router" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" "testing" ) func TestNewSender_ErrorBytes(t *testing.T) { a := assert.New(t) //given emptyBytes := []byte("") emptyPassword := "" cfg := Config{ CertificateBytes: &emptyBytes, CertificatePassword: &emptyPassword, } //when pusher, err := NewSender(cfg) // then a.Error(err) a.Nil(pusher) } func TestNewSender_ErrorFile(t *testing.T) { a := assert.New(t) //given wrongFilename := "." emptyPassword := "" cfg := Config{ CertificateFileName: &wrongFilename, CertificatePassword: &emptyPassword, } //when pusher, err := NewSender(cfg) // then a.Error(err) a.Nil(pusher) } func TestSender_Send(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) // given routeParams := make(map[string]string) routeParams["device_id"] = "1234" routeConfig := router.RouteConfig{ Path: protocol.Path("path"), RouteParams: routeParams, } route := router.NewRoute(routeConfig) msg := &protocol.Message{ Body: []byte("{}"), } mSubscriber := NewMockSubscriber(testutil.MockCtrl) mSubscriber.EXPECT().Route().Return(route).AnyTimes() mRequest := NewMockRequest(testutil.MockCtrl) mRequest.EXPECT().Subscriber().Return(mSubscriber).AnyTimes() mRequest.EXPECT().Message().Return(msg).AnyTimes() mPusher := NewMockPusher(testutil.MockCtrl) mPusher.EXPECT().Push(gomock.Any()).Return(nil, nil) // and s, err := NewSenderUsingPusher(mPusher, "com.myapp") a.NoError(err) // when rsp, err := s.Send(mRequest) // then a.NoError(err) a.Nil(rsp) } func TestSender_Retry(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) // given routeParams := make(map[string]string) routeParams["device_id"] = "1234" routeConfig := router.RouteConfig{ Path: protocol.Path("path"), RouteParams: routeParams, } route := router.NewRoute(routeConfig) msg := &protocol.Message{ Body: []byte("{}"), } mSubscriber := NewMockSubscriber(testutil.MockCtrl) mSubscriber.EXPECT().Route().Return(route).AnyTimes() mRequest := NewMockRequest(testutil.MockCtrl) mRequest.EXPECT().Subscriber().Return(mSubscriber).AnyTimes() mRequest.EXPECT().Message().Return(msg).AnyTimes() mPusher := NewMockPusher(testutil.MockCtrl) mPusher.EXPECT().Push(gomock.Any()).Return(nil, errMockTimeout) mPusher.EXPECT().Push(gomock.Any()).Return(nil, nil) // and s, err := NewSenderUsingPusher(mPusher, "com.myapp") a.NoError(err) // when rsp, err := s.Send(mRequest) // then a.NoError(err) a.Nil(rsp) } type resultpair struct { result interface{} err error } func Test_Retriable(t *testing.T) { a := assert.New(t) testCases := []struct { name string maxTries int results []resultpair expectedResult string expectedError error expectedMethodCalls int }{ {"No errors", 3, []resultpair{{result: "0"}}, "0", nil, 1}, {"Retry once", 3, []resultpair{{err: errMockTimeout}, {result: "1"}}, "1", nil, 2}, {"Retry twice", 3, []resultpair{{err: errMockTimeout}, {err: errMockTimeout}, {result: "2"}}, "2", nil, 3}, {"Retry only twice", 3, []resultpair{{err: errMockTimeout}, {err: errMockTimeout}, {err: errMockTimeout, result: ""}, {result: "3"}}, "", ErrRetryFailed, 3}, {"Do not retry", 3, []resultpair{{err: errMockOther, result: ""}, {result: "1"}}, "", errMockOther, 1}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // given var counter int method := func() (interface{}, error) { elem := tc.results[counter] counter++ return elem.result, elem.err } withRetry := &retryable{ maxTries: tc.maxTries, } // when result, err := withRetry.execute(method) // then a.EqualValues(tc.expectedResult, result) a.EqualValues(tc.expectedError, err) a.EqualValues(tc.expectedMethodCalls, counter) }) } } // see - net.Error type mockTimeout struct{} func (e *mockTimeout) Error() string { return "mock i/o timeout" } func (e *mockTimeout) Timeout() bool { return true } func (e *mockTimeout) Temporary() bool { return true } var errMockTimeout error = &mockTimeout{} var errMockOther error = errors.New("mock not retriable") ================================================ FILE: server/apns/apns_test.go ================================================ package apns import ( "errors" "github.com/golang/mock/gomock" "github.com/sideshow/apns2" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/connector" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" "testing" ) var ErrSendRandomError = errors.New("A Sender error") func TestNew_WithoutKVStore(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) //given mRouter := NewMockRouter(testutil.MockCtrl) errKVS := errors.New("No KVS was set-up in Router") mRouter.EXPECT().KVStore().Return(nil, errKVS).AnyTimes() mSender := NewMockSender(testutil.MockCtrl) prefix := "/apns/" workers := 1 cfg := Config{ Prefix: &prefix, Workers: &workers, } //when c, err := New(mRouter, mSender, cfg) //then a.Error(err) a.Nil(c) } func TestConn_HandleResponseOnSendError(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) //given c, _ := newAPNSConnector(t) mRequest := NewMockRequest(testutil.MockCtrl) //when err := c.HandleResponse(mRequest, nil, nil, ErrSendRandomError) //then a.Equal(ErrSendRandomError, err) } func TestConn_HandleResponse(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) //given c, mKVS := newAPNSConnector(t) mSubscriber := NewMockSubscriber(testutil.MockCtrl) mSubscriber.EXPECT().SetLastID(gomock.Any()) mSubscriber.EXPECT().Key().Return("key").AnyTimes() mSubscriber.EXPECT().Encode().Return([]byte("{}"), nil).AnyTimes() mKVS.EXPECT().Put(schema, "key", []byte("{}")).Times(2) c.Manager().Add(mSubscriber) message := &protocol.Message{ ID: 42, } mRequest := NewMockRequest(testutil.MockCtrl) mRequest.EXPECT().Message().Return(message).AnyTimes() mRequest.EXPECT().Subscriber().Return(mSubscriber).AnyTimes() response := &apns2.Response{ ApnsID: "id-life", StatusCode: 200, } //when err := c.HandleResponse(mRequest, response, nil, nil) //then a.NoError(err) } func TestNew_HandleResponseHandleSubscriber(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) //given c, mKVS := newAPNSConnector(t) removeForReasons := []string{ apns2.ReasonMissingDeviceToken, apns2.ReasonBadDeviceToken, apns2.ReasonDeviceTokenNotForTopic, apns2.ReasonUnregistered, } for _, reason := range removeForReasons { message := &protocol.Message{ ID: 42, } mSubscriber := NewMockSubscriber(testutil.MockCtrl) mSubscriber.EXPECT().SetLastID(gomock.Any()) mSubscriber.EXPECT().Cancel() mSubscriber.EXPECT().Key().Return("key").AnyTimes() mSubscriber.EXPECT().Encode().Return([]byte("{}"), nil).AnyTimes() mKVS.EXPECT().Put(schema, "key", []byte("{}")).Times(2) mKVS.EXPECT().Delete(schema, "key") c.Manager().Add(mSubscriber) mRequest := NewMockRequest(testutil.MockCtrl) mRequest.EXPECT().Message().Return(message).AnyTimes() mRequest.EXPECT().Subscriber().Return(mSubscriber).AnyTimes() response := &apns2.Response{ ApnsID: "id-life", StatusCode: 400, Reason: reason, } //when err := c.HandleResponse(mRequest, response, nil, nil) //then a.NoError(err) } } func TestNew_HandleResponseDoNotHandleSubscriber(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) //given c, mKVS := newAPNSConnector(t) noActionForReasons := []string{ apns2.ReasonPayloadEmpty, apns2.ReasonPayloadTooLarge, apns2.ReasonBadTopic, apns2.ReasonTopicDisallowed, apns2.ReasonBadMessageID, apns2.ReasonBadExpirationDate, apns2.ReasonBadPriority, apns2.ReasonDuplicateHeaders, apns2.ReasonBadCertificateEnvironment, apns2.ReasonBadCertificate, apns2.ReasonForbidden, apns2.ReasonBadPath, apns2.ReasonMethodNotAllowed, apns2.ReasonTooManyRequests, apns2.ReasonIdleTimeout, apns2.ReasonShutdown, apns2.ReasonInternalServerError, apns2.ReasonServiceUnavailable, apns2.ReasonMissingTopic, } for _, reason := range noActionForReasons { message := &protocol.Message{ ID: 42, } mSubscriber := NewMockSubscriber(testutil.MockCtrl) mSubscriber.EXPECT().SetLastID(gomock.Any()) mSubscriber.EXPECT().Key().Return("key").AnyTimes() mSubscriber.EXPECT().Encode().Return([]byte("{}"), nil).AnyTimes() mSubscriber.EXPECT().Cancel() mKVS.EXPECT().Put(schema, "key", []byte("{}")).Times(2) mKVS.EXPECT().Delete(schema, "key") c.Manager().Add(mSubscriber) mRequest := NewMockRequest(testutil.MockCtrl) mRequest.EXPECT().Message().Return(message).AnyTimes() mRequest.EXPECT().Subscriber().Return(mSubscriber).AnyTimes() response := &apns2.Response{ ApnsID: "id-apns", StatusCode: 400, Reason: reason, } //when err := c.HandleResponse(mRequest, response, nil, nil) //then a.NoError(err) c.Manager().Remove(mSubscriber) } } func newAPNSConnector(t *testing.T) (c connector.ResponsiveConnector, mKVS *MockKVStore) { mKVS = NewMockKVStore(testutil.MockCtrl) mRouter := NewMockRouter(testutil.MockCtrl) mRouter.EXPECT().KVStore().Return(mKVS, nil).AnyTimes() mSender := NewMockSender(testutil.MockCtrl) prefix := "/apns/" workers := 1 intervalMetrics := false password := "test" bytes := []byte("test") cfg := Config{ Prefix: &prefix, Workers: &workers, IntervalMetrics: &intervalMetrics, CertificatePassword: &password, CertificateBytes: &bytes, } c, err := New(mRouter, mSender, cfg) assert.NoError(t, err) assert.NotNil(t, c) return } ================================================ FILE: server/apns/logger.go ================================================ package apns import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithField("module", "apns") ================================================ FILE: server/apns/mocks_connector_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/connector (interfaces: Sender,Request,Subscriber) package apns import ( "context" "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/connector" "github.com/smancke/guble/server/router" ) // Mock of Sender interface type MockSender struct { ctrl *gomock.Controller recorder *_MockSenderRecorder } // Recorder for MockSender (not exported) type _MockSenderRecorder struct { mock *MockSender } func NewMockSender(ctrl *gomock.Controller) *MockSender { mock := &MockSender{ctrl: ctrl} mock.recorder = &_MockSenderRecorder{mock} return mock } func (_m *MockSender) EXPECT() *_MockSenderRecorder { return _m.recorder } func (_m *MockSender) Send(_param0 connector.Request) (interface{}, error) { ret := _m.ctrl.Call(_m, "Send", _param0) ret0, _ := ret[0].(interface{}) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockSenderRecorder) Send(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Send", arg0) } // Mock of Request interface type MockRequest struct { ctrl *gomock.Controller recorder *_MockRequestRecorder } // Recorder for MockRequest (not exported) type _MockRequestRecorder struct { mock *MockRequest } func NewMockRequest(ctrl *gomock.Controller) *MockRequest { mock := &MockRequest{ctrl: ctrl} mock.recorder = &_MockRequestRecorder{mock} return mock } func (_m *MockRequest) EXPECT() *_MockRequestRecorder { return _m.recorder } func (_m *MockRequest) Message() *protocol.Message { ret := _m.ctrl.Call(_m, "Message") ret0, _ := ret[0].(*protocol.Message) return ret0 } func (_mr *_MockRequestRecorder) Message() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Message") } func (_m *MockRequest) Subscriber() connector.Subscriber { ret := _m.ctrl.Call(_m, "Subscriber") ret0, _ := ret[0].(connector.Subscriber) return ret0 } func (_mr *_MockRequestRecorder) Subscriber() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Subscriber") } // Mock of Subscriber interface type MockSubscriber struct { ctrl *gomock.Controller recorder *_MockSubscriberRecorder } // Recorder for MockSubscriber (not exported) type _MockSubscriberRecorder struct { mock *MockSubscriber } func NewMockSubscriber(ctrl *gomock.Controller) *MockSubscriber { mock := &MockSubscriber{ctrl: ctrl} mock.recorder = &_MockSubscriberRecorder{mock} return mock } func (_m *MockSubscriber) EXPECT() *_MockSubscriberRecorder { return _m.recorder } func (_m *MockSubscriber) Cancel() { _m.ctrl.Call(_m, "Cancel") } func (_mr *_MockSubscriberRecorder) Cancel() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Cancel") } func (_m *MockSubscriber) Encode() ([]byte, error) { ret := _m.ctrl.Call(_m, "Encode") ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockSubscriberRecorder) Encode() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Encode") } func (_m *MockSubscriber) Filter(_param0 map[string]string) bool { ret := _m.ctrl.Call(_m, "Filter", _param0) ret0, _ := ret[0].(bool) return ret0 } func (_mr *_MockSubscriberRecorder) Filter(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Filter", arg0) } func (_m *MockSubscriber) Key() string { ret := _m.ctrl.Call(_m, "Key") ret0, _ := ret[0].(string) return ret0 } func (_mr *_MockSubscriberRecorder) Key() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Key") } func (_m *MockSubscriber) Loop(_param0 context.Context, _param1 connector.Queue) error { ret := _m.ctrl.Call(_m, "Loop", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockSubscriberRecorder) Loop(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Loop", arg0, arg1) } func (_m *MockSubscriber) Reset() error { ret := _m.ctrl.Call(_m, "Reset") ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockSubscriberRecorder) Reset() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Reset") } func (_m *MockSubscriber) Route() *router.Route { ret := _m.ctrl.Call(_m, "Route") ret0, _ := ret[0].(*router.Route) return ret0 } func (_mr *_MockSubscriberRecorder) Route() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Route") } func (_m *MockSubscriber) SetLastID(_param0 uint64) { _m.ctrl.Call(_m, "SetLastID", _param0) } func (_mr *_MockSubscriberRecorder) SetLastID(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "SetLastID", arg0) } ================================================ FILE: server/apns/mocks_kvstore_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/kvstore (interfaces: KVStore) package apns import ( gomock "github.com/golang/mock/gomock" ) // Mock of KVStore interface type MockKVStore struct { ctrl *gomock.Controller recorder *_MockKVStoreRecorder } // Recorder for MockKVStore (not exported) type _MockKVStoreRecorder struct { mock *MockKVStore } func NewMockKVStore(ctrl *gomock.Controller) *MockKVStore { mock := &MockKVStore{ctrl: ctrl} mock.recorder = &_MockKVStoreRecorder{mock} return mock } func (_m *MockKVStore) EXPECT() *_MockKVStoreRecorder { return _m.recorder } func (_m *MockKVStore) Delete(_param0 string, _param1 string) error { ret := _m.ctrl.Call(_m, "Delete", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockKVStoreRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Delete", arg0, arg1) } func (_m *MockKVStore) Get(_param0 string, _param1 string) ([]byte, bool, error) { ret := _m.ctrl.Call(_m, "Get", _param0, _param1) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(bool) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } func (_mr *_MockKVStoreRecorder) Get(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Get", arg0, arg1) } func (_m *MockKVStore) Iterate(_param0 string, _param1 string) chan [2]string { ret := _m.ctrl.Call(_m, "Iterate", _param0, _param1) ret0, _ := ret[0].(chan [2]string) return ret0 } func (_mr *_MockKVStoreRecorder) Iterate(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Iterate", arg0, arg1) } func (_m *MockKVStore) IterateKeys(_param0 string, _param1 string) chan string { ret := _m.ctrl.Call(_m, "IterateKeys", _param0, _param1) ret0, _ := ret[0].(chan string) return ret0 } func (_mr *_MockKVStoreRecorder) IterateKeys(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "IterateKeys", arg0, arg1) } func (_m *MockKVStore) Put(_param0 string, _param1 string, _param2 []byte) error { ret := _m.ctrl.Call(_m, "Put", _param0, _param1, _param2) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockKVStoreRecorder) Put(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Put", arg0, arg1, arg2) } ================================================ FILE: server/apns/mocks_pusher_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/apns (interfaces: Pusher) package apns import ( gomock "github.com/golang/mock/gomock" apns2 "github.com/sideshow/apns2" ) // Mock of Pusher interface type MockPusher struct { ctrl *gomock.Controller recorder *_MockPusherRecorder } // Recorder for MockPusher (not exported) type _MockPusherRecorder struct { mock *MockPusher } func NewMockPusher(ctrl *gomock.Controller) *MockPusher { mock := &MockPusher{ctrl: ctrl} mock.recorder = &_MockPusherRecorder{mock} return mock } func (_m *MockPusher) EXPECT() *_MockPusherRecorder { return _m.recorder } func (_m *MockPusher) Push(_param0 *apns2.Notification) (*apns2.Response, error) { ret := _m.ctrl.Call(_m, "Push", _param0) ret0, _ := ret[0].(*apns2.Response) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockPusherRecorder) Push(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Push", arg0) } ================================================ FILE: server/apns/mocks_router_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/router (interfaces: Router) package apns import ( "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/cluster" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" ) // Mock of Router interface type MockRouter struct { ctrl *gomock.Controller recorder *_MockRouterRecorder } // Recorder for MockRouter (not exported) type _MockRouterRecorder struct { mock *MockRouter } func NewMockRouter(ctrl *gomock.Controller) *MockRouter { mock := &MockRouter{ctrl: ctrl} mock.recorder = &_MockRouterRecorder{mock} return mock } func (_m *MockRouter) EXPECT() *_MockRouterRecorder { return _m.recorder } func (_m *MockRouter) AccessManager() (auth.AccessManager, error) { ret := _m.ctrl.Call(_m, "AccessManager") ret0, _ := ret[0].(auth.AccessManager) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) AccessManager() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "AccessManager") } func (_m *MockRouter) Cluster() *cluster.Cluster { ret := _m.ctrl.Call(_m, "Cluster") ret0, _ := ret[0].(*cluster.Cluster) return ret0 } func (_mr *_MockRouterRecorder) Cluster() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Cluster") } func (_m *MockRouter) Done() <-chan bool { ret := _m.ctrl.Call(_m, "Done") ret0, _ := ret[0].(<-chan bool) return ret0 } func (_mr *_MockRouterRecorder) Done() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Done") } func (_m *MockRouter) Fetch(_param0 *store.FetchRequest) error { ret := _m.ctrl.Call(_m, "Fetch", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockRouter) GetSubscribers(_param0 string) ([]byte, error) { ret := _m.ctrl.Call(_m, "GetSubscribers", _param0) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) GetSubscribers(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetSubscribers", arg0) } func (_m *MockRouter) HandleMessage(_param0 *protocol.Message) error { ret := _m.ctrl.Call(_m, "HandleMessage", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) HandleMessage(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "HandleMessage", arg0) } func (_m *MockRouter) KVStore() (kvstore.KVStore, error) { ret := _m.ctrl.Call(_m, "KVStore") ret0, _ := ret[0].(kvstore.KVStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) KVStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "KVStore") } func (_m *MockRouter) MessageStore() (store.MessageStore, error) { ret := _m.ctrl.Call(_m, "MessageStore") ret0, _ := ret[0].(store.MessageStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) MessageStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MessageStore") } func (_m *MockRouter) Subscribe(_param0 *router.Route) (*router.Route, error) { ret := _m.ctrl.Call(_m, "Subscribe", _param0) ret0, _ := ret[0].(*router.Route) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) Subscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Subscribe", arg0) } func (_m *MockRouter) Unsubscribe(_param0 *router.Route) { _m.ctrl.Call(_m, "Unsubscribe", _param0) } func (_mr *_MockRouterRecorder) Unsubscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Unsubscribe", arg0) } ================================================ FILE: server/auth/accessmanager.go ================================================ package auth import ( "github.com/smancke/guble/protocol" ) // AccessType permission required by the user type AccessType int const ( // READ permission READ AccessType = iota // WRITE permission WRITE ) // AccessManager interface allows to provide a custom authentication mechanism type AccessManager interface { IsAllowed(accessType AccessType, userID string, path protocol.Path) bool } ================================================ FILE: server/auth/accessmanager_test.go ================================================ package auth import ( "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "testing" ) func Test_AllowAllAccessManager(t *testing.T) { a := assert.New(t) am := AccessManager(NewAllowAllAccessManager(true)) a.True(am.IsAllowed(READ, "userid", "/path")) am = AccessManager(NewAllowAllAccessManager(false)) a.False(am.IsAllowed(READ, "userid", "/path")) } func Test_RestAccessManagerAllowed(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("true")) })) defer ts.Close() a := assert.New(t) am := NewRestAccessManager(ts.URL) a.True(am.IsAllowed(READ, "foo", "/foo")) a.True(am.IsAllowed(WRITE, "foo", "/foo")) } func Test_RestAccessManagerNotAllowed(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("false")) })) defer ts.Close() am := NewRestAccessManager(ts.URL) a := assert.New(t) a.False(am.IsAllowed(READ, "user", "/foo")) } func Test_RestAccessManagerNotAllowedWithServerNotStarted(t *testing.T) { ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("false")) })) defer ts.Close() am := NewRestAccessManager(ts.URL) a := assert.New(t) a.False(am.IsAllowed(READ, "user", "/foo")) } func Test_RestAccessManagerNotAllowedHttpReturningStatusForbidden(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) })) defer ts.Close() a := assert.New(t) am := NewRestAccessManager(ts.URL) a.False(am.IsAllowed(READ, "foo", "/foo")) a.False(am.IsAllowed(WRITE, "foo", "/foo")) } ================================================ FILE: server/auth/allow_all_accessmanager.go ================================================ package auth import ( "github.com/smancke/guble/protocol" ) //AllowAllAccessManager is a dummy implementation that grants access for everything. type AllowAllAccessManager bool //NewAllowAllAccessManager returns a new AllowAllAccessManager (depending on the passed parameter, always true or always false) func NewAllowAllAccessManager(allowAll bool) AllowAllAccessManager { return AllowAllAccessManager(allowAll) } //IsAllowed returns always the same value, given at construction time (true or false). func (am AllowAllAccessManager) IsAllowed(accessType AccessType, userID string, path protocol.Path) bool { return bool(am) } ================================================ FILE: server/auth/logger.go ================================================ package auth import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithFields(log.Fields{ "module": "accessManager", }) ================================================ FILE: server/auth/mocks_auth_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/auth (interfaces: AccessManager) package auth import ( "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" ) // Mock of AccessManager interface type MockAccessManager struct { ctrl *gomock.Controller recorder *_MockAccessManagerRecorder } // Recorder for MockAccessManager (not exported) type _MockAccessManagerRecorder struct { mock *MockAccessManager } func NewMockAccessManager(ctrl *gomock.Controller) *MockAccessManager { mock := &MockAccessManager{ctrl: ctrl} mock.recorder = &_MockAccessManagerRecorder{mock} return mock } func (_m *MockAccessManager) EXPECT() *_MockAccessManagerRecorder { return _m.recorder } func (_m *MockAccessManager) IsAllowed(_param0 AccessType, _param1 string, _param2 protocol.Path) bool { ret := _m.ctrl.Call(_m, "IsAllowed", _param0, _param1, _param2) ret0, _ := ret[0].(bool) return ret0 } func (_mr *_MockAccessManagerRecorder) IsAllowed(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "IsAllowed", arg0, arg1, arg2) } ================================================ FILE: server/auth/rest_accessmanager.go ================================================ package auth import ( "github.com/smancke/guble/protocol" log "github.com/Sirupsen/logrus" "io/ioutil" "net/http" "net/url" ) // RestAccessManager is a url for which the access is allowed or not. type RestAccessManager string // NewRestAccessManager returns a new RestAccessManager. func NewRestAccessManager(url string) RestAccessManager { return RestAccessManager(url) } // IsAllowed is an implementation of the AccessManager interface. // The boolean result is based on matching between the desired AccessType, the userId and the path. func (ram RestAccessManager) IsAllowed(accessType AccessType, userId string, path protocol.Path) bool { u, _ := url.Parse(string(ram)) q := u.Query() if accessType == READ { q.Set("type", "read") } else { q.Set("type", "write") } q.Set("userId", userId) q.Set("path", string(path)) resp, err := http.DefaultClient.Get(u.String()) if err != nil { logger.WithError(err).WithField("module", "RestAccessManager").Warn("Write message failed") return false } defer resp.Body.Close() responseBody, err := ioutil.ReadAll(resp.Body) if err != nil || resp.StatusCode != 200 { logger.WithError(err).WithField("httpCode", resp.StatusCode).Info("Error getting permission") logger.WithField("responseBody", responseBody).Debug("HTTP Response Body") return false } logger.WithFields(log.Fields{ "access_type": accessType, "userId": userId, "path": path, "responseBody": string(responseBody), }).Debug("Access allowed") return "true" == string(responseBody) } ================================================ FILE: server/benchmarking_apns_test.go ================================================ package server import ( "bytes" "fmt" log "github.com/Sirupsen/logrus" "github.com/golang/mock/gomock" "github.com/sideshow/apns2" "github.com/smancke/guble/client" "github.com/smancke/guble/server/apns" "github.com/smancke/guble/server/connector" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/websocket" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" "io/ioutil" "net/http" "os" "strings" "testing" "time" ) // APNS benchmarks func BenchmarkAPNS_1Workers50MilliTimeout(b *testing.B) { params := &benchParams{ B: b, workers: 1, subscriptions: 8, timeout: 50 * time.Millisecond, clients: 8, sender: sendMessageSample, } params.throughputAPNS() fmt.Println(params) } func BenchmarkAPNS_8Workers50MilliTimeout(b *testing.B) { params := &benchParams{ B: b, workers: 8, subscriptions: 8, timeout: 50 * time.Millisecond, clients: 8, sender: sendMessageSample, } params.throughputAPNS() fmt.Println(params) } func BenchmarkAPNS_16Workers50MilliTimeout(b *testing.B) { params := &benchParams{ B: b, workers: 16, subscriptions: 8, timeout: 50 * time.Millisecond, clients: 8, sender: sendMessageSample, } params.throughputAPNS() fmt.Println(params) } func BenchmarkAPNS_1Workers100MilliTimeout(b *testing.B) { params := &benchParams{ B: b, workers: 1, subscriptions: 8, timeout: 100 * time.Millisecond, clients: 8, sender: sendMessageSample, } params.throughputAPNS() fmt.Println(params) } func BenchmarkAPNS_8Workers100MilliTimeout(b *testing.B) { params := &benchParams{ B: b, workers: 8, subscriptions: 8, timeout: 100 * time.Millisecond, clients: 8, sender: sendMessageSample, } params.throughputAPNS() fmt.Println(params) } func BenchmarkAPNS_16Workers100MilliTimeout(b *testing.B) { params := &benchParams{ B: b, workers: 16, subscriptions: 8, timeout: 100 * time.Millisecond, clients: 8, sender: sendMessageSample, } params.throughputAPNS() fmt.Println(params) } func (params *benchParams) throughputAPNS() { defer testutil.EnableDebugForMethod()() _, finish := testutil.NewMockBenchmarkCtrl(params.B) defer finish() defer testutil.ResetDefaultRegistryHealthCheck() a := assert.New(params) dir, errTempDir := ioutil.TempDir("", "guble_benchmarking_apns_test") a.NoError(errTempDir) *Config.HttpListen = "localhost:0" *Config.KVS = "memory" *Config.MS = "file" *Config.StoragePath = dir *Config.APNS.Enabled = true *Config.APNS.AppTopic = "app.topic" *Config.APNS.Prefix = "/apns/" params.receiveC = make(chan bool) CreateModules = createModulesWebsocketAndMockAPNSPusher(params.receiveC, params.timeout) params.service = StartService() var apnsConn connector.ResponsiveConnector var ok bool for _, iface := range params.service.ModulesSortedByStartOrder() { apnsConn, ok = iface.(connector.ResponsiveConnector) if ok { break } } if apnsConn == nil { a.FailNow("There should be a module of type: APNS Connector") } urlFormat := fmt.Sprintf("http://%s/apns/apns-%%d/%%d/%%s", params.service.WebServer().GetAddr()) for i := 1; i <= params.subscriptions; i++ { // create APNS subscription response, errPost := http.Post( fmt.Sprintf(urlFormat, i, i, strings.TrimPrefix(testTopic, "/")), "text/plain", bytes.NewBufferString(""), ) a.NoError(errPost) a.Equal(response.StatusCode, 200) body, errReadAll := ioutil.ReadAll(response.Body) a.NoError(errReadAll) a.Equal("{\"subscribed\":\"/topic\"}", string(body)) } clients := params.createClients() // Report allocations also params.ReportAllocs() expectedMessagesNumber := params.N * params.clients * params.subscriptions logger.WithFields(log.Fields{ "expectedMessagesNumber": expectedMessagesNumber, "b.N": params.N, }).Info("Expecting messages") params.wg.Add(expectedMessagesNumber) // start the receive loop (a select on receiveC and doneC) params.doneC = make(chan struct{}) params.receiveLoop() params.ResetTimer() // send all messages, or fail on any error for _, cl := range clients { go func(cl client.Client) { for i := 0; i < params.N; i++ { err := params.sender(cl) if err != nil { a.FailNow("Message could not be sent") } params.sent++ } }(cl) } // wait to receive all messages params.wg.Wait() // stop timer after the actual test params.StopTimer() close(params.doneC) a.NoError(params.service.Stop()) params.service = nil close(params.receiveC) errRemove := os.RemoveAll(dir) if errRemove != nil { logger.WithError(errRemove).WithField("module", "testing").Error("Could not remove directory") } } var createModulesWebsocketAndMockAPNSPusher = func(receiveC chan bool, simulatedLatency time.Duration) func(router router.Router) []interface{} { return func(router router.Router) []interface{} { var modules []interface{} if wsHandler, err := websocket.NewWSHandler(router, "/stream/"); err != nil { logger.WithError(err).Error("Error loading WSHandler module") } else { modules = append(modules, wsHandler) } if *Config.APNS.Enabled { if *Config.APNS.AppTopic == "" { logger.Panic("The Mobile App Topic (usually the bundle-id) has to be provided when APNS is enabled") } // create and use a mock Pusher - introducing a latency per each message rsp := &apns2.Response{ ApnsID: "apns-id", StatusCode: 200, } mPusher := NewMockPusher(testutil.MockCtrl) mPusher.EXPECT().Push(gomock.Any()). Do(func(notif *apns2.Notification) (*apns2.Response, error) { time.Sleep(simulatedLatency) receiveC <- true return nil, nil }).Return(rsp, nil).AnyTimes() apnsSender, err := apns.NewSenderUsingPusher(mPusher, *Config.APNS.AppTopic) if err != nil { logger.Panic("APNS Sender could not be created") } if apnsConn, err := apns.New(router, apnsSender, Config.APNS); err != nil { logger.WithError(err).Error("Error creating APNS connector") } else { modules = append(modules, apnsConn) } } else { logger.Info("APNS: disabled") } return modules } } ================================================ FILE: server/benchmarking_common_test.go ================================================ package server import ( "fmt" "github.com/smancke/guble/client" "github.com/smancke/guble/server/service" "github.com/stretchr/testify/assert" "strconv" "sync" "testing" "time" ) const ( testTopic = "/topic" ) type sender func(c client.Client) error func sendMessageSample(c client.Client) error { return c.Send(testTopic, "test-body", "{id:id}") } type benchParams struct { *testing.B workers int // number of workers subscriptions int // number of subscriptions listening on the topic timeout time.Duration // timeout response clients int // number of clients sender sender // the function that will send the messages sent int // sent messages received int // received messages service *service.Service receiveC chan bool doneC chan struct{} wg sync.WaitGroup start time.Time end time.Time } func (params *benchParams) createClients() (clients []client.Client) { wsURL := "ws://" + params.service.WebServer().GetAddr() + "/stream/user/" for clientID := 0; clientID < params.clients; clientID++ { location := wsURL + strconv.Itoa(clientID) c, err := client.Open(location, "http://localhost/", 1000, true) if err != nil { assert.FailNow(params, "guble client could not connect to server") } clients = append(clients, c) } return } func (params *benchParams) receiveLoop() { for i := 0; i <= params.workers; i++ { go func() { for { select { case <-params.receiveC: params.received++ logger.WithField("received", params.received).Debug("Received a call") params.wg.Done() case <-params.doneC: return } } }() } } func (params *benchParams) String() string { return fmt.Sprintf(` Throughput %.2f messages/second using: %d workers %d subscriptions %s response timeout %d clients `, params.messagesPerSecond(), params.workers, params.subscriptions, params.timeout, params.clients) } func (params *benchParams) ResetTimer() { params.start = time.Now() params.B.ResetTimer() } func (params *benchParams) StopTimer() { params.end = time.Now() params.B.StopTimer() } func (params *benchParams) duration() time.Duration { return params.end.Sub(params.start) } func (params *benchParams) messagesPerSecond() float64 { return float64(params.received) / params.duration().Seconds() } ================================================ FILE: server/benchmarking_fcm_test.go ================================================ package server import ( "bytes" "fmt" "io/ioutil" "net/http" "os" "strings" "testing" "time" log "github.com/Sirupsen/logrus" "github.com/smancke/guble/client" "github.com/smancke/guble/server/connector" "github.com/smancke/guble/server/fcm" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" ) // FCM benchmarks // Default number of clients and subscriptions are 8, for tests that do not // specify this in their name func BenchmarkFCM_1Workers50MilliTimeout(b *testing.B) { params := &benchParams{ B: b, workers: 1, subscriptions: 8, timeout: 50 * time.Millisecond, clients: 8, sender: sendMessageSample, } params.throughputFCM() fmt.Println(params) } func BenchmarkFCM_8Workers50MilliTimeout(b *testing.B) { params := &benchParams{ B: b, workers: 8, subscriptions: 8, timeout: 50 * time.Millisecond, clients: 8, sender: sendMessageSample, } params.throughputFCM() fmt.Println(params) } func BenchmarkFCM_16Workers50MilliTimeout(b *testing.B) { params := &benchParams{ B: b, workers: 16, subscriptions: 8, timeout: 50 * time.Millisecond, clients: 8, sender: sendMessageSample, } params.throughputFCM() fmt.Println(params) } func BenchmarkFCM_1Workers100MilliTimeout(b *testing.B) { params := &benchParams{ B: b, workers: 1, subscriptions: 8, timeout: 100 * time.Millisecond, clients: 8, sender: sendMessageSample, } params.throughputFCM() fmt.Println(params) } func BenchmarkFCM_8Workers100MilliTimeout(b *testing.B) { params := &benchParams{ B: b, workers: 8, subscriptions: 8, timeout: 100 * time.Millisecond, clients: 8, sender: sendMessageSample, } params.throughputFCM() fmt.Println(params) } func BenchmarkFCM_16Workers100MilliTimeout(b *testing.B) { params := &benchParams{ B: b, workers: 16, subscriptions: 8, timeout: 100 * time.Millisecond, clients: 8, sender: sendMessageSample, } params.throughputFCM() fmt.Println(params) } func (params *benchParams) throughputFCM() { defer testutil.ResetDefaultRegistryHealthCheck() a := assert.New(params) dir, errTempDir := ioutil.TempDir("", "guble_benchmarking_fcm_test") a.NoError(errTempDir) *Config.HttpListen = "localhost:0" *Config.KVS = "memory" *Config.MS = "file" *Config.StoragePath = dir *Config.FCM.Enabled = true *Config.FCM.APIKey = "WILL BE OVERWRITTEN" *Config.FCM.Workers = params.workers params.service = StartService() var fcmConn connector.ResponsiveConnector var ok bool for _, iface := range params.service.ModulesSortedByStartOrder() { fcmConn, ok = iface.(connector.ResponsiveConnector) if ok { break } } if fcmConn == nil { a.FailNow("There should be a module of type: FCM Connector") } params.receiveC = make(chan bool) sender, err := fcm.CreateFcmSender(fcm.SuccessFCMResponse, params.receiveC, params.timeout) a.NoError(err) fcmConn.SetSender(sender) urlFormat := fmt.Sprintf("http://%s/fcm/%%d/gcmId%%d/subscribe/%%s", params.service.WebServer().GetAddr()) for i := 1; i <= params.subscriptions; i++ { // create FCM subscription response, errPost := http.Post( fmt.Sprintf(urlFormat, i, i, strings.TrimPrefix(testTopic, "/")), "text/plain", bytes.NewBufferString(""), ) a.NoError(errPost) a.Equal(response.StatusCode, 200) body, errReadAll := ioutil.ReadAll(response.Body) a.NoError(errReadAll) a.Equal("{\"subscribed\":\"/topic\"}", string(body)) } clients := params.createClients() // Report allocations also params.ReportAllocs() expectedMessagesNumber := params.N * params.clients * params.subscriptions logger.WithFields(log.Fields{ "expectedMessagesNumber": expectedMessagesNumber, "N": params.N, }).Info("Expecting messages") params.wg.Add(expectedMessagesNumber) // start the receive loop (a select on receiveC and doneC) params.doneC = make(chan struct{}) params.receiveLoop() params.ResetTimer() // send all messages, or fail on any error for _, cl := range clients { go func(cl client.Client) { for i := 0; i < params.N; i++ { err := params.sender(cl) if err != nil { a.FailNow("Message could not be sent") } params.sent++ } }(cl) } // wait to receive all messages params.wg.Wait() // stop timer after the actual test params.StopTimer() close(params.doneC) a.NoError(params.service.Stop()) params.service = nil close(params.receiveC) errRemove := os.RemoveAll(dir) if errRemove != nil { logger.WithError(errRemove).WithField("module", "testing").Error("Could not remove directory") } } ================================================ FILE: server/benchmarking_fetch_test.go ================================================ package server import ( "github.com/smancke/guble/client" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" "fmt" "io/ioutil" "os" "strconv" "testing" "time" ) func Benchmark_E2E_Fetch_HelloWorld_Messages(b *testing.B) { defer testutil.ResetDefaultRegistryHealthCheck() a := assert.New(b) dir, _ := ioutil.TempDir("", "guble_benchmarking_fetch_test") defer os.RemoveAll(dir) *Config.HttpListen = "localhost:0" *Config.KVS = "memory" *Config.MS = "file" *Config.StoragePath = dir service := StartService() defer service.Stop() time.Sleep(time.Millisecond * 10) // fill the topic location := "ws://" + service.WebServer().GetAddr() + "/stream/user/xy" c, err := client.Open(location, "http://localhost/", 1000, true) a.NoError(err) for i := 1; i <= b.N; i++ { a.NoError(c.Send("/hello", fmt.Sprintf("Hello %v", i), "")) select { case <-c.StatusMessages(): // wait for, but ignore case <-time.After(time.Millisecond * 100): a.Fail("timeout on send notification") return } } start := time.Now() b.ResetTimer() c.WriteRawMessage([]byte("+ /hello 0 1000000")) for i := 1; i <= b.N; i++ { select { case msg := <-c.Messages(): a.Equal(fmt.Sprintf("Hello %v", i), msg.BodyAsString()) case e := <-c.Errors(): a.Fail(string(e.Bytes())) return case <-time.After(time.Second): a.Fail("timeout on message: " + strconv.Itoa(i)) return } } b.StopTimer() end := time.Now() throughput := float64(b.N) / end.Sub(start).Seconds() fmt.Printf("\n\tThroughput: %v/sec (%v message in %v)\n", int(throughput), b.N, end.Sub(start)) } ================================================ FILE: server/benchmarking_test.go ================================================ package server import ( "fmt" "io/ioutil" "log" "os" "testing" "time" "github.com/stretchr/testify/assert" "github.com/smancke/guble/client" "github.com/smancke/guble/protocol" "github.com/smancke/guble/testutil" ) type testgroup struct { t *testing.T groupID int addr string done chan bool messagesToSend int consumer, publisher client.Client topic string } func newTestgroup(t *testing.T, groupID int, addr string, messagesToSend int) *testgroup { return &testgroup{ t: t, groupID: groupID, addr: addr, done: make(chan bool), messagesToSend: messagesToSend, } } func TestThroughput(t *testing.T) { // TODO: We disabled this test because the receiver implementation of fetching messages // should be reimplemented according to the new message store testutil.SkipIfDisabled(t) testutil.SkipIfShort(t) defer testutil.ResetDefaultRegistryHealthCheck() dir, _ := ioutil.TempDir("", "guble_benchmarking_test") *Config.HttpListen = "localhost:0" *Config.KVS = "memory" *Config.MS = "file" *Config.StoragePath = dir service := StartService() testgroupCount := 4 messagesPerGroup := 100 log.Printf("init the %v testgroups", testgroupCount) testgroups := make([]*testgroup, testgroupCount, testgroupCount) for i := range testgroups { testgroups[i] = newTestgroup(t, i, service.WebServer().GetAddr(), messagesPerGroup) } // init test log.Print("init the testgroups") for i := range testgroups { testgroups[i].Init() } defer func() { // cleanup tests log.Print("cleanup the testgroups") for i := range testgroups { testgroups[i].Clean() } service.Stop() os.RemoveAll(dir) }() // start test log.Print("start the testgroups") start := time.Now() for i := range testgroups { go testgroups[i].Start() } log.Print("wait for finishing") for i, test := range testgroups { select { case successFlag := <-test.done: if !successFlag { t.Logf("testgroup %v returned with error", i) t.FailNow() return } case <-time.After(time.Second * 20): t.Log("timeout. testgroups not ready before timeout") t.Fail() return } } end := time.Now() totalMessages := testgroupCount * messagesPerGroup throughput := float64(totalMessages) / end.Sub(start).Seconds() log.Printf("finished! Throughput: %v/sec (%v message in %v)", int(throughput), totalMessages, end.Sub(start)) time.Sleep(time.Second * 1) } func (tg *testgroup) Init() { tg.topic = fmt.Sprintf("/%v-foo", tg.groupID) var err error location := "ws://" + tg.addr + "/stream/user/xy" //location := "ws://gathermon.mancke.net:8080/stream/" //location := "ws://127.0.0.1:8080/stream/" tg.consumer, err = client.Open(location, "http://localhost/", 10, false) if err != nil { panic(err) } tg.publisher, err = client.Open(location, "http://localhost/", 10, false) if err != nil { panic(err) } tg.expectStatusMessage(protocol.SUCCESS_CONNECTED, "You are connected to the server.") tg.consumer.Subscribe(tg.topic) time.Sleep(time.Millisecond * 1) //test.expectStatusMessage(protocol.SUCCESS_SUBSCRIBED_TO, test.topic) } func (tg *testgroup) expectStatusMessage(name string, arg string) { select { case notify := <-tg.consumer.StatusMessages(): assert.Equal(tg.t, name, notify.Name) assert.Equal(tg.t, arg, notify.Arg) case <-time.After(time.Second * 1): tg.t.Logf("[%v] no notification of type %s until timeout", tg.groupID, name) tg.done <- false tg.t.Fail() return } } func (tg *testgroup) Start() { go func() { for i := 0; i < tg.messagesToSend; i++ { body := fmt.Sprintf("Hallo-%d", i) tg.publisher.Send(tg.topic, body, "") } }() for i := 0; i < tg.messagesToSend; i++ { body := fmt.Sprintf("Hallo-%d", i) select { case msg := <-tg.consumer.Messages(): assert.Equal(tg.t, tg.topic, string(msg.Path)) if !assert.Equal(tg.t, body, msg.BodyAsString()) { tg.t.FailNow() tg.done <- false } case msg := <-tg.consumer.Errors(): tg.t.Logf("[%v] received error: %v", tg.groupID, msg) tg.done <- false tg.t.Fail() return case <-time.After(time.Second * 5): tg.t.Logf("[%v] no message received until timeout, expected message %v", tg.groupID, i) tg.done <- false tg.t.Fail() return } } tg.done <- true } func (tg *testgroup) Clean() { tg.consumer.Close() tg.publisher.Close() } ================================================ FILE: server/cluster/cluster.go ================================================ package cluster import ( "io/ioutil" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/store" log "github.com/Sirupsen/logrus" "github.com/hashicorp/memberlist" "errors" "fmt" "net" "strconv" ) var ( ErrNodeNotFound = errors.New("Node not found.") ) // Config is a struct used by the local node when creating and running the guble cluster type Config struct { ID uint8 Host string Port int Remotes []*net.TCPAddr HealthScoreThreshold int } // router interface specify only the methods we require in cluster from the Router // router is an interface used for handling messages in cluster. // It is logically connected to the router.Router interface, by reusing the same func signature. type router interface { HandleMessage(message *protocol.Message) error MessageStore() (store.MessageStore, error) } // Cluster is a struct for managing the `local view` of the guble cluster, as seen by a node. type Cluster struct { // Pointer to a Config struct, based on which the Cluster node is created and runs. Config *Config // Router is used for dispatching messages received by this node. // Should be set after the node is created with New(), and before Start(). Router router name string memberlist *memberlist.Memberlist broadcasts [][]byte numJoins int numLeaves int numUpdates int synchronizer *synchronizer } //New returns a new instance of the cluster, created using the given Config. func New(config *Config) (*Cluster, error) { c := &Cluster{ Config: config, name: fmt.Sprintf("%d", config.ID), } memberlistConfig := memberlist.DefaultLANConfig() memberlistConfig.Name = c.name memberlistConfig.BindAddr = config.Host memberlistConfig.BindPort = config.Port //TODO Cosmin temporarily disabling any logging from memberlist, we might want to enable it again using logrus? memberlistConfig.LogOutput = ioutil.Discard ml, err := memberlist.Create(memberlistConfig) if err != nil { logger.WithField("error", err).Error("Error when creating the internal memberlist of the cluster") return nil, err } c.memberlist = ml memberlistConfig.Delegate = c memberlistConfig.Conflict = c memberlistConfig.Events = c return c, nil } // Start the cluster module. func (cluster *Cluster) Start() error { logger.WithField("remotes", cluster.Config.Remotes).Debug("Starting Cluster") if cluster.Router == nil { errorMessage := "There should be a valid Router already set-up" logger.Error(errorMessage) return errors.New(errorMessage) } synchronizer, err := newSynchronizer(cluster) if err != nil { logger.WithError(err).Error("Error creating cluster synchronizer") return err } cluster.synchronizer = synchronizer num, err := cluster.memberlist.Join(cluster.remotesAsStrings()) if err != nil { logger.WithField("error", err).Error("Error when this node wanted to join the cluster") return err } if num == 0 { errorMessage := "No remote hosts were successfully contacted when this node wanted to join the cluster" logger.WithField("remotes", cluster.remotesAsStrings()).Error(errorMessage) return errors.New(errorMessage) } logger.Debug("Started Cluster") return nil } // Stop the cluster module. func (cluster *Cluster) Stop() error { if cluster.synchronizer != nil { close(cluster.synchronizer.stopC) } return cluster.memberlist.Shutdown() } // Check returns a non-nil error if the health status of the cluster (as seen by this node) is not perfect. func (cluster *Cluster) Check() error { if healthScore := cluster.memberlist.GetHealthScore(); healthScore > cluster.Config.HealthScoreThreshold { errorMessage := "Cluster Health Score is not perfect" logger.WithField("healthScore", healthScore).Error(errorMessage) return errors.New(errorMessage) } return nil } // newMessage returns a *message to be used in broadcasting or sending to a node func (cluster *Cluster) newMessage(t messageType, body []byte) *message { return &message{ NodeID: cluster.Config.ID, Type: t, Body: body, } } func (cluster *Cluster) newEncoderMessage(t messageType, entity encoder) (*message, error) { body, err := entity.encode() if err != nil { return nil, err } return cluster.newMessage(t, body), nil } // BroadcastString broadcasts a string to all the other nodes in the guble cluster func (cluster *Cluster) BroadcastString(sMessage *string) error { logger.WithField("string", sMessage).Debug("BroadcastString") cMessage := &message{ NodeID: cluster.Config.ID, Type: mtStringMessage, Body: []byte(*sMessage), } return cluster.broadcastClusterMessage(cMessage) } // BroadcastMessage broadcasts a guble-protocol-message to all the other nodes in the guble cluster. func (cluster *Cluster) BroadcastMessage(pMessage *protocol.Message) error { logger.WithField("message", pMessage).Debug("BroadcastMessage") cMessage := &message{ NodeID: cluster.Config.ID, Type: mtGubleMessage, Body: pMessage.Bytes(), } return cluster.broadcastClusterMessage(cMessage) } func (cluster *Cluster) broadcastClusterMessage(cMessage *message) error { if cMessage == nil { errorMessage := "Could not broadcast a nil cluster-message" logger.Error(errorMessage) return errors.New(errorMessage) } cMessageBytes, err := cMessage.encode() if err != nil { logger.WithError(err).Error("Could not encode and broadcast cluster-message") return err } for _, node := range cluster.memberlist.Members() { if cluster.name == node.Name { continue } go cluster.sendToNode(node, cMessageBytes) } return nil } func (cluster *Cluster) sendToNode(node *memberlist.Node, msgBytes []byte) error { logger.WithFields(log.Fields{ "node": cluster.Config.ID, "to": node.Name, }).Debug("Sending cluster-message to a node") err := cluster.memberlist.SendToTCP(node, msgBytes) if err != nil { logger.WithFields(log.Fields{ "err": err, "node": node, }).Error("Error sending cluster-message to a node") return err } return nil } func (cluster *Cluster) sendMessageToNode(node *memberlist.Node, cmsg *message) error { logger.WithField("node", node.Name).Debug("Sending message to a node") bytes, err := cmsg.encode() if err != nil { logger.WithError(err).Error("Could not encode and broadcast cluster-message") return err } if err = cluster.memberlist.SendToTCP(node, bytes); err != nil { logger.WithField("node", node.Name).WithError(err).Error("Error send message to node") return err } return nil } func (cluster *Cluster) sendMessageToNodeID(nodeID uint8, cmsg *message) error { node := cluster.GetNodeByID(nodeID) if node == nil { return ErrNodeNotFound } return cluster.sendMessageToNode(node, cmsg) } func (cluster *Cluster) GetNodeByID(id uint8) *memberlist.Node { name := strconv.FormatUint(uint64(id), 10) for _, node := range cluster.memberlist.Members() { if node.Name == name { return node } } return nil } func (cluster *Cluster) remotesAsStrings() (strings []string) { log.WithField("Remotes", cluster.Config.Remotes).Debug("Cluster remotes") for _, remote := range cluster.Config.Remotes { strings = append(strings, remote.IP.String()+":"+strconv.Itoa(remote.Port)) } return } ================================================ FILE: server/cluster/cluster_benchmarking_test.go ================================================ package cluster import ( log "github.com/Sirupsen/logrus" "github.com/hashicorp/memberlist" "fmt" "io/ioutil" "testing" "time" ) func BenchmarkMemberListCluster(b *testing.B) { benchmarkCluster(b, 36, 10*time.Second, 15000) } func benchmarkCluster(b *testing.B, num int, timeoutForAllJoins time.Duration, lowestPort int) { startTime := time.Now() var nodes []*memberlist.Memberlist eventC := make(chan memberlist.NodeEvent, num) addr := "127.0.0.1" var firstMemberName string for i := 0; i < num; i++ { c := memberlist.DefaultLANConfig() port := lowestPort + i c.Name = fmt.Sprintf("%s:%d", addr, port) c.BindAddr = addr c.BindPort = port c.ProbeInterval = 20 * time.Millisecond c.ProbeTimeout = 100 * time.Millisecond c.GossipInterval = 20 * time.Millisecond c.PushPullInterval = 200 * time.Millisecond c.LogOutput = ioutil.Discard if i == 0 { c.Events = &memberlist.ChannelEventDelegate{eventC} firstMemberName = c.Name } newMember, err := memberlist.Create(c) if err != nil { log.WithField("error", err).Fatal("Unexpected error when creating the memberlist") } nodes = append(nodes, newMember) defer newMember.Shutdown() if i > 0 { numContacted, err := newMember.Join([]string{firstMemberName}) if numContacted == 0 || err != nil { log.WithField("error", err).Fatal("Unexpected fatal error when node wanted to join the cluster") } } } if convergence(nodes, num, eventC, timeoutForAllJoins) { endTime := time.Now() log.WithField("durationSeconds", endTime.Sub(startTime).Seconds()).Info("Cluster convergence reached") } b.StartTimer() sendMessagesInCluster(nodes, b.N) b.StopTimer() } func convergence(nodes []*memberlist.Memberlist, num int, eventC chan memberlist.NodeEvent, timeoutForAllJoins time.Duration) bool { breakTimer := time.After(timeoutForAllJoins) numJoins := 0 WAIT: for { select { case e := <-eventC: l := log.WithFields(log.Fields{ "node": *e.Node, "numJoins": numJoins, "numMembers": nodes[0].NumMembers(), }) if e.Event == memberlist.NodeJoin { l.Info("Node join") numJoins++ if numJoins == num { l.Info("All nodes joined") break WAIT } } else { l.Info("Node leave") } case <-breakTimer: break WAIT } } if numJoins != num { log.WithFields(log.Fields{ "joinCounter": numJoins, "num": num, }).Error("Timeout before completing all joins") } convergence := false for !convergence { convergence = true for idx, node := range nodes { numSeenByNode := node.NumMembers() if numSeenByNode != num { log.WithFields(log.Fields{ "index": idx, "expected": num, "actual": numSeenByNode, }).Debug("Wrong number of nodes") convergence = false break } } } return numJoins == num } func sendMessagesInCluster(nodes []*memberlist.Memberlist, numMessages int) { for senderID, node := range nodes { for receiverID, member := range node.Members() { for i := 0; i < numMessages; i++ { message := fmt.Sprintf("Hello from %v to %v !", senderID, receiverID) log.WithField("message", message).Debug("SendToTCP") node.SendToTCP(member, []byte(message)) } } } } ================================================ FILE: server/cluster/cluster_conflict.go ================================================ package cluster import ( log "github.com/Sirupsen/logrus" "github.com/hashicorp/memberlist" ) // ============================================================= // memberlist.ConflictDelegate implementation for cluster struct // ============================================================= func (cluster *Cluster) NotifyConflict(existing, other *memberlist.Node) { logger.WithFields(log.Fields{ "existing": *existing, "other": *other, }).Panic("NotifyConflict") } ================================================ FILE: server/cluster/cluster_delegate.go ================================================ package cluster import ( log "github.com/Sirupsen/logrus" "github.com/smancke/guble/protocol" ) // ====================================================== // memberslist.Delegate implementation for cluster struct // ====================================================== // NotifyMsg is invoked each time a message is received by this node of the cluster; // it decodes and dispatches the messages. func (cluster *Cluster) NotifyMsg(data []byte) { logger.WithField("msgAsBytes", data).Debug("NotifyMsg") cmsg := new(message) err := cmsg.decode(data) if err != nil { logger.WithError(err).Error("Decoding of cluster message failed") return } logger.WithFields(log.Fields{ "senderNodeID": cmsg.NodeID, "type": cmsg.Type, }).Debug("NotifyMsg: Received cluster message") switch cmsg.Type { case mtGubleMessage: cluster.handleGubleMessage(cmsg) case mtSyncPartitions: cluster.handleSyncPartitions(cmsg) case mtSyncMessage: cluster.handleSyncMessage(cmsg) case mtSyncMessageRequest: // cluster node is requesting to receive messages for sync cluster.handleSyncMessageRequest(cmsg) } } func (cluster *Cluster) GetBroadcasts(overhead, limit int) [][]byte { b := cluster.broadcasts cluster.broadcasts = nil return b } func (cluster *Cluster) NodeMeta(limit int) []byte { return nil } func (cluster *Cluster) LocalState(join bool) []byte { return nil } func (cluster *Cluster) MergeRemoteState(s []byte, join bool) {} // handles message received with type `mtGubleMessage` func (cluster *Cluster) handleGubleMessage(cmsg *message) { if cluster.Router == nil { return } message, err := protocol.ParseMessage(cmsg.Body) if err != nil { logger.WithField("err", err).Error("Parsing of guble-message contained in cluster-message failed") return } cluster.Router.HandleMessage(message) } // handles message received with type `mtSyncPartitions` func (cluster *Cluster) handleSyncPartitions(cmsg *message) { logger.WithField("message", cmsg).Debug("Received sync partitions message") // Decode message partitionsSlice := make(partitions, 0) // Decode data into the new slice err := partitionsSlice.decode(cmsg.Body) if err != nil { logger.WithError(err).Error("Error decoding partitions") return } logger.WithFields(log.Fields{ "partitions": partitionsSlice, "nodeID": cmsg.NodeID, }).Debug("Partitions received") // add to synchronizer cluster.synchronizer.sync(cmsg.NodeID, partitionsSlice) } func (cluster *Cluster) handleSyncMessage(cmsg *message) { logger.WithField("cmsg", cmsg).Debug("Handling sync message") err := cluster.synchronizer.syncMessage(cmsg.NodeID, cmsg.Body) if err != nil { logger.WithError(err).Error("Error synchronizing messages") } } func (cluster *Cluster) handleSyncMessageRequest(cmsg *message) { logger.WithField("cmsg", cmsg).Debug("Handling sync message request") err := cluster.synchronizer.messageRequest(cmsg.NodeID, cmsg.Body) if err != nil { logger.WithError(err).Error("Error send synchronization messages") } } ================================================ FILE: server/cluster/cluster_event_delegate.go ================================================ package cluster import ( log "github.com/Sirupsen/logrus" "github.com/hashicorp/memberlist" ) // ========================================================== // memberlist.EventDelegate implementation for cluster struct // ========================================================== func (cluster *Cluster) NotifyJoin(node *memberlist.Node) { cluster.numJoins++ cluster.eventLog(node, "Cluster Node Join") cluster.sendPartitions(node) } func (cluster *Cluster) NotifyLeave(node *memberlist.Node) { cluster.numLeaves++ cluster.eventLog(node, "Cluster Node Leave") } func (cluster *Cluster) NotifyUpdate(node *memberlist.Node) { cluster.numUpdates++ cluster.eventLog(node, "Cluster Node Update") } func (cluster *Cluster) eventLog(node *memberlist.Node, message string) { logger.WithFields(log.Fields{ "node": node.Name, "numJoins": cluster.numJoins, "numLeaves": cluster.numLeaves, "numUpdates": cluster.numUpdates, }).Debug(message) } func (cluster *Cluster) sendPartitions(node *memberlist.Node) { if _, inSync := cluster.synchronizer.inSync(node.Name); inSync { logger.WithField("node", node.Name).Debug("Already in sync with node") return } logger.WithField("node", node.Name).Debug("Sending partitions info") // Send message partitions to the new node store, err := cluster.Router.MessageStore() if err != nil { logger.WithError(err).Error("Error retriving message store to get partitions") return } partitionsSlice := partitionsFromStore(store) // sending partitions data, err := partitionsSlice.encode() if err != nil { logger.WithError(err).Error("Error encoding partitions") return } cmsg := cluster.newMessage(mtSyncPartitions, data) // send message to node err = cluster.sendMessageToNode(node, cmsg) if err != nil { logger.WithField("node", node.Name).WithError(err).Error("Error sending partitions info to node") } } ================================================ FILE: server/cluster/cluster_test.go ================================================ package cluster import ( "io/ioutil" "github.com/smancke/guble/server/store/filestore" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/store" "github.com/hashicorp/go-multierror" "github.com/stretchr/testify/assert" "errors" "net" "testing" "time" ) const basePort = 10000 var ( index = 1 ) func testConfig() (config Config) { remoteAddr := net.TCPAddr{IP: []byte{127, 0, 0, 1}, Port: basePort + index} var remotes []*net.TCPAddr remotes = append(remotes, &remoteAddr) config = Config{ID: uint8(index), Host: "127.0.0.1", Port: basePort + index, Remotes: remotes} index++ return } func testConfigAnother() (config Config) { remoteAddr := net.TCPAddr{IP: []byte{127, 0, 0, 1}, Port: basePort + index - 1} var remotes []*net.TCPAddr remotes = append(remotes, &remoteAddr) config = Config{ID: uint8(index), Host: "127.0.0.1", Port: basePort + index, Remotes: remotes} index++ return } func TestCluster_StartCheckStop(t *testing.T) { a := assert.New(t) conf := testConfig() node, err := New(&conf) a.NoError(err, "No error should be raised when Creating the Cluster") node.Router = newDummyRouter(t) err = node.Start() a.NoError(err, "No error should be raised when Starting the Cluster") err = node.Check() a.NoError(err, "Health-check score of a Cluster with a single node should be OK") err = node.Stop() a.NoError(err, "No error should be raised when Stopping the Cluster") } func TestCluster_BroadcastStringAndMessageAndCheck(t *testing.T) { a := assert.New(t) config1 := testConfig() node1, err := New(&config1) a.NoError(err, "No error should be raised when Creating the Cluster") node1.Router = newDummyRouter(t) //start the cluster node 1 defer node1.Stop() err = node1.Start() a.NoError(err, "No error should be raised when starting node 1 of the Cluster") config2 := testConfigAnother() node2, err := New(&config2) a.NoError(err, "No error should be raised when Creating the Cluster") node2.Router = newDummyRouter(t) //start the cluster node 2 defer node2.Stop() err = node2.Start() a.NoError(err, "No error should be raised when starting node 2 of the Cluster") // Send a String Message str := "TEST" err = node1.BroadcastString(&str) a.NoError(err, "No error should be raised when sending a string to Cluster") // and a protocol message pmsg := protocol.Message{ ID: 1, Path: "/stuff", UserID: "id", ApplicationID: "appId", Time: time.Now().Unix(), HeaderJSON: "{}", Body: []byte("test"), NodeID: 1} err = node1.BroadcastMessage(&pmsg) a.NoError(err, "No error should be raised when sending a protocol message to Cluster") err = node1.Check() a.NoError(err, "Health-check score of a Cluster with 2 nodes should be OK for node 1") err = node2.Check() a.NoError(err, "Health-check score of a Cluster with 2 nodes should be OK for node 2") } func TestCluster_NewShouldReturnErrorWhenPortIsInvalid(t *testing.T) { a := assert.New(t) remoteAddr := net.TCPAddr{IP: []byte{127, 0, 0, 1}, Port: basePort + index - 1} var remotes []*net.TCPAddr remotes = append(remotes, &remoteAddr) index++ config := Config{ID: 1, Host: "localhost", Port: -1, Remotes: remotes} _, err := New(&config) if a.Error(err, "An error was expected when Creating the Cluster") { a.Equal(err, errors.New("Failed to start TCP listener. Err: listen tcp :-1: bind: invalid argument"), "Error should be precisely defined") } } func TestCluster_StartShouldReturnErrorWhenNoRemotes(t *testing.T) { a := assert.New(t) var remotes []*net.TCPAddr index++ config := Config{ID: 1, Host: "localhost", Port: basePort + index - 1, Remotes: remotes} node, err := New(&config) a.NoError(err, "No error should be raised when Creating the Cluster") node.Router = newDummyRouter(t) defer node.Stop() err = node.Start() if a.Error(err, "An error is expected when Starting the Cluster") { a.Equal(err, errors.New("No remote hosts were successfully contacted when this node wanted to join the cluster"), "Error should be precisely defined") } } func TestCluster_StartShouldReturnErrorWhenInvalidRemotes(t *testing.T) { a := assert.New(t) remoteAddr := net.TCPAddr{IP: []byte{127, 0, 0, 1}, Port: 0} var remotes []*net.TCPAddr remotes = append(remotes, &remoteAddr) index++ config := Config{ID: 1, Host: "localhost", Port: basePort + index - 1, Remotes: remotes} node, err := New(&config) a.NoError(err, "No error should be raised when Creating the Cluster") node.Router = newDummyRouter(t) defer node.Stop() err = node.Start() if a.Error(err, "An error is expected when Starting the Cluster") { expected := multierror.Append(errors.New("Failed to join 127.0.0.1: dial tcp 127.0.0.1:0: getsockopt: connection refused")) a.Equal(err, expected, "Error should be precisely defined") } } func TestCluster_StartShouldReturnErrorWhenNoMessageHandler(t *testing.T) { a := assert.New(t) config := testConfig() node, err := New(&config) a.NoError(err, "No error should be raised when Creating the Cluster") defer node.Stop() err = node.Start() if a.Error(err, "An error is expected when Starting the Cluster") { expected := errors.New("There should be a valid Router already set-up") a.Equal(expected, err, "Error should be precisely defined") } } func TestCluster_NotifyMsgShouldSimplyReturnWhenDecodingInvalidMessage(t *testing.T) { a := assert.New(t) config := testConfig() node, err := New(&config) a.NoError(err, "No error should be raised when Creating the Cluster") node.Router = newDummyRouter(t) defer node.Stop() err = node.Start() a.NoError(err, "No error should be raised when Starting the Cluster") node.NotifyMsg([]byte{}) //TODO Cosmin check that HandleMessage is not invoked (i.e. invalid message is not dispatched) } func TestCluster_broadcastClusterMessage(t *testing.T) { a := assert.New(t) config := testConfig() node, err := New(&config) a.NoError(err, "No error should be raised when Creating the Cluster") node.Router = newDummyRouter(t) defer node.Stop() err = node.Start() a.NoError(err, "No error should be raised when Starting the Cluster") err = node.broadcastClusterMessage(nil) if a.Error(err, "An error is expected from broadcastClusterMessage") { expected := errors.New("Could not broadcast a nil cluster-message") a.Equal(err, expected, "Error should be precisely defined") } } type dummyRouter struct { store store.MessageStore } func newDummyRouter(t *testing.T) *dummyRouter { dir, err := ioutil.TempDir("", "guble_cluster_test") assert.NoError(t, err) return &dummyRouter{store: filestore.New(dir)} } func (_ *dummyRouter) HandleMessage(pmsg *protocol.Message) error { return nil } func (d *dummyRouter) MessageStore() (store.MessageStore, error) { return d.store, nil } ================================================ FILE: server/cluster/codec.go ================================================ package cluster import ( log "github.com/Sirupsen/logrus" "github.com/ugorji/go/codec" ) type messageType int var h = &codec.MsgpackHandle{} const ( // Guble protocol.Message mtGubleMessage messageType = iota // A node will send this message type when the body contains the partitions // in it's store with the max message id for each ([]partitions) mtSyncPartitions // Sent this to request a node to give us the next message so we can save it mtSyncMessageRequest // Sent to synchronize a message, contains the message to synchonrize along with // updated partition info mtSyncMessage mtStringMessage ) type encoder interface { encode() ([]byte, error) } type decoder interface { decode(data []byte) error } type message struct { NodeID uint8 Type messageType Body []byte } func (cmsg *message) encode() ([]byte, error) { logger.WithFields(log.Fields{ "nodeID": cmsg.NodeID, "type": cmsg.Type, "body": string(cmsg.Body), }).Debug("Encoding cluster message") return encode(cmsg) } func (cmsg *message) decode(data []byte) error { logger.WithField("data", string(data)).Debug("decode") return decode(cmsg, data) } func encode(entity interface{}) ([]byte, error) { logger.WithField("entity", entity).Debug("Encoding") var bytes []byte encoder := codec.NewEncoderBytes(&bytes, h) err := encoder.Encode(entity) if err != nil { logger.WithField("err", err).Error("Encoding failed") return nil, err } return bytes, nil } func decode(o interface{}, data []byte) error { logger.WithField("data", string(data)).Debug("Decoding") decoder := codec.NewDecoderBytes(data, h) err := decoder.Decode(o) if err != nil { logger.WithField("err", err).Error("Decoding failed") return err } return nil } ================================================ FILE: server/cluster/codec_test.go ================================================ package cluster ================================================ FILE: server/cluster/logger.go ================================================ package cluster import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithFields(log.Fields{ "module": "cluster", }) ================================================ FILE: server/cluster/synchronizer.go ================================================ package cluster import ( "errors" "math" "strconv" "sync" log "github.com/Sirupsen/logrus" "github.com/smancke/guble/server/store" "github.com/ugorji/go/codec" ) const ( syncPartitionsProcessBuffer = 100 ) var ( ErrNodeNotInSync = errors.New("Node not found in syncPartitions list.") ErrMissingSyncPartition = errors.New("Missing sync partition") ) type synchronizer struct { cluster *Cluster store store.MessageStore // map to keep track of nodes and remote partitions and local partitions syncPartitions map[string]*syncPartition nodes map[uint8]partitions // store the lastest info received from a node sync.RWMutex logger *log.Entry stopC chan struct{} } func newSynchronizer(cluster *Cluster) (*synchronizer, error) { store, err := cluster.Router.MessageStore() if err != nil { logger.WithError(err).Error("Error retriving message store for synchronizer") return nil, err } return &synchronizer{ cluster: cluster, store: store, syncPartitions: make(map[string]*syncPartition), nodes: make(map[uint8]partitions), logger: logger.WithField("module", "synchronizer"), stopC: make(chan struct{}), }, nil } // add the partitions received from a node to the nodes list and start the loop // for each partition func (s *synchronizer) sync(nodeID uint8, partitions partitions) { if s.inSyncID(nodeID) { return } s.addNode(nodeID, partitions) } // inSync returns nodeID and a boolean value specifying if this node is already in sync // returns 0 as nodeID and false if the node cannot be parsed // the cluster should not send partitions nor should accept partitions information // from a node that is already in sync func (s *synchronizer) inSync(nodeID string) (uint8, bool) { id, err := strconv.ParseUint(nodeID, 10, 8) if err != nil { logger.WithError(err).Error("Error parsing node ID") return 0, false } ID := uint8(id) return ID, s.inSyncID(ID) } func (s *synchronizer) inSyncID(nodeID uint8) bool { s.RLock() defer s.RUnlock() _, in := s.nodes[nodeID] return in } // addNode adds the node to the state with the missing partitions func (s *synchronizer) addNode(nodeID uint8, partitions partitions) { s.Lock() defer s.Unlock() s.nodes[nodeID] = partitions for _, p := range partitions { sp, exists := s.syncPartitions[p.Name] if !exists { localPartition, err := s.store.Partition(p.Name) if err != nil { logger.WithError(err).WithField("partition", p.Name).Error("Error retrieving local partition") return } localMaxID := localPartition.MaxMessageID() sp = &syncPartition{ synchronizer: s, localPartition: localPartition, localStartMaxID: localMaxID, nodes: make(map[uint8]partition, 1), lastID: localMaxID, processC: make(chan *syncMessage, syncPartitionsProcessBuffer), } } sp.nodes[nodeID] = p s.syncPartitions[p.Name] = sp go sp.run() } } func (s *synchronizer) messageRequest(nodeID uint8, data []byte) error { smr := &syncMessageRequest{} err := smr.decode(data) if err != nil { return err } // start goroutine that will fetch messages from store and send them to the node go s.requestLoop(nodeID, smr) return nil } // requestLoop handles sending messages fetched from the store to the node // that made the request a message sent from here will be received by the syncMessage // method on the other node func (s *synchronizer) requestLoop(nodeID uint8, smr *syncMessageRequest) { s.logger.WithFields(log.Fields{ "requestNodeID": nodeID, "syncMessageRequest": smr, }).Debug("Sending requested messages") req := &store.FetchRequest{ Partition: smr.Partition, StartID: smr.StartID, EndID: smr.EndID, Direction: 1, MessageC: make(chan *store.FetchedMessage, 10), ErrorC: make(chan error), StartC: make(chan int), Count: math.MaxInt32, } s.store.Fetch(req) var fetchedMessage *store.FetchedMessage opened := true for opened { select { case count := <-req.StartC: logger.WithField("count", count). Debug("Receiving messages for sync request from store") case fetchedMessage, opened = <-req.MessageC: if !opened { s.logger.WithField("requestNodeID", nodeID). Debug("Receive channel closed by the store for the sync request") return } // send message to node cmsg, err := s.cluster.newEncoderMessage(mtSyncMessage, &syncMessage{ Partition: smr.Partition, ID: fetchedMessage.ID, Message: fetchedMessage.Message, }) if err != nil { logger.WithError(err). WithField("fetchedMessage", fetchedMessage). Error("Error creating cluster message for fetched message") continue } err = s.cluster.sendMessageToNodeID(nodeID, cmsg) if err != nil { logger.WithError(err). WithField("clusterMesssage", cmsg). Error("Error sending sync message to node") continue } case <-s.stopC: s.logger.WithField("requestNodeID", nodeID).Debug("Stopping synchronization request loop") break } } } // syncMessage received data from another node after we made a request for a set // of messages it will decode the data into a *syncMessage and send it into the // appropriate syncPartition processC channel func (s *synchronizer) syncMessage(nodeID uint8, data []byte) error { if !s.inSyncID(nodeID) { return ErrNodeNotInSync } sm := &syncMessage{} err := sm.decode(data) if err != nil { logger.WithError(err).WithFields(log.Fields{ "nodeID": nodeID, "data": string(data), }).Error("Error decoding sync message received") return err } s.RLock() defer s.RUnlock() syncPartition, exists := s.syncPartitions[sm.Partition] if !exists { return ErrMissingSyncPartition } s.logger.WithFields(log.Fields{ "sm": sm, "syncPartiton": syncPartition, }).Debug("Processing received message") syncPartition.processC <- sm return nil } // keep state of fetching for a partition type syncPartition struct { sync.RWMutex synchronizer *synchronizer localPartition store.MessagePartition localStartMaxID uint64 // max message ID in the local store before the sync request nodes map[uint8]partition // store nodes that have this partition and the info in does nodes lastID uint64 // last fetched message ID // processC channel will receive the message from the cluster and store it in the // it's partition updating the lastID and sending a new request processC chan *syncMessage running bool runningMu sync.RWMutex } // start the loop that will synchronize this partition with other nodes func (sp *syncPartition) run() { if sp.isRunning() { return } sp.setRunning(true) defer sp.setRunning(false) sp.loop() } // syncLoop will start to sync the partition // Each nodes job is to ask the messages it's missing from own store. func (sp *syncPartition) loop() { // send request for the missing messages // get node with the highest message maxID, nodeID := sp.maxIDNode() partitionName := sp.localPartition.Name() cmsg, err := sp.synchronizer.cluster.newEncoderMessage(mtSyncMessageRequest, &syncMessageRequest{ Partition: partitionName, StartID: sp.lastID, EndID: maxID, }) if err != nil { sp.synchronizer.logger.WithError(err).Error("Error creating sync message request") return } err = sp.synchronizer.cluster.sendMessageToNodeID(nodeID, cmsg) if err != nil { sp.synchronizer.logger.WithError(err).WithFields(log.Fields{ "nodeID": nodeID, "StartID": sp.lastID, "EndID": maxID, }).Error("Error sending sync message request to node") return } for { // wait to receive the message // end the loop in case we are stopping the process or we finished synchronizing select { case sm := <-sp.processC: err := sp.synchronizer.store.Store(partitionName, sm.ID, sm.Message) if err != nil { sp.synchronizer.logger.WithError(err). WithField("messageID", sm.ID). Error("Error storing synchronize message") } // end loop if we reached the end sp.lastID = sm.ID if sm.ID >= maxID { return } case <-sp.synchronizer.stopC: return } } } func (sp *syncPartition) maxIDNode() (max uint64, nodeID uint8) { for nid, p := range sp.nodes { if p.MaxID > max { max = p.MaxID nodeID = nid } } return } func (sp *syncPartition) isRunning() bool { sp.runningMu.RLock() defer sp.runningMu.RUnlock() return sp.running } func (sp *syncPartition) setRunning(r bool) { sp.runningMu.Lock() defer sp.runningMu.Unlock() sp.running = r } type partition struct { Name string MaxID uint64 } // syncPartitions will be sent by cluster server to notify the joining server // on the partitions they store type partitions []partition func (p *partitions) encode() ([]byte, error) { var bytes []byte encoder := codec.NewEncoderBytes(&bytes, h) err := encoder.Encode(p) if err != nil { logger.WithError(err).Error("Error encoding partitions") return nil, err } return bytes, nil } // decode will decode the bytes into the receiver `p` in our case // Example: // ``` // p := make(partitions, 0) // err := p.decode(data) // if err != nil { // ... // } // ``` func (p *partitions) decode(data []byte) error { decoder := codec.NewDecoderBytes(data, h) err := decoder.Decode(p) if err != nil { logger.WithError(err).Error("Error decoding partitions data") return err } return nil } func partitionsFromStore(store store.MessageStore) *partitions { messagePartitions, err := store.Partitions() if err != nil { logger.WithError(err).Error("Error retrieving store localPartitions") return nil } localPartitions := make(partitions, 0, len(messagePartitions)) for _, p := range messagePartitions { localPartitions = append(localPartitions, partition{ Name: p.Name(), MaxID: p.MaxMessageID(), }) } return &localPartitions } // send this struct to a node to request the messages between StartID and EndID type syncMessageRequest struct { Partition string StartID uint64 EndID uint64 } func (smr *syncMessageRequest) encode() ([]byte, error) { return encode(smr) } func (smr *syncMessageRequest) decode(data []byte) error { return decode(smr, data) } type syncMessage struct { // Hold updated partition info Partition string ID uint64 Message []byte } func (sm *syncMessage) encode() ([]byte, error) { return encode(sm) } func (sm *syncMessage) decode(data []byte) error { return decode(sm, data) } ================================================ FILE: server/cluster_integration_test.go ================================================ package server import ( log "github.com/Sirupsen/logrus" "github.com/smancke/guble/protocol" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" "testing" "time" ) func Test_Cluster_Subscribe_To_Random_Node(t *testing.T) { testutil.SkipIfShort(t) a := assert.New(t) node1 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8090", NodeID: 1, NodePort: 11000, Remotes: "localhost:11000", }) a.NotNil(node1) defer node1.cleanup(true) node2 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8091", NodeID: 2, NodePort: 11001, Remotes: "localhost:11000", }) a.NotNil(node2) defer node2.cleanup(true) client1, err := node1.client("user1", 10, true) a.NoError(err) err = client1.Subscribe("/foo/bar") a.NoError(err, "Subscribe to first node should work") client1.Close() time.Sleep(50 * time.Millisecond) client1, err = node2.client("user1", 10, true) a.NoError(err, "Connection to second node should return no error") err = client1.Subscribe("/foo/bar") a.NoError(err, "Subscribe to second node should work") client1.Close() } func Test_Cluster_Integration(t *testing.T) { testutil.SkipIfShort(t) defer testutil.ResetDefaultRegistryHealthCheck() a := assert.New(t) node1 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8092", NodeID: 1, NodePort: 11002, Remotes: "localhost:11002", }) a.NotNil(node1) defer node1.cleanup(true) node2 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8093", NodeID: 2, NodePort: 11003, Remotes: "localhost:11002", }) a.NotNil(node2) defer node2.cleanup(true) client1, err := node1.client("user1", 10, false) a.NoError(err) client2, err := node2.client("user2", 10, false) a.NoError(err) err = client2.Subscribe("/testTopic/m") a.NoError(err) client3, err := node1.client("user3", 10, false) a.NoError(err) numSent := 3 for i := 0; i < numSent; i++ { err := client1.Send("/testTopic/m", "body", "{jsonHeader:1}") a.NoError(err) err = client3.Send("/testTopic/m", "body", "{jsonHeader:4}") a.NoError(err) } breakTimer := time.After(3 * time.Second) numReceived := 0 idReceived := make(map[uint64]bool) // see if the correct number of messages arrived at the other client, before timeout is reached WAIT: for { select { case incomingMessage := <-client2.Messages(): numReceived++ logger.WithFields(log.Fields{ "nodeID": incomingMessage.NodeID, "path": incomingMessage.Path, "incomingMsgUserId": incomingMessage.UserID, "headerJson": incomingMessage.HeaderJSON, "body": incomingMessage.BodyAsString(), "numReceived": numReceived, }).Info("Client2 received a message") a.Equal(protocol.Path("/testTopic/m"), incomingMessage.Path) a.Equal("body", incomingMessage.BodyAsString()) a.True(incomingMessage.ID > 0) idReceived[incomingMessage.ID] = true if 2*numReceived == numSent { break WAIT } case <-breakTimer: break WAIT } } } var syncTopic = "/syncTopic" // Test synchronizing messages when a new node is func TestSynchronizerIntegration(t *testing.T) { testutil.SkipIfShort(t) //TODO REACTIVATE THIS AND see if it is working for future testutil.SkipIfDisabled(t) defer testutil.EnableDebugForMethod()() a := assert.New(t) node1 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8094", NodeID: 1, NodePort: 11004, Remotes: "localhost:11004", }) a.NotNil(node1) defer node1.cleanup(true) time.Sleep(2 * time.Second) client1, err := node1.client("client1", 10, true) a.NoError(err) client1.Send(syncTopic, "nobody", "") client1.Send(syncTopic, "nobody", "") client1.Send(syncTopic, "nobody", "") time.Sleep(2 * time.Second) node2 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8095", NodeID: 2, NodePort: 11005, Remotes: "localhost:11004", }) a.NotNil(node2) defer node2.cleanup(true) client2, err := node2.client("client2", 10, true) a.NoError(err) cmd := &protocol.Cmd{ Name: protocol.CmdReceive, Arg: syncTopic + " -3", } doneC := make(chan struct{}) go func() { for { select { case m := <-client2.Messages(): log.WithField("m", m).Error("Message received from first cluster") case e := <-client2.Errors(): log.WithField("clientError", e).Error("Client error") case status := <-client2.StatusMessages(): log.WithField("status", status).Error("Client status messasge") case <-doneC: return } } }() log.Error(string(cmd.Bytes())) client2.WriteRawMessage(cmd.Bytes()) time.Sleep(10 * time.Second) close(doneC) } ================================================ FILE: server/config.go ================================================ package server import ( "github.com/Bogh/gcm" log "github.com/Sirupsen/logrus" "gopkg.in/alecthomas/kingpin.v2" "fmt" "net" "runtime" "strconv" "strings" "github.com/smancke/guble/server/apns" "github.com/smancke/guble/server/fcm" "github.com/smancke/guble/server/sms" ) const ( defaultHttpListen = ":8080" defaultHealthEndpoint = "/admin/healthcheck" defaultMetricsEndpoint = "/admin/metrics" defaultKVSBackend = "file" defaultMSBackend = "file" defaultStoragePath = "/var/lib/guble" defaultNodePort = "10000" development = "dev" integration = "int" preproduction = "pre" production = "prod" memProfile = "mem" cpuProfile = "cpu" blockProfile = "block" ) var ( defaultFCMEndpoint = gcm.GcmSendEndpoint defaultFCMMetrics = true defaultAPNSMetrics = true defaultSMSMetrics = true environments = []string{development, integration, preproduction, production} ) type ( // PostgresConfig is used for configuring the Postgresql connection. PostgresConfig struct { Host *string Port *int User *string Password *string DbName *string } // ClusterConfig is used for configuring the cluster component. ClusterConfig struct { NodeID *uint8 NodePort *int Remotes *tcpAddrList } // GubleConfig is used for configuring Guble server (including its modules / connectors). GubleConfig struct { Log *string EnvName *string HttpListen *string KVS *string MS *string StoragePath *string HealthEndpoint *string MetricsEndpoint *string Profile *string Postgres PostgresConfig FCM fcm.Config APNS apns.Config SMS sms.Config Cluster ClusterConfig } ) var ( parsed = false // Config is the active configuration of guble (used when starting-up the server) Config = &GubleConfig{ Log: kingpin.Flag("log", "Log level"). Default(log.ErrorLevel.String()). Envar("GUBLE_LOG"). Enum(logLevels()...), EnvName: kingpin.Flag("env", `Name of the environment on which the application is running`). Default(development). Envar("GUBLE_ENV"). Enum(environments...), HttpListen: kingpin.Flag("http", `The address to for the HTTP server to listen on (format: "[Host]:Port")`). Default(defaultHttpListen). Envar("GUBLE_HTTP_LISTEN"). String(), KVS: kingpin.Flag("kvs", "The storage backend for the key-value store to use : file | memory | postgres "). Default(defaultKVSBackend). Envar("GUBLE_KVS"). String(), MS: kingpin.Flag("ms", "The message storage backend : file | memory"). Default(defaultMSBackend). HintOptions("file", "memory"). Envar("GUBLE_MS"). String(), StoragePath: kingpin.Flag("storage-path", "The path for storing messages and key-value data if 'file' is selected"). Default(defaultStoragePath). Envar("GUBLE_STORAGE_PATH"). ExistingDir(), HealthEndpoint: kingpin.Flag("health-endpoint", `The health endpoint to be used by the HTTP server (value for disabling it: "")`). Default(defaultHealthEndpoint). Envar("GUBLE_HEALTH_ENDPOINT"). String(), MetricsEndpoint: kingpin.Flag("metrics-endpoint", `The metrics endpoint to be used by the HTTP server (value for disabling it: "")`). Default(defaultMetricsEndpoint). Envar("GUBLE_METRICS_ENDPOINT"). String(), Profile: kingpin.Flag("profile", `The profiler to be used (default: none): mem | cpu | block`). Default(""). Envar("GUBLE_PROFILE"). Enum("mem", "cpu", "block", ""), Postgres: PostgresConfig{ Host: kingpin.Flag("pg-host", "The PostgreSQL hostname"). Default("localhost"). Envar("GUBLE_PG_HOST"). String(), Port: kingpin.Flag("pg-port", "The PostgreSQL port"). Default("5432"). Envar("GUBLE_PG_PORT"). Int(), User: kingpin.Flag("pg-user", "The PostgreSQL user"). Default("guble"). Envar("GUBLE_PG_USER"). String(), Password: kingpin.Flag("pg-password", "The PostgreSQL password"). Default("guble"). Envar("GUBLE_PG_PASSWORD"). String(), DbName: kingpin.Flag("pg-dbname", "The PostgreSQL database name"). Default("guble"). Envar("GUBLE_PG_DBNAME"). String(), }, FCM: fcm.Config{ Enabled: kingpin.Flag("fcm", "Enable the Google Firebase Cloud Messaging connector"). Envar("GUBLE_FCM"). Bool(), APIKey: kingpin.Flag("fcm-api-key", "The Google API Key for Google Firebase Cloud Messaging"). Envar("GUBLE_FCM_API_KEY"). String(), Workers: kingpin.Flag("fcm-workers", "The number of workers handling traffic with Firebase Cloud Messaging (default: number of CPUs)"). Default(strconv.Itoa(runtime.NumCPU())). Envar("GUBLE_FCM_WORKERS"). Int(), Endpoint: kingpin.Flag("fcm-endpoint", "The Google Firebase Cloud Messaging endpoint"). Default(defaultFCMEndpoint). Envar("GUBLE_FCM_ENDPOINT"). String(), Prefix: kingpin.Flag("fcm-prefix", "The FCM prefix / endpoint"). Envar("GUBLE_FCM_PREFIX"). Default("/fcm/"). String(), IntervalMetrics: &defaultFCMMetrics, }, APNS: apns.Config{ Enabled: kingpin.Flag("apns", "Enable the APNS connector (by default, in Development mode)"). Envar("GUBLE_APNS"). Bool(), Production: kingpin.Flag("apns-production", "Enable the APNS connector in Production mode"). Envar("GUBLE_APNS_PRODUCTION"). Bool(), CertificateFileName: kingpin.Flag("apns-cert-file", "The APNS certificate file name"). Envar("GUBLE_APNS_CERT_FILE"). String(), CertificateBytes: kingpin.Flag("apns-cert-bytes", "The APNS certificate bytes, as a string of hex-values"). Envar("GUBLE_APNS_CERT_BYTES"). HexBytes(), CertificatePassword: kingpin.Flag("apns-cert-password", "The APNS certificate password"). Envar("GUBLE_APNS_CERT_PASSWORD"). String(), AppTopic: kingpin.Flag("apns-app-topic", "The APNS topic (as used by the mobile application)"). Envar("GUBLE_APNS_APP_TOPIC"). String(), Prefix: kingpin.Flag("apns-prefix", "The APNS prefix / endpoint"). Envar("GUBLE_APNS_PREFIX"). Default("/apns/"). String(), Workers: kingpin.Flag("apns-workers", "The number of workers handling traffic with APNS (default: number of CPUs)"). Default(strconv.Itoa(runtime.NumCPU())). Envar("GUBLE_APNS_WORKERS"). Int(), IntervalMetrics: &defaultAPNSMetrics, }, Cluster: ClusterConfig{ NodeID: kingpin.Flag("node-id", "(cluster mode) This guble node's own ID: a strictly positive integer number which must be unique in cluster"). Envar("GUBLE_NODE_ID").Uint8(), NodePort: kingpin.Flag("node-port", "(cluster mode) This guble node's own local port: a strictly positive integer number"). Default(defaultNodePort).Envar("GUBLE_NODE_PORT").Int(), Remotes: tcpAddrListParser(kingpin.Flag("remotes", `(cluster mode) The list of TCP addresses of some other guble nodes (format: "IP:port")`). Envar("GUBLE_NODE_REMOTES")), }, SMS: sms.Config{ Enabled: kingpin.Flag("sms", "Enable the SMS gateway)"). Envar("GUBLE_SMS"). Bool(), APIKey: kingpin.Flag("sms-api-key", "The Nexmo API Key for Sending sms"). Envar("GUBLE_SMS_API_KEY"). String(), APISecret: kingpin.Flag("sms-api-secret", "The Nexmo API Secret for Sending sms"). Envar("GUBLE_SMS_API_SECRET"). String(), SMSTopic: kingpin.Flag("sms-topic", "The topic for sms route"). Envar("GUBLE_SMS_TOPIC"). Default(sms.SMSDefaultTopic). String(), Workers: kingpin.Flag("sms-workers", "The number of workers handling traffic with Nexmo sms endpoint(default: number of CPUs)"). Default(strconv.Itoa(runtime.NumCPU())). Envar("GUBLE_SMS_WORKERS"). Int(), IntervalMetrics: &defaultSMSMetrics, }, } ) func logLevels() (levels []string) { for _, level := range log.AllLevels { levels = append(levels, level.String()) } return } // parseConfig parses the flags from command line. Must be used before accessing the config. // If there are missing or invalid arguments it will exit the application // and display a message. func parseConfig() { if parsed { return } kingpin.Parse() parsed = true return } type tcpAddrList []*net.TCPAddr func (h *tcpAddrList) Set(value string) error { addresses := strings.Split(value, " ") // Reset the list also, when running tests we add to the same list and is incorrect *h = make(tcpAddrList, 0) for _, addr := range addresses { logger.WithField("addr", addr).Info("value") parts := strings.SplitN(addr, ":", 2) if len(parts) != 2 { return fmt.Errorf("expected HEADER:VALUE got '%s'", addr) } addr, err := net.ResolveTCPAddr("tcp", addr) if err != nil { return err } *h = append(*h, addr) } return nil } func tcpAddrListParser(s kingpin.Settings) (target *tcpAddrList) { slist := make(tcpAddrList, 0) s.SetValue(&slist) return &slist } func (h *tcpAddrList) String() string { return "" } ================================================ FILE: server/config_test.go ================================================ package server import ( "github.com/stretchr/testify/assert" "net" "os" "testing" ) func TestParsingOfEnvironmentVariables(t *testing.T) { a := assert.New(t) originalArgs := os.Args os.Args = []string{os.Args[0]} defer func() { os.Args = originalArgs }() // given: some environment variables os.Setenv("GUBLE_HTTP_LISTEN", "http_listen") defer os.Unsetenv("GUBLE_HTTP_LISTEN") os.Setenv("GUBLE_LOG", "debug") defer os.Unsetenv("GUBLE_LOG") os.Setenv("GUBLE_ENV", "dev") defer os.Unsetenv("GUBLE_ENV") os.Setenv("GUBLE_PROFILE", "mem") defer os.Unsetenv("GUBLE_PROFILE") os.Setenv("GUBLE_KVS", "kvs-backend") defer os.Unsetenv("GUBLE_KVS") os.Setenv("GUBLE_STORAGE_PATH", os.TempDir()) defer os.Unsetenv("GUBLE_STORAGE_PATH") os.Setenv("GUBLE_HEALTH_ENDPOINT", "health_endpoint") defer os.Unsetenv("GUBLE_HEALTH_ENDPOINT") os.Setenv("GUBLE_METRICS_ENDPOINT", "metrics_endpoint") defer os.Unsetenv("GUBLE_METRICS_ENDPOINT") os.Setenv("GUBLE_MS", "ms-backend") defer os.Unsetenv("GUBLE_MS") os.Setenv("GUBLE_FCM", "true") defer os.Unsetenv("GUBLE_FCM") os.Setenv("GUBLE_FCM_API_KEY", "fcm-api-key") defer os.Unsetenv("GUBLE_FCM_API_KEY") os.Setenv("GUBLE_FCM_WORKERS", "3") defer os.Unsetenv("GUBLE_FCM_WORKERS") os.Setenv("GUBLE_APNS", "true") defer os.Unsetenv("GUBLE_APNS") os.Setenv("GUBLE_APNS_PRODUCTION", "true") defer os.Unsetenv("GUBLE_APNS_PRODUCTION") os.Setenv("GUBLE_APNS_CERT_BYTES", "00ff") defer os.Unsetenv("GUBLE_APNS_CERT_BYTES") os.Setenv("GUBLE_APNS_CERT_PASSWORD", "rotten") defer os.Unsetenv("GUBLE_APNS_CERT_PASSWORD") os.Setenv("GUBLE_APNS_APP_TOPIC", "com.myapp") defer os.Unsetenv("GUBLE_APNS_APP_TOPIC") os.Setenv("GUBLE_NODE_ID", "1") defer os.Unsetenv("GUBLE_NODE_ID") os.Setenv("GUBLE_NODE_PORT", "10000") defer os.Unsetenv("GUBLE_NODE_PORT") os.Setenv("GUBLE_PG_HOST", "pg-host") defer os.Unsetenv("GUBLE_PG_HOST") os.Setenv("GUBLE_PG_PORT", "5432") defer os.Unsetenv("GUBLE_PG_PORT") os.Setenv("GUBLE_PG_USER", "pg-user") defer os.Unsetenv("GUBLE_PG_USER") os.Setenv("GUBLE_PG_PASSWORD", "pg-password") defer os.Unsetenv("GUBLE_PG_PASSWORD") os.Setenv("GUBLE_PG_DBNAME", "pg-dbname") defer os.Unsetenv("GUBLE_PG_DBNAME") os.Setenv("GUBLE_NODE_REMOTES", "127.0.0.1:8080 127.0.0.1:20002") defer os.Unsetenv("GUBLE_NODE_REMOTES") // when we parse the arguments from environment variables parseConfig() // then the parsed parameters are correctly set assertArguments(a) } func TestParsingArgs(t *testing.T) { a := assert.New(t) originalArgs := os.Args defer func() { os.Args = originalArgs }() // given: a command line os.Args = []string{os.Args[0], "--http", "http_listen", "--env", "dev", "--log", "debug", "--profile", "mem", "--storage-path", os.TempDir(), "--kvs", "kvs-backend", "--ms", "ms-backend", "--health-endpoint", "health_endpoint", "--metrics-endpoint", "metrics_endpoint", "--fcm", "--fcm-api-key", "fcm-api-key", "--fcm-workers", "3", "--apns", "--apns-production", "--apns-cert-bytes", "00ff", "--apns-cert-password", "rotten", "--apns-app-topic", "com.myapp", "--node-id", "1", "--node-port", "10000", "--pg-host", "pg-host", "--pg-port", "5432", "--pg-user", "pg-user", "--pg-password", "pg-password", "--pg-dbname", "pg-dbname", "--remotes", "127.0.0.1:8080 127.0.0.1:20002", } // when we parse the arguments from command-line flags parseConfig() // then the parsed parameters are correctly set assertArguments(a) } func assertArguments(a *assert.Assertions) { a.Equal("http_listen", *Config.HttpListen) a.Equal("kvs-backend", *Config.KVS) a.Equal(os.TempDir(), *Config.StoragePath) a.Equal("ms-backend", *Config.MS) a.Equal("health_endpoint", *Config.HealthEndpoint) a.Equal("metrics_endpoint", *Config.MetricsEndpoint) a.Equal(true, *Config.FCM.Enabled) a.Equal("fcm-api-key", *Config.FCM.APIKey) a.Equal(3, *Config.FCM.Workers) a.Equal(true, *Config.APNS.Enabled) a.Equal(true, *Config.APNS.Production) a.Equal([]byte{0, 255}, *Config.APNS.CertificateBytes) a.Equal("rotten", *Config.APNS.CertificatePassword) a.Equal("com.myapp", *Config.APNS.AppTopic) a.Equal(uint8(1), *Config.Cluster.NodeID) a.Equal(10000, *Config.Cluster.NodePort) a.Equal("pg-host", *Config.Postgres.Host) a.Equal(5432, *Config.Postgres.Port) a.Equal("pg-user", *Config.Postgres.User) a.Equal("pg-password", *Config.Postgres.Password) a.Equal("pg-dbname", *Config.Postgres.DbName) a.Equal("debug", *Config.Log) a.Equal("dev", *Config.EnvName) a.Equal("mem", *Config.Profile) assertClusterRemotes(a) } func assertClusterRemotes(a *assert.Assertions) { ip1, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:8080") ip2, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:20002") ipList := make(tcpAddrList, 0) ipList = append(ipList, ip1) ipList = append(ipList, ip2) a.Equal(ipList, *Config.Cluster.Remotes) } ================================================ FILE: server/connector/connector.go ================================================ package connector import ( "context" "encoding/json" "fmt" "net/http" "sync" "time" log "github.com/Sirupsen/logrus" "github.com/gorilla/mux" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/service" ) const ( DefaultWorkers = 1 SubstitutePath = "/substitute/" ) var ( TopicParam = "topic" ConnectorParam = "connector" ) type Sender interface { // Send takes a Request and returns the response or error Send(Request) (interface{}, error) } type SenderSetter interface { Sender() Sender SetSender(Sender) } type Metadata struct { Latency time.Duration } type ResponseHandler interface { // HandleResponse handles the response+error (returned by a Sender) HandleResponse(Request, interface{}, *Metadata, error) error } type ResponseHandlerSetter interface { ResponseHandler() ResponseHandler SetResponseHandler(ResponseHandler) } type Runner interface { Run(Subscriber) } type Connector interface { service.Startable service.Stopable service.Endpoint SenderSetter ResponseHandlerSetter Runner Manager() Manager Context() context.Context } type ResponsiveConnector interface { Connector ResponseHandler } type connector struct { config Config sender Sender handler ResponseHandler manager Manager queue Queue router router.Router mux *mux.Router ctx context.Context cancel context.CancelFunc logger *log.Entry wg sync.WaitGroup } type Config struct { Name string Schema string Prefix string URLPattern string Workers int } func NewConnector(router router.Router, sender Sender, config Config) (Connector, error) { kvs, err := router.KVStore() if err != nil { return nil, err } if config.Workers <= 0 { config.Workers = DefaultWorkers } c := &connector{ config: config, sender: sender, manager: NewManager(config.Schema, kvs), queue: NewQueue(sender, config.Workers), router: router, logger: logger.WithField("name", config.Name), } c.initMuxRouter() return c, nil } func (c *connector) initMuxRouter() { muxRouter := mux.NewRouter() baseRouter := muxRouter.PathPrefix(c.GetPrefix()).Subrouter() baseRouter.Methods(http.MethodGet).HandlerFunc(c.GetList) baseRouter.Methods(http.MethodPost).PathPrefix(SubstitutePath).HandlerFunc(c.Substitute) subRouter := baseRouter.Path(c.config.URLPattern).Subrouter() subRouter.Methods(http.MethodPost).HandlerFunc(c.Post) subRouter.Methods(http.MethodDelete).HandlerFunc(c.Delete) c.mux = muxRouter } func (c *connector) ServeHTTP(w http.ResponseWriter, req *http.Request) { c.logger.WithFields(log.Fields{ "path": req.URL.RequestURI(), }).Info("Handling HTTP request") c.mux.ServeHTTP(w, req) } func (c *connector) GetPrefix() string { return c.config.Prefix } // GetList returns list of subscribers func (c *connector) GetList(w http.ResponseWriter, req *http.Request) { query := req.URL.Query() filters := make(map[string]string, len(query)) for key, value := range query { if len(value) == 0 { continue } filters[key] = value[0] } c.logger.WithField("filters", filters).Info("Get list of subscriptions") if len(filters) == 0 { http.Error(w, `{"error":"Missing filters"}`, http.StatusBadRequest) return } subscribers := c.manager.Filter(filters) topics := make([]string, 0, len(subscribers)) for _, s := range subscribers { topics = append(topics, s.Route().Path.RemovePrefixSlash()) } encoder := json.NewEncoder(w) err := encoder.Encode(topics) if err != nil { http.Error(w, "Error encoding data.", http.StatusInternalServerError) c.logger.WithField("error", err.Error()).Error("Error encoding data.") return } } // Post creates a new subscriber func (c *connector) Post(w http.ResponseWriter, req *http.Request) { params := mux.Vars(req) c.logger.WithField("params", params).Info("POST subscription") topic, ok := params[TopicParam] if !ok { fmt.Fprintf(w, "Missing topic parameter.") return } delete(params, TopicParam) params[ConnectorParam] = c.config.Name c.logger.WithField("params", params).WithField("topic", topic).Info("Creating subscription") subscriber, err := c.manager.Create(protocol.Path("/"+topic), params) if err != nil { if err == ErrSubscriberExists { fmt.Fprintf(w, `{"error":"subscription already exists"}`) } else { http.Error(w, fmt.Sprintf(`{"error":"unknown error: %s"}`, err.Error()), http.StatusInternalServerError) } return } go c.Run(subscriber) c.logger.WithField("topic", topic).Info("Subscription created") fmt.Fprintf(w, `{"subscribed":"/%v"}`, topic) } // Delete removes a subscriber func (c *connector) Delete(w http.ResponseWriter, req *http.Request) { params := mux.Vars(req) c.logger.WithField("params", params).Info("DELETE subscription") topic, ok := params[TopicParam] if !ok { fmt.Fprintf(w, "Missing topic parameter.") return } delete(params, TopicParam) params[ConnectorParam] = c.config.Name c.logger.WithField("params", params).WithField("topic", topic).Info("Finding subscription to delete it") subscriber := c.manager.Find(GenerateKey("/"+topic, params)) if subscriber == nil { http.Error(w, `{"error":"subscription not found"}`, http.StatusNotFound) return } c.logger.WithField("params", params).WithField("topic", topic).Info("Deleting subscription") err := c.manager.Remove(subscriber) if err != nil { http.Error(w, fmt.Sprintf(`{"error":"unknown error: %s"}`, err.Error()), http.StatusInternalServerError) return } fmt.Fprintf(w, `{"unsubscribed":"/%v"}`, topic) } func (c *connector) Substitute(w http.ResponseWriter, req *http.Request) { s := new(substitution) err := json.NewDecoder(req.Body).Decode(&s) if err != nil { http.Error(w, fmt.Sprintf(`{"error":"json body could not be decoded: %s"}`, err.Error()), http.StatusBadRequest) return } if !s.isValid() { http.Error(w, `{"error":"not all required values were supplied"}`, http.StatusBadRequest) return } filters := map[string]string{} filters[s.FieldName] = s.OldValue subscribers := c.manager.Filter(filters) totalSubscribersUpdated := 0 for _, sub := range subscribers { sub.Route().Set(s.FieldName, s.NewValue) err = c.manager.Update(sub) if err != nil { http.Error(w, fmt.Sprintf(`{"error":"%s"}`, err.Error()), http.StatusInternalServerError) return } totalSubscribersUpdated++ } c.logger.WithField("subscribers", subscribers).WithField("req", s).Info("Substituted subscriber info ") fmt.Fprintf(w, `{"modified":"%d"}`, totalSubscribersUpdated) } // Start will run start all current subscriptions and workers to process the messages func (c *connector) Start() error { c.queue.Start() c.logger.Info("Starting connector") c.ctx, c.cancel = context.WithCancel(context.Background()) c.logger.Info("Loading subscriptions") err := c.manager.Load() if err != nil { return err } c.logger.Info("Starting subscriptions") for _, s := range c.manager.List() { go c.Run(s) } c.logger.Info("Started connector") return nil } func (c *connector) Run(s Subscriber) { c.wg.Add(1) defer c.wg.Done() var provideErr error go func() { err := s.Route().Provide(c.router, true) if err != nil { // cancel subscription loop if there is an error on the provider provideErr = err s.Cancel() } }() err := s.Loop(c.ctx, c.queue) if err != nil && provideErr == nil { c.logger.WithField("error", err.Error()).Error("Error returned by subscriber loop") // if context cancelled loop then unsubscribe the route from router // in case it's been subscribed if err == context.Canceled { c.router.Unsubscribe(s.Route()) return } // If Route channel closed try restarting if err == ErrRouteChannelClosed { c.restart(s) return } } if provideErr != nil { // TODO Bogdan Treat errors where a subscription provide fails c.logger.WithField("error", provideErr.Error()).Error("Route provide error") // Router closed the route, try restart if provideErr == router.ErrInvalidRoute { c.restart(s) return } // Router module is stopping, exit the process if _, ok := provideErr.(*router.ModuleStoppingError); ok { return } } } func (c *connector) restart(s Subscriber) error { s.Cancel() err := s.Reset() if err != nil { c.logger.WithField("err", err.Error()).Error("Error reseting subscriber") return err } go c.Run(s) return nil } // Stop the connector (the context, the queue, the subscription loops) func (c *connector) Stop() error { c.logger.Info("Stopping connector") c.cancel() c.queue.Stop() c.wg.Wait() c.logger.Info("Stopped connector") return nil } func (c *connector) Manager() Manager { return c.manager } func (c *connector) Context() context.Context { return c.ctx } func (c *connector) ResponseHandler() ResponseHandler { return c.handler } func (c *connector) SetResponseHandler(handler ResponseHandler) { c.handler = handler c.queue.SetResponseHandler(handler) } func (c *connector) Sender() Sender { return c.sender } func (c *connector) SetSender(s Sender) { c.sender = s c.queue.SetSender(s) } ================================================ FILE: server/connector/connector_test.go ================================================ package connector import ( "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/router" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" ) type connectorMocks struct { router *MockRouter sender *MockSender queue *MockQueue manager *MockManager kvstore *MockKVStore } // Ensure the subscription is started when posting func TestConnector_PostSubscription(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) recorder := httptest.NewRecorder() conn, mocks := getTestConnector(t, Config{ Name: "test", Schema: "test", Prefix: "/connector/", URLPattern: "/{device_token}/{user_id}/{topic:.*}", }, true, false) mocks.manager.EXPECT().Load().Return(nil) mocks.manager.EXPECT().List().Return(make([]Subscriber, 0)) err := conn.Start() a.NoError(err) defer conn.Stop() subscriber := NewMockSubscriber(testutil.MockCtrl) mocks.manager.EXPECT().Create(gomock.Eq(protocol.Path("/topic1")), gomock.Eq(router.RouteParams{ "device_token": "device1", "user_id": "user1", "connector": "test", })).Return(subscriber, nil) subscriber.EXPECT().Loop(gomock.Any(), gomock.Any()) r := router.NewRoute(router.RouteConfig{ Path: protocol.Path("topic1"), RouteParams: router.RouteParams{ "device_token": "device1", "user_id": "user1", }, }) subscriber.EXPECT().Route().Return(r) mocks.router.EXPECT().Subscribe(gomock.Eq(r)).Return(r, nil) req, err := http.NewRequest(http.MethodPost, "/connector/device1/user1/topic1", strings.NewReader("")) a.NoError(err) conn.ServeHTTP(recorder, req) a.Equal(`{"subscribed":"/topic1"}`, recorder.Body.String()) time.Sleep(100 * time.Millisecond) } func TestConnector_PostSubscriptionNoMocks(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) recorder := httptest.NewRecorder() conn, mocks := getTestConnector(t, Config{ Name: "name", Schema: "schema", Prefix: "/connector/", URLPattern: "/{device_token}/{user_id}/{topic:.*}", }, false, false) entriesC := make(chan [2]string) mocks.kvstore.EXPECT().Iterate(gomock.Eq("schema"), gomock.Eq("")).Return(entriesC) close(entriesC) mocks.kvstore.EXPECT().Put(gomock.Eq("schema"), gomock.Eq(GenerateKey("/topic1", map[string]string{ "device_token": "device1", "user_id": "user1", "connector": "name", })), gomock.Any()) mocks.router.EXPECT().Subscribe(gomock.Any()) err := conn.Start() a.NoError(err) defer conn.Stop() req, err := http.NewRequest(http.MethodPost, "/connector/device1/user1/topic1", strings.NewReader("")) a.NoError(err) conn.ServeHTTP(recorder, req) a.Equal(`{"subscribed":"/topic1"}`, recorder.Body.String()) time.Sleep(100 * time.Millisecond) } func TestConnector_DeleteSubscription(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) recorder := httptest.NewRecorder() conn, mocks := getTestConnector(t, Config{ Name: "name", Schema: "schema", Prefix: "/connector/", URLPattern: "/{device_token}/{user_id}/{topic:.*}", }, true, false) subscriber := NewMockSubscriber(testutil.MockCtrl) mocks.manager.EXPECT().Find(gomock.Eq(GenerateKey("/topic1", map[string]string{ "device_token": "device1", "user_id": "user1", "connector": "name", }))).Return(subscriber) mocks.manager.EXPECT().Remove(subscriber).Return(nil) req, err := http.NewRequest(http.MethodDelete, "/connector/device1/user1/topic1", strings.NewReader("")) a.NoError(err) conn.ServeHTTP(recorder, req) a.Equal(`{"unsubscribed":"/topic1"}`, recorder.Body.String()) time.Sleep(200 * time.Millisecond) } func TestConnector_GetList_And_Getters(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) recorder := httptest.NewRecorder() conn, mocks := getTestConnector(t, Config{ Name: "test", Schema: "test", Prefix: "/connector/", URLPattern: "/{device_token}/{user_id}/{topic:.*}", }, true, false) req, err := http.NewRequest(http.MethodGet, "/connector/", strings.NewReader("")) a.NoError(err) conn.ServeHTTP(recorder, req) expectedJSON := `{"error":"Missing filters"}` a.JSONEq(expectedJSON, recorder.Body.String()) a.Equal(http.StatusBadRequest, recorder.Code) a.Equal("/connector/", conn.GetPrefix()) a.Equal(mocks.manager, conn.Manager()) a.Equal(nil, conn.ResponseHandler()) } func TestConnector_GetListWithFilters(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) recorder := httptest.NewRecorder() conn, mocks := getTestConnector(t, Config{ Name: "test", Schema: "test", Prefix: "/connector/", URLPattern: "/{device_token}/{user_id}/{topic:.*}", }, true, false) mocks.manager.EXPECT().Filter(gomock.Eq(map[string]string{ "filter1": "value1", "filter2": "value2", })).Return([]Subscriber{}) req, err := http.NewRequest( http.MethodGet, "/connector/?filter1=value1&filter2=value2", strings.NewReader("")) a.NoError(err) conn.ServeHTTP(recorder, req) } func TestConnector_StartWithSubscriptions(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) conn, mocks := getTestConnector(t, Config{ Name: "test", Schema: "test", Prefix: "/connector/", URLPattern: "/{device_token}/{user_id}/{topic:.*}", }, false, false) entriesC := make(chan [2]string) mocks.kvstore.EXPECT().Iterate(gomock.Eq("test"), gomock.Eq("")).Return(entriesC) close(entriesC) mocks.kvstore.EXPECT().Put(gomock.Any(), gomock.Any(), gomock.Any()).Times(4) err := conn.Start() a.NoError(err) routes := make([]*router.Route, 0, 4) mocks.router.EXPECT().Subscribe(gomock.Any()).Do(func(r *router.Route) (*router.Route, error) { routes = append(routes, r) return r, nil }).Times(4) // create subscriptions createSubscriptions(t, conn, 4) time.Sleep(100 * time.Millisecond) mocks.sender.EXPECT().Send(gomock.Any()).Return(nil, nil).Times(4) // send message in route channel for i, r := range routes { r.Deliver(&protocol.Message{ ID: uint64(i), Path: protocol.Path("/topic"), Body: []byte("test body"), }, true) } time.Sleep(100 * time.Millisecond) err = conn.Stop() a.NoError(err) } func TestConnector_Substitute(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) conn, mocks := getTestConnector(t, Config{ Name: "test", Schema: "test", Prefix: "/connector/", URLPattern: "/{device_token}/{user_id}/{topic:.*}", }, false, false) entriesC := make(chan [2]string) mocks.kvstore.EXPECT().Iterate(gomock.Eq("test"), gomock.Eq("")).Return(entriesC) close(entriesC) mocks.kvstore.EXPECT().Put(gomock.Any(), gomock.Any(), gomock.Any()).Times(4) err := conn.Start() a.NoError(err) routes := make([]*router.Route, 0, 4) mocks.router.EXPECT().Subscribe(gomock.Any()).Do(func(r *router.Route) (*router.Route, error) { routes = append(routes, r) return r, nil }).Times(4) // create subscriptions createSubscriptions(t, conn, 4) time.Sleep(100 * time.Millisecond) postBody := `{ "field":"device_token", "old_value":"device1", "new_value":"asgasgasgagasgaasg2" } ` mocks.kvstore.EXPECT().Put(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) recorder := httptest.NewRecorder() req, err := http.NewRequest(http.MethodPost, "/connector"+SubstitutePath, strings.NewReader(postBody)) conn.ServeHTTP(recorder, req) a.Equal(http.StatusOK, recorder.Code) a.Equal(`{"modified":"1"}`, recorder.Body.String()) } func TestConnector_SubstituteWrongPostBody(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) conn, mocks := getTestConnector(t, Config{ Name: "test", Schema: "test", Prefix: "/connector/", URLPattern: "/{device_token}/{user_id}/{topic:.*}", }, false, false) entriesC := make(chan [2]string) mocks.kvstore.EXPECT().Iterate(gomock.Eq("test"), gomock.Eq("")).Return(entriesC) close(entriesC) mocks.kvstore.EXPECT().Put(gomock.Any(), gomock.Any(), gomock.Any()).Times(4) err := conn.Start() a.NoError(err) routes := make([]*router.Route, 0, 4) mocks.router.EXPECT().Subscribe(gomock.Any()).Do(func(r *router.Route) (*router.Route, error) { routes = append(routes, r) return r, nil }).Times(4) // create subscriptions createSubscriptions(t, conn, 4) time.Sleep(100 * time.Millisecond) postBody := `{ "field_invalid":"device_token", "old_value":"device1", "new_value":"asgasgasgagasgaasg2" } ` recorder := httptest.NewRecorder() req, err := http.NewRequest(http.MethodPost, "/connector"+SubstitutePath, strings.NewReader(postBody)) conn.ServeHTTP(recorder, req) a.Equal(http.StatusBadRequest, recorder.Code) } func createSubscriptions(t *testing.T, conn Connector, count int) { a := assert.New(t) for i := 1; i <= count; i++ { recorder := httptest.NewRecorder() r, err := http.NewRequest( http.MethodPost, fmt.Sprintf("/connector/device%d/user%d/topic", i, i), strings.NewReader("")) a.NoError(err) conn.ServeHTTP(recorder, r) a.Equal(200, recorder.Code) a.Equal(`{"subscribed":"/topic"}`, recorder.Body.String()) } } func TestConnector_StartAndStopWithoutSubscribers(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) conn, mocks := getTestConnector(t, Config{ Name: "test", Schema: "test", Prefix: "/connector/", URLPattern: "/{device_token}/{user_id}/{topic:.*}", }, true, true) mocks.manager.EXPECT().Load().Return(nil) mocks.manager.EXPECT().List().Return(nil) mocks.queue.EXPECT().Start().Return(nil) mocks.queue.EXPECT().Stop().Return(nil) err := conn.Start() a.NoError(err) err = conn.Stop() a.NoError(err) } func getTestConnector(t *testing.T, config Config, mockManager bool, mockQueue bool) (Connector, *connectorMocks) { a := assert.New(t) var ( mManager *MockManager mQueue *MockQueue ) mKVS := NewMockKVStore(testutil.MockCtrl) mRouter := NewMockRouter(testutil.MockCtrl) mRouter.EXPECT().KVStore().Return(mKVS, nil).AnyTimes() mSender := NewMockSender(testutil.MockCtrl) conn, err := NewConnector(mRouter, mSender, config) a.NoError(err) if mockManager { mManager = NewMockManager(testutil.MockCtrl) conn.(*connector).manager = mManager } if mockQueue { mQueue = NewMockQueue(testutil.MockCtrl) conn.(*connector).queue = mQueue } return conn, &connectorMocks{ mRouter, mSender, mQueue, mManager, mKVS, } } ================================================ FILE: server/connector/logger.go ================================================ package connector import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithField("module", "connector") ================================================ FILE: server/connector/manager.go ================================================ package connector import ( "sync" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/router" ) type Manager interface { Load() error List() []Subscriber Filter(map[string]string) []Subscriber Find(string) Subscriber Exists(string) bool Create(protocol.Path, router.RouteParams) (Subscriber, error) Add(Subscriber) error Update(Subscriber) error Remove(Subscriber) error } type manager struct { sync.RWMutex schema string kvstore kvstore.KVStore subscribers map[string]Subscriber } func NewManager(schema string, kvstore kvstore.KVStore) Manager { return &manager{ schema: schema, kvstore: kvstore, subscribers: make(map[string]Subscriber, 0), } } func (m *manager) Load() error { // try to load s from kvstore entries := m.kvstore.Iterate(m.schema, "") for e := range entries { subscriber, err := NewSubscriberFromJSON([]byte(e[1])) if err != nil { return err } m.subscribers[subscriber.Key()] = subscriber } return nil } func (m *manager) Find(key string) Subscriber { m.RLock() defer m.RUnlock() if s, exists := m.subscribers[key]; exists { return s } return nil } func (m *manager) Create(topic protocol.Path, params router.RouteParams) (Subscriber, error) { key := GenerateKey(string(topic), params) //TODO MARIAN remove this logs when 503 is done. logger.WithField("key", key).Info("Create generated key") if m.Exists(key) { logger.WithField("key", key).Info("Create key exists already") return nil, ErrSubscriberExists } s := NewSubscriber(topic, params, 0) logger.WithField("subscriber", s).Info("Created new subscriber") err := m.Add(s) if err != nil { logger.WithField("error", err.Error()).Info("Create Manager Add failed") return nil, err } logger.Info("Create finished") return s, nil } func (m *manager) List() []Subscriber { m.RLock() defer m.RUnlock() l := make([]Subscriber, 0, len(m.subscribers)) for _, s := range m.subscribers { l = append(l, s) } return l } func (m *manager) Filter(filters map[string]string) (subscribers []Subscriber) { m.RLock() defer m.RUnlock() for _, s := range m.subscribers { if s.Filter(filters) { subscribers = append(subscribers, s) } } return } func (m *manager) Add(s Subscriber) error { logger.WithField("subscriber", s).WithField("lock", m.RWMutex).Info("Add subscriber started") if m.Exists(s.Key()) { return ErrSubscriberExists } if err := m.updateStore(s); err != nil { return err } m.putSubscriber(s) logger.WithField("subscriber", s).Info("Add subscriber finished") return nil } func (m *manager) Update(s Subscriber) error { logger.WithField("subscriber", s).Info("Update subscriber started") if !m.Exists(s.Key()) { return ErrSubscriberDoesNotExist } err := m.updateStore(s) if err != nil { return err } m.putSubscriber(s) logger.WithField("subscriber", s).Info("Update subscriber finished") return nil } func (m *manager) putSubscriber(s Subscriber) { m.Lock() defer m.Unlock() m.subscribers[s.Key()] = s } func (m *manager) deleteSubscriber(s Subscriber) { m.Lock() defer m.Unlock() delete(m.subscribers, s.Key()) } func (m *manager) Exists(key string) bool { m.RLock() defer m.RUnlock() _, found := m.subscribers[key] return found } func (m *manager) Remove(s Subscriber) error { logger.WithField("subscriber", s).Info("Remove subscriber started") m.cancelSubscriber(s) if !m.Exists(s.Key()) { return ErrSubscriberDoesNotExist } err := m.removeStore(s) if err != nil { return err } m.deleteSubscriber(s) logger.WithField("subscriber", s).Info("Remove subscriber finished") return nil } func (m *manager) cancelSubscriber(s Subscriber) { m.Lock() defer m.Unlock() s.Cancel() } func (m *manager) updateStore(s Subscriber) error { data, err := s.Encode() if err != nil { return err } //TODO MARIAN also remove this logs. logger.WithField("subscriber", s).Info("UpdateStore") return m.kvstore.Put(m.schema, s.Key(), data) } func (m *manager) removeStore(s Subscriber) error { //TODO MARIAN also remove this logs. logger.WithField("subscriber", s).Info("RemoveStore") return m.kvstore.Delete(m.schema, s.Key()) } ================================================ FILE: server/connector/manager_test.go ================================================ package connector ================================================ FILE: server/connector/mocks_connector_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/connector (interfaces: Connector,Sender,ResponseHandler,Manager,Queue,Request,Subscriber) package connector import ( "context" "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/router" "net/http" ) // Mock of Connector interface type MockConnector struct { ctrl *gomock.Controller recorder *_MockConnectorRecorder } // Recorder for MockConnector (not exported) type _MockConnectorRecorder struct { mock *MockConnector } func NewMockConnector(ctrl *gomock.Controller) *MockConnector { mock := &MockConnector{ctrl: ctrl} mock.recorder = &_MockConnectorRecorder{mock} return mock } func (_m *MockConnector) EXPECT() *_MockConnectorRecorder { return _m.recorder } func (_m *MockConnector) Context() context.Context { ret := _m.ctrl.Call(_m, "Context") ret0, _ := ret[0].(context.Context) return ret0 } func (_mr *_MockConnectorRecorder) Context() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Context") } func (_m *MockConnector) GetPrefix() string { ret := _m.ctrl.Call(_m, "GetPrefix") ret0, _ := ret[0].(string) return ret0 } func (_mr *_MockConnectorRecorder) GetPrefix() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetPrefix") } func (_m *MockConnector) Manager() Manager { ret := _m.ctrl.Call(_m, "Manager") ret0, _ := ret[0].(Manager) return ret0 } func (_mr *_MockConnectorRecorder) Manager() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Manager") } func (_m *MockConnector) ResponseHandler() ResponseHandler { ret := _m.ctrl.Call(_m, "ResponseHandler") ret0, _ := ret[0].(ResponseHandler) return ret0 } func (_mr *_MockConnectorRecorder) ResponseHandler() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "ResponseHandler") } func (_m *MockConnector) Run(_param0 Subscriber) { _m.ctrl.Call(_m, "Run", _param0) } func (_mr *_MockConnectorRecorder) Run(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Run", arg0) } func (_m *MockConnector) Sender() Sender { ret := _m.ctrl.Call(_m, "Sender") ret0, _ := ret[0].(Sender) return ret0 } func (_mr *_MockConnectorRecorder) Sender() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Sender") } func (_m *MockConnector) ServeHTTP(_param0 http.ResponseWriter, _param1 *http.Request) { _m.ctrl.Call(_m, "ServeHTTP", _param0, _param1) } func (_mr *_MockConnectorRecorder) ServeHTTP(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "ServeHTTP", arg0, arg1) } func (_m *MockConnector) SetResponseHandler(_param0 ResponseHandler) { _m.ctrl.Call(_m, "SetResponseHandler", _param0) } func (_mr *_MockConnectorRecorder) SetResponseHandler(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "SetResponseHandler", arg0) } func (_m *MockConnector) SetSender(_param0 Sender) { _m.ctrl.Call(_m, "SetSender", _param0) } func (_mr *_MockConnectorRecorder) SetSender(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "SetSender", arg0) } func (_m *MockConnector) Start() error { ret := _m.ctrl.Call(_m, "Start") ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockConnectorRecorder) Start() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Start") } func (_m *MockConnector) Stop() error { ret := _m.ctrl.Call(_m, "Stop") ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockConnectorRecorder) Stop() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Stop") } // Mock of Sender interface type MockSender struct { ctrl *gomock.Controller recorder *_MockSenderRecorder } // Recorder for MockSender (not exported) type _MockSenderRecorder struct { mock *MockSender } func NewMockSender(ctrl *gomock.Controller) *MockSender { mock := &MockSender{ctrl: ctrl} mock.recorder = &_MockSenderRecorder{mock} return mock } func (_m *MockSender) EXPECT() *_MockSenderRecorder { return _m.recorder } func (_m *MockSender) Send(_param0 Request) (interface{}, error) { ret := _m.ctrl.Call(_m, "Send", _param0) ret0, _ := ret[0].(interface{}) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockSenderRecorder) Send(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Send", arg0) } // Mock of ResponseHandler interface type MockResponseHandler struct { ctrl *gomock.Controller recorder *_MockResponseHandlerRecorder } // Recorder for MockResponseHandler (not exported) type _MockResponseHandlerRecorder struct { mock *MockResponseHandler } func NewMockResponseHandler(ctrl *gomock.Controller) *MockResponseHandler { mock := &MockResponseHandler{ctrl: ctrl} mock.recorder = &_MockResponseHandlerRecorder{mock} return mock } func (_m *MockResponseHandler) EXPECT() *_MockResponseHandlerRecorder { return _m.recorder } func (_m *MockResponseHandler) HandleResponse(_param0 Request, _param1 interface{}, _param2 *Metadata, _param3 error) error { ret := _m.ctrl.Call(_m, "HandleResponse", _param0, _param1, _param2, _param3) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockResponseHandlerRecorder) HandleResponse(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "HandleResponse", arg0, arg1, arg2, arg3) } // Mock of Manager interface type MockManager struct { ctrl *gomock.Controller recorder *_MockManagerRecorder } // Recorder for MockManager (not exported) type _MockManagerRecorder struct { mock *MockManager } func NewMockManager(ctrl *gomock.Controller) *MockManager { mock := &MockManager{ctrl: ctrl} mock.recorder = &_MockManagerRecorder{mock} return mock } func (_m *MockManager) EXPECT() *_MockManagerRecorder { return _m.recorder } func (_m *MockManager) Add(_param0 Subscriber) error { ret := _m.ctrl.Call(_m, "Add", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockManagerRecorder) Add(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Add", arg0) } func (_m *MockManager) Create(_param0 protocol.Path, _param1 router.RouteParams) (Subscriber, error) { ret := _m.ctrl.Call(_m, "Create", _param0, _param1) ret0, _ := ret[0].(Subscriber) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockManagerRecorder) Create(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Create", arg0, arg1) } func (_m *MockManager) Exists(_param0 string) bool { ret := _m.ctrl.Call(_m, "Exists", _param0) ret0, _ := ret[0].(bool) return ret0 } func (_mr *_MockManagerRecorder) Exists(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Exists", arg0) } func (_m *MockManager) Filter(_param0 map[string]string) []Subscriber { ret := _m.ctrl.Call(_m, "Filter", _param0) ret0, _ := ret[0].([]Subscriber) return ret0 } func (_mr *_MockManagerRecorder) Filter(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Filter", arg0) } func (_m *MockManager) Find(_param0 string) Subscriber { ret := _m.ctrl.Call(_m, "Find", _param0) ret0, _ := ret[0].(Subscriber) return ret0 } func (_mr *_MockManagerRecorder) Find(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Find", arg0) } func (_m *MockManager) List() []Subscriber { ret := _m.ctrl.Call(_m, "List") ret0, _ := ret[0].([]Subscriber) return ret0 } func (_mr *_MockManagerRecorder) List() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "List") } func (_m *MockManager) Load() error { ret := _m.ctrl.Call(_m, "Load") ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockManagerRecorder) Load() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Load") } func (_m *MockManager) Remove(_param0 Subscriber) error { ret := _m.ctrl.Call(_m, "Remove", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockManagerRecorder) Remove(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Remove", arg0) } func (_m *MockManager) Update(_param0 Subscriber) error { ret := _m.ctrl.Call(_m, "Update", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockManagerRecorder) Update(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Update", arg0) } // Mock of Queue interface type MockQueue struct { ctrl *gomock.Controller recorder *_MockQueueRecorder } // Recorder for MockQueue (not exported) type _MockQueueRecorder struct { mock *MockQueue } func NewMockQueue(ctrl *gomock.Controller) *MockQueue { mock := &MockQueue{ctrl: ctrl} mock.recorder = &_MockQueueRecorder{mock} return mock } func (_m *MockQueue) EXPECT() *_MockQueueRecorder { return _m.recorder } func (_m *MockQueue) Push(_param0 Request) error { ret := _m.ctrl.Call(_m, "Push", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockQueueRecorder) Push(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Push", arg0) } func (_m *MockQueue) ResponseHandler() ResponseHandler { ret := _m.ctrl.Call(_m, "ResponseHandler") ret0, _ := ret[0].(ResponseHandler) return ret0 } func (_mr *_MockQueueRecorder) ResponseHandler() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "ResponseHandler") } func (_m *MockQueue) Sender() Sender { ret := _m.ctrl.Call(_m, "Sender") ret0, _ := ret[0].(Sender) return ret0 } func (_mr *_MockQueueRecorder) Sender() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Sender") } func (_m *MockQueue) SetResponseHandler(_param0 ResponseHandler) { _m.ctrl.Call(_m, "SetResponseHandler", _param0) } func (_mr *_MockQueueRecorder) SetResponseHandler(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "SetResponseHandler", arg0) } func (_m *MockQueue) SetSender(_param0 Sender) { _m.ctrl.Call(_m, "SetSender", _param0) } func (_mr *_MockQueueRecorder) SetSender(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "SetSender", arg0) } func (_m *MockQueue) Start() error { ret := _m.ctrl.Call(_m, "Start") ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockQueueRecorder) Start() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Start") } func (_m *MockQueue) Stop() error { ret := _m.ctrl.Call(_m, "Stop") ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockQueueRecorder) Stop() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Stop") } // Mock of Request interface type MockRequest struct { ctrl *gomock.Controller recorder *_MockRequestRecorder } // Recorder for MockRequest (not exported) type _MockRequestRecorder struct { mock *MockRequest } func NewMockRequest(ctrl *gomock.Controller) *MockRequest { mock := &MockRequest{ctrl: ctrl} mock.recorder = &_MockRequestRecorder{mock} return mock } func (_m *MockRequest) EXPECT() *_MockRequestRecorder { return _m.recorder } func (_m *MockRequest) Message() *protocol.Message { ret := _m.ctrl.Call(_m, "Message") ret0, _ := ret[0].(*protocol.Message) return ret0 } func (_mr *_MockRequestRecorder) Message() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Message") } func (_m *MockRequest) Subscriber() Subscriber { ret := _m.ctrl.Call(_m, "Subscriber") ret0, _ := ret[0].(Subscriber) return ret0 } func (_mr *_MockRequestRecorder) Subscriber() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Subscriber") } // Mock of Subscriber interface type MockSubscriber struct { ctrl *gomock.Controller recorder *_MockSubscriberRecorder } // Recorder for MockSubscriber (not exported) type _MockSubscriberRecorder struct { mock *MockSubscriber } func NewMockSubscriber(ctrl *gomock.Controller) *MockSubscriber { mock := &MockSubscriber{ctrl: ctrl} mock.recorder = &_MockSubscriberRecorder{mock} return mock } func (_m *MockSubscriber) EXPECT() *_MockSubscriberRecorder { return _m.recorder } func (_m *MockSubscriber) Cancel() { _m.ctrl.Call(_m, "Cancel") } func (_mr *_MockSubscriberRecorder) Cancel() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Cancel") } func (_m *MockSubscriber) Encode() ([]byte, error) { ret := _m.ctrl.Call(_m, "Encode") ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockSubscriberRecorder) Encode() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Encode") } func (_m *MockSubscriber) Filter(_param0 map[string]string) bool { ret := _m.ctrl.Call(_m, "Filter", _param0) ret0, _ := ret[0].(bool) return ret0 } func (_mr *_MockSubscriberRecorder) Filter(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Filter", arg0) } func (_m *MockSubscriber) Key() string { ret := _m.ctrl.Call(_m, "Key") ret0, _ := ret[0].(string) return ret0 } func (_mr *_MockSubscriberRecorder) Key() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Key") } func (_m *MockSubscriber) Loop(_param0 context.Context, _param1 Queue) error { ret := _m.ctrl.Call(_m, "Loop", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockSubscriberRecorder) Loop(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Loop", arg0, arg1) } func (_m *MockSubscriber) Reset() error { ret := _m.ctrl.Call(_m, "Reset") ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockSubscriberRecorder) Reset() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Reset") } func (_m *MockSubscriber) Route() *router.Route { ret := _m.ctrl.Call(_m, "Route") ret0, _ := ret[0].(*router.Route) return ret0 } func (_mr *_MockSubscriberRecorder) Route() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Route") } func (_m *MockSubscriber) SetLastID(_param0 uint64) { _m.ctrl.Call(_m, "SetLastID", _param0) } func (_mr *_MockSubscriberRecorder) SetLastID(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "SetLastID", arg0) } ================================================ FILE: server/connector/mocks_kvstore_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/kvstore (interfaces: KVStore) package connector import ( gomock "github.com/golang/mock/gomock" ) // Mock of KVStore interface type MockKVStore struct { ctrl *gomock.Controller recorder *_MockKVStoreRecorder } // Recorder for MockKVStore (not exported) type _MockKVStoreRecorder struct { mock *MockKVStore } func NewMockKVStore(ctrl *gomock.Controller) *MockKVStore { mock := &MockKVStore{ctrl: ctrl} mock.recorder = &_MockKVStoreRecorder{mock} return mock } func (_m *MockKVStore) EXPECT() *_MockKVStoreRecorder { return _m.recorder } func (_m *MockKVStore) Delete(_param0 string, _param1 string) error { ret := _m.ctrl.Call(_m, "Delete", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockKVStoreRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Delete", arg0, arg1) } func (_m *MockKVStore) Get(_param0 string, _param1 string) ([]byte, bool, error) { ret := _m.ctrl.Call(_m, "Get", _param0, _param1) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(bool) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } func (_mr *_MockKVStoreRecorder) Get(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Get", arg0, arg1) } func (_m *MockKVStore) Iterate(_param0 string, _param1 string) chan [2]string { ret := _m.ctrl.Call(_m, "Iterate", _param0, _param1) ret0, _ := ret[0].(chan [2]string) return ret0 } func (_mr *_MockKVStoreRecorder) Iterate(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Iterate", arg0, arg1) } func (_m *MockKVStore) IterateKeys(_param0 string, _param1 string) chan string { ret := _m.ctrl.Call(_m, "IterateKeys", _param0, _param1) ret0, _ := ret[0].(chan string) return ret0 } func (_mr *_MockKVStoreRecorder) IterateKeys(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "IterateKeys", arg0, arg1) } func (_m *MockKVStore) Put(_param0 string, _param1 string, _param2 []byte) error { ret := _m.ctrl.Call(_m, "Put", _param0, _param1, _param2) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockKVStoreRecorder) Put(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Put", arg0, arg1, arg2) } ================================================ FILE: server/connector/mocks_router_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/router (interfaces: Router) package connector import ( "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/cluster" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" ) // Mock of Router interface type MockRouter struct { ctrl *gomock.Controller recorder *_MockRouterRecorder } // Recorder for MockRouter (not exported) type _MockRouterRecorder struct { mock *MockRouter } func NewMockRouter(ctrl *gomock.Controller) *MockRouter { mock := &MockRouter{ctrl: ctrl} mock.recorder = &_MockRouterRecorder{mock} return mock } func (_m *MockRouter) EXPECT() *_MockRouterRecorder { return _m.recorder } func (_m *MockRouter) AccessManager() (auth.AccessManager, error) { ret := _m.ctrl.Call(_m, "AccessManager") ret0, _ := ret[0].(auth.AccessManager) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) AccessManager() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "AccessManager") } func (_m *MockRouter) Cluster() *cluster.Cluster { ret := _m.ctrl.Call(_m, "Cluster") ret0, _ := ret[0].(*cluster.Cluster) return ret0 } func (_mr *_MockRouterRecorder) Cluster() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Cluster") } func (_m *MockRouter) Done() <-chan bool { ret := _m.ctrl.Call(_m, "Done") ret0, _ := ret[0].(<-chan bool) return ret0 } func (_mr *_MockRouterRecorder) Done() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Done") } func (_m *MockRouter) Fetch(_param0 *store.FetchRequest) error { ret := _m.ctrl.Call(_m, "Fetch", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockRouter) GetSubscribers(_param0 string) ([]byte, error) { ret := _m.ctrl.Call(_m, "GetSubscribers", _param0) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) GetSubscribers(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetSubscribers", arg0) } func (_m *MockRouter) HandleMessage(_param0 *protocol.Message) error { ret := _m.ctrl.Call(_m, "HandleMessage", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) HandleMessage(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "HandleMessage", arg0) } func (_m *MockRouter) KVStore() (kvstore.KVStore, error) { ret := _m.ctrl.Call(_m, "KVStore") ret0, _ := ret[0].(kvstore.KVStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) KVStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "KVStore") } func (_m *MockRouter) MessageStore() (store.MessageStore, error) { ret := _m.ctrl.Call(_m, "MessageStore") ret0, _ := ret[0].(store.MessageStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) MessageStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MessageStore") } func (_m *MockRouter) Subscribe(_param0 *router.Route) (*router.Route, error) { ret := _m.ctrl.Call(_m, "Subscribe", _param0) ret0, _ := ret[0].(*router.Route) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) Subscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Subscribe", arg0) } func (_m *MockRouter) Unsubscribe(_param0 *router.Route) { _m.ctrl.Call(_m, "Unsubscribe", _param0) } func (_mr *_MockRouterRecorder) Unsubscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Unsubscribe", arg0) } ================================================ FILE: server/connector/queue.go ================================================ package connector import ( "sync" "time" log "github.com/Sirupsen/logrus" ) // Queue is an interface modeling a task-queue (it is started and more Requests can be pushed to it, and finally it is stopped after all requests are handled). type Queue interface { ResponseHandlerSetter SenderSetter Start() error Push(request Request) error Stop() error } type queue struct { sender Sender responseHandler ResponseHandler requestsC chan Request nWorkers int metrics bool wg sync.WaitGroup } // NewQueue returns a new Queue (not started). func NewQueue(sender Sender, nWorkers int) Queue { q := &queue{ sender: sender, nWorkers: nWorkers, metrics: true, } return q } func (q *queue) SetResponseHandler(rh ResponseHandler) { q.responseHandler = rh } func (q *queue) ResponseHandler() ResponseHandler { return q.responseHandler } func (q *queue) Sender() Sender { return q.sender } func (q *queue) SetSender(s Sender) { q.sender = s } // Start a fixed number of goroutines to handle requests and responses w.r.t. external push-notification services. func (q *queue) Start() error { q.requestsC = make(chan Request) for i := 1; i <= q.nWorkers; i++ { go q.worker(i) } return nil } func (q *queue) worker(i int) { logger.WithField("worker", i).Info("starting queue worker") for request := range q.requestsC { q.handle(request) } } func (q *queue) handle(request Request) { q.wg.Add(1) defer q.wg.Done() var beforeSend time.Time if q.metrics { beforeSend = time.Now() } response, err := q.sender.Send(request) if q.responseHandler != nil { var metadata *Metadata if q.metrics { metadata = &Metadata{time.Since(beforeSend)} } err = q.responseHandler.HandleResponse(request, response, metadata, err) if err != nil { logger.WithFields(log.Fields{ "error": err.Error(), "subscriber": request.Subscriber(), "message": request.Message(), }).Error("error handling connector response") } } else if err == nil { logger.WithField("response", response).Info("no response handler was set") } else { logger.WithField("error", err.Error()).Error("error while sending, and no response handler was set") } } func (q *queue) Push(request Request) error { // recover if the channel been closed defer func() { if r := recover(); r != nil { switch x := r.(type) { case error: logger.WithError(x).Error("recovered from error") default: panic(r) } } }() q.requestsC <- request return nil } func (q *queue) Stop() error { close(q.requestsC) q.wg.Wait() return nil } ================================================ FILE: server/connector/request.go ================================================ package connector import "github.com/smancke/guble/protocol" type Request interface { Subscriber() Subscriber Message() *protocol.Message } type request struct { subscriber Subscriber message *protocol.Message } func NewRequest(s Subscriber, m *protocol.Message) Request { return &request{s, m} } func (r *request) Subscriber() Subscriber { return r.subscriber } func (r *request) Message() *protocol.Message { return r.message } ================================================ FILE: server/connector/subscriber.go ================================================ package connector import ( "context" "crypto/sha1" "encoding/hex" "encoding/json" "errors" "fmt" "io" "sort" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" ) var ( ErrSubscriberExists = errors.New("Subscriber exists.") ErrSubscriberDoesNotExist = errors.New("Subscriber does not exist.") ErrRouteChannelClosed = errors.New("Subscriber route channel has been closed.") ) type Subscriber interface { // Reset will recreate the route inside the subscribe with the information stored // in the subscriber data Reset() error Key() string Route() *router.Route Filter(map[string]string) bool Loop(context.Context, Queue) error SetLastID(ID uint64) Cancel() Encode() ([]byte, error) } type SubscriberData struct { Topic protocol.Path Params router.RouteParams LastID uint64 } func (sd *SubscriberData) newRoute() *router.Route { var fr *store.FetchRequest if sd.LastID > 0 { fr = store.NewFetchRequest(sd.Topic.Partition(), sd.LastID, 0, store.DirectionForward, -1) } return router.NewRoute(router.RouteConfig{ Path: sd.Topic, RouteParams: sd.Params, FetchRequest: fr, }) } type subscriber struct { data SubscriberData key string route *router.Route cancel context.CancelFunc } func NewSubscriber(topic protocol.Path, params router.RouteParams, lastID uint64) Subscriber { return NewSubscriberFromData(SubscriberData{ Topic: topic, Params: params, LastID: lastID, }) } func NewSubscriberFromData(data SubscriberData) Subscriber { return &subscriber{ data: data, route: data.newRoute(), } } func NewSubscriberFromJSON(data []byte) (Subscriber, error) { sd := SubscriberData{} err := json.Unmarshal(data, &sd) if err != nil { return nil, err } return NewSubscriberFromData(sd), nil } func (s *subscriber) String() string { return s.Key() } func (s *subscriber) Reset() error { s.route = s.data.newRoute() s.cancel = nil return nil } func (s *subscriber) Key() string { if s.key == "" { s.key = GenerateKey(string(s.data.Topic), s.data.Params) } return s.key } func (s *subscriber) Filter(filters map[string]string) bool { return s.route.Filter(filters) } func (s *subscriber) Route() *router.Route { return s.route } func (s *subscriber) Loop(ctx context.Context, q Queue) error { var m *protocol.Message sCtx, cancel := context.WithCancel(ctx) s.cancel = cancel defer func() { s.cancel = nil }() opened := true for opened { select { case m, opened = <-s.route.MessagesChannel(): if !opened { break } q.Push(NewRequest(s, m)) case <-sCtx.Done(): // If the parent context is still running then only this subscriber context // has been cancelled if ctx.Err() == nil { return sCtx.Err() } return nil } } //TODO Cosmin Bogdan returning this error can mean 2 things: overflow of route's channel, or intentional stopping of router / gubled. return ErrRouteChannelClosed } func (s *subscriber) SetLastID(ID uint64) { s.data.LastID = ID } func (s *subscriber) Cancel() { if s.cancel != nil { s.cancel() } } func (s *subscriber) Encode() ([]byte, error) { return json.Marshal(s.data) } func GenerateKey(topic string, params map[string]string) string { // compute the key from params h := sha1.New() io.WriteString(h, topic) // compute the hash with ordered params keys keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { io.WriteString(h, fmt.Sprintf("%s:%s", k, params[k])) } sum := h.Sum(nil) return hex.EncodeToString(sum[:]) } ================================================ FILE: server/connector/substitution.go ================================================ package connector type substitution struct { FieldName string `json:"field"` OldValue string `json:"old_value"` NewValue string `json:"new_value"` } func (s *substitution) isValid() bool { return s.FieldName != "" && s.NewValue != "" && s.OldValue != "" } ================================================ FILE: server/fcm/fcm.go ================================================ package fcm import ( "fmt" "github.com/Bogh/gcm" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/connector" "github.com/smancke/guble/server/metrics" "github.com/smancke/guble/server/router" "time" ) const ( // schema is the default database schema for FCM schema = "fcm_registration" deviceTokenKey = "device_token" userIDKEy = "user_id" ) // Config is used for configuring the Firebase Cloud Messaging component. type Config struct { Enabled *bool APIKey *string Workers *int Endpoint *string Prefix *string IntervalMetrics *bool AfterMessageDelivery protocol.MessageDeliveryCallback } // Connector is the structure for handling the communication with Firebase Cloud Messaging type fcm struct { Config connector.Connector } // New creates a new *fcm and returns it as an connector.ResponsiveConnector func New(router router.Router, sender connector.Sender, config Config) (connector.ResponsiveConnector, error) { baseConn, err := connector.NewConnector(router, sender, connector.Config{ Name: "fcm", Schema: schema, Prefix: *config.Prefix, URLPattern: fmt.Sprintf("/{%s}/{%s}/{%s:.*}", deviceTokenKey, userIDKEy, connector.TopicParam), Workers: *config.Workers, }) if err != nil { logger.WithError(err).Error("Base connector error") return nil, err } f := &fcm{config, baseConn} f.SetResponseHandler(f) return f, nil } func (f *fcm) Start() error { err := f.Connector.Start() if err == nil { f.startMetrics() } return err } func (f *fcm) startMetrics() { mTotalSentMessages.Set(0) mTotalSendErrors.Set(0) mTotalResponseErrors.Set(0) mTotalResponseInternalErrors.Set(0) mTotalResponseNotRegisteredErrors.Set(0) mTotalReplacedCanonicalErrors.Set(0) mTotalResponseOtherErrors.Set(0) if *f.IntervalMetrics { f.startIntervalMetric(mMinute, time.Minute) f.startIntervalMetric(mHour, time.Hour) f.startIntervalMetric(mDay, time.Hour*24) } } func (f *fcm) startIntervalMetric(m metrics.Map, td time.Duration) { metrics.RegisterInterval(f.Context(), m, td, resetIntervalMetrics, processAndResetIntervalMetrics) } func (f *fcm) HandleResponse(request connector.Request, responseIface interface{}, metadata *connector.Metadata, err error) error { if err != nil && !isValidResponseError(err) { logger.WithField("error", err.Error()).Error("Error sending message to FCM") mTotalSendErrors.Add(1) if *f.IntervalMetrics && metadata != nil { addToLatenciesAndCountsMaps(currentTotalErrorsLatenciesKey, currentTotalErrorsKey, metadata.Latency) } return err } message := request.Message() subscriber := request.Subscriber() response, ok := responseIface.(*gcm.Response) if !ok { mTotalResponseErrors.Add(1) return fmt.Errorf("Invalid FCM Response") } logger.WithField("messageID", message.ID).Debug("Delivered message to FCM") subscriber.SetLastID(message.ID) if err := f.Manager().Update(request.Subscriber()); err != nil { logger.WithField("error", err.Error()).Error("Manager could not update subscription") mTotalResponseInternalErrors.Add(1) return err } if response.Ok() { mTotalSentMessages.Add(1) if *f.IntervalMetrics && metadata != nil { addToLatenciesAndCountsMaps(currentTotalMessagesLatenciesKey, currentTotalMessagesKey, metadata.Latency) } return nil } logger.WithField("success", response.Success).Debug("Handling FCM Error") switch errText := response.Error.Error(); errText { case "NotRegistered": logger.Debug("Removing not registered FCM subscription") f.Manager().Remove(subscriber) mTotalResponseNotRegisteredErrors.Add(1) return response.Error case "InvalidRegistration": logger.WithField("jsonError", errText).Error("InvalidRegistration of FCM subscription") default: logger.WithField("jsonError", errText).Error("Unexpected error while sending to FCM") } if response.CanonicalIDs != 0 { mTotalReplacedCanonicalErrors.Add(1) // we only send to one receiver, so we know that we can replace the old id with the first registration id (=canonical id) return f.replaceCanonical(request.Subscriber(), response.Results[0].RegistrationID) } mTotalResponseOtherErrors.Add(1) return nil } func (f *fcm) replaceCanonical(subscriber connector.Subscriber, newToken string) error { manager := f.Manager() err := manager.Remove(subscriber) if err != nil { return err } topic := subscriber.Route().Path params := subscriber.Route().RouteParams.Copy() params[deviceTokenKey] = newToken newSubscriber, err := manager.Create(topic, params) go f.Run(newSubscriber) return err } ================================================ FILE: server/fcm/fcm_metrics.go ================================================ package fcm import ( "github.com/smancke/guble/server/metrics" "time" ) var ( ns = metrics.NS("fcm") mTotalSentMessages = ns.NewInt("total_sent_messages") mTotalSendErrors = ns.NewInt("total_sent_message_errors") mTotalResponseErrors = ns.NewInt("total_response_errors") mTotalResponseInternalErrors = ns.NewInt("total_response_internal_errors") mTotalResponseNotRegisteredErrors = ns.NewInt("total_response_not_registered_errors") mTotalReplacedCanonicalErrors = ns.NewInt("total_replaced_canonical_errors") mTotalResponseOtherErrors = ns.NewInt("total_response_other_errors") mMinute = ns.NewMap("minute") mHour = ns.NewMap("hour") mDay = ns.NewMap("day") ) const ( currentTotalMessagesLatenciesKey = "current_messages_total_latencies_nanos" currentTotalMessagesKey = "current_messages_count" currentTotalErrorsLatenciesKey = "current_errors_total_latencies_nanos" currentTotalErrorsKey = "current_errors_count" ) func processAndResetIntervalMetrics(m metrics.Map, td time.Duration, t time.Time) { msgLatenciesValue := m.Get(currentTotalMessagesLatenciesKey) msgNumberValue := m.Get(currentTotalMessagesKey) errLatenciesValue := m.Get(currentTotalErrorsLatenciesKey) errNumberValue := m.Get(currentTotalErrorsKey) m.Init() resetIntervalMetrics(m, t) metrics.SetRate(m, "last_messages_rate_sec", msgNumberValue, td, time.Second) metrics.SetRate(m, "last_errors_rate_sec", errNumberValue, td, time.Second) metrics.SetAverage(m, "last_messages_average_latency_msec", msgLatenciesValue, msgNumberValue, metrics.MilliPerNano, metrics.DefaultAverageLatencyJSONValue) metrics.SetAverage(m, "last_errors_average_latency_msec", errLatenciesValue, errNumberValue, metrics.MilliPerNano, metrics.DefaultAverageLatencyJSONValue) } func resetIntervalMetrics(m metrics.Map, t time.Time) { m.Set("current_interval_start", metrics.NewTime(t)) metrics.AddToMaps(currentTotalMessagesLatenciesKey, 0, m) metrics.AddToMaps(currentTotalMessagesKey, 0, m) metrics.AddToMaps(currentTotalErrorsLatenciesKey, 0, m) metrics.AddToMaps(currentTotalErrorsKey, 0, m) } func addToLatenciesAndCountsMaps(latenciesKey string, countKey string, latency time.Duration) { metrics.AddToMaps(latenciesKey, int64(latency), mMinute, mHour, mDay) metrics.AddToMaps(countKey, 1, mMinute, mHour, mDay) } ================================================ FILE: server/fcm/fcm_sender.go ================================================ package fcm import ( "encoding/json" "time" log "github.com/Sirupsen/logrus" "github.com/Bogh/gcm" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/connector" ) const ( // sendRetries is the number of retries when something fails sendRetries = 5 // sendTimeout timeout to wait for response from FCM sendTimeout = time.Second ) type sender struct { gcmSender gcm.Sender } func NewSender(apiKey string) *sender { return &sender{ gcmSender: gcm.NewSender(apiKey, sendRetries, sendTimeout), } } func (s *sender) Send(request connector.Request) (interface{}, error) { deviceToken := request.Subscriber().Route().Get(deviceTokenKey) fcmMessage := fcmMessage(request.Message()) fcmMessage.To = deviceToken logger.WithFields(log.Fields{"deviceToken": fcmMessage.To}).Debug("sending message") return s.gcmSender.Send(fcmMessage) } func fcmMessage(message *protocol.Message) *gcm.Message { m := &gcm.Message{} err := json.Unmarshal(message.Body, m) if err != nil { logger.WithFields(log.Fields{ "error": err.Error(), "body": string(message.Body), "messageID": message.ID, }).Debug("Could not decode gcm.Message from guble message body") } else if m.Notification != nil && m.Data != nil { return m } err = json.Unmarshal(message.Body, &m.Data) if err != nil { m.Data = map[string]interface{}{ "message": message.Body, } } return m } // isValidResponseError returns True if the error is accepted as a valid response // cases are InvalidRegistration and NotRegistered func isValidResponseError(err error) bool { return err.Error() == "InvalidRegistration" || err.Error() == "NotRegistered" } ================================================ FILE: server/fcm/fcm_test.go ================================================ package fcm import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/Bogh/gcm" "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/connector" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/router" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" ) var fullFCMMessage = `{ "notification": { "title": "TEST", "body": "notification body", "icon": "ic_notification_test_icon", "click_action": "estimated_arrival" }, "data": {"field1": "value1", "field2": "value2"} }` type mocks struct { router *MockRouter store *MockMessageStore gcmSender *MockSender } func TestConnector_GetErrorMessageFromFCM(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) fcm, mocks := testFCM(t, true) err := fcm.Start() a.NoError(err) var route *router.Route mocks.router.EXPECT().Subscribe(gomock.Any()).Do(func(r *router.Route) (*router.Route, error) { a.Equal("/topic", string(r.Path)) a.Equal("user01", r.Get("user_id")) a.Equal("device01", r.Get(deviceTokenKey)) route = r return r, nil }) // put a dummy FCM message with minimum information postSubscription(t, fcm, "user01", "device01", "topic") time.Sleep(100 * time.Millisecond) a.NoError(err) a.NotNil(route) // expect the route unsubscribed mocks.router.EXPECT().Unsubscribe(gomock.Any()).Do(func(route *router.Route) { a.Equal("/topic", string(route.Path)) a.Equal("device01", route.Get(deviceTokenKey)) }) // expect the route subscribe with the new canonicalID from replaceSubscriptionWithCanonicalID mocks.router.EXPECT().Subscribe(gomock.Any()).Do(func(route *router.Route) { a.Equal("/topic", string(route.Path)) a.Equal("user01", route.Get("user_id")) appid := route.Get(deviceTokenKey) a.Equal("fcmCanonicalID", appid) }) // mocks.store.EXPECT().MaxMessageID(gomock.Any()).Return(uint64(4), nil) response := new(gcm.Response) err = json.Unmarshal([]byte(ErrorFCMResponse), response) a.NoError(err) mocks.gcmSender.EXPECT().Send(gomock.Any()).Return(response, nil) // send the message into the subscription route channel route.Deliver(&protocol.Message{ ID: uint64(4), Path: "/topic", Body: []byte("{id:id}"), }, true) // wait before closing the FCM connector time.Sleep(100 * time.Millisecond) err = fcm.Stop() a.NoError(err) } func TestFCMFormatMessage(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) var subRoute *router.Route fcm, mocks := testFCM(t, false) fcm.Start() defer fcm.Stop() time.Sleep(50 * time.Millisecond) mocks.router.EXPECT().Subscribe(gomock.Any()).Do(func(route *router.Route) (*router.Route, error) { subRoute = route return route, nil }) postSubscription(t, fcm, "user01", "device01", "topic") time.Sleep(100 * time.Millisecond) // send a fully formated GCM message m := &protocol.Message{ Path: "/topic", ID: 1, Body: []byte(fullFCMMessage), } if !a.NotNil(subRoute) { return } doneC := make(chan bool) mocks.gcmSender.EXPECT().Send(gomock.Any()).Do(func(m *gcm.Message) (*gcm.Response, error) { a.NotNil(m.Notification) a.Equal("TEST", m.Notification.Title) a.Equal("notification body", m.Notification.Body) a.Equal("ic_notification_test_icon", m.Notification.Icon) a.Equal("estimated_arrival", m.Notification.ClickAction) a.NotNil(m.Data) if a.Contains(m.Data, "field1") { a.Equal("value1", m.Data["field1"]) } if a.Contains(m.Data, "field2") { a.Equal("value2", m.Data["field2"]) } doneC <- true return nil, nil }).Return(&gcm.Response{}, nil) subRoute.Deliver(m, true) select { case <-doneC: case <-time.After(100 * time.Millisecond): a.Fail("Message not received by FCM") } m = &protocol.Message{ Path: "/topic", ID: 1, Body: []byte(`plain body`), } mocks.gcmSender.EXPECT().Send(gomock.Any()).Do(func(m *gcm.Message) (*gcm.Response, error) { a.Nil(m.Notification) a.NotNil(m.Data) a.Contains(m.Data, "message") doneC <- true return nil, nil }).Return(&gcm.Response{}, nil) subRoute.Deliver(m, true) select { case <-doneC: case <-time.After(100 * time.Millisecond): a.Fail("Message not received by FCM") } } func testFCM(t *testing.T, mockStore bool) (connector.ResponsiveConnector, *mocks) { mcks := new(mocks) mcks.router = NewMockRouter(testutil.MockCtrl) mcks.router.EXPECT().Cluster().Return(nil).AnyTimes() kvs := kvstore.NewMemoryKVStore() mcks.router.EXPECT().KVStore().Return(kvs, nil).AnyTimes() key := "TEST-API-KEY" nWorkers := 1 endpoint := "" prefix := "/fcm/" intervalMetrics := false mcks.gcmSender = NewMockSender(testutil.MockCtrl) sender := NewSender(key) sender.gcmSender = mcks.gcmSender conn, err := New(mcks.router, sender, Config{ APIKey: &key, Workers: &nWorkers, Endpoint: &endpoint, Prefix: &prefix, IntervalMetrics: &intervalMetrics, }) assert.NoError(t, err) if mockStore { mcks.store = NewMockMessageStore(testutil.MockCtrl) mcks.router.EXPECT().MessageStore().Return(mcks.store, nil).AnyTimes() } return conn, mcks } func postSubscription(t *testing.T, fcmConn connector.ResponsiveConnector, userID, gcmID, topic string) { a := assert.New(t) u := fmt.Sprintf("http://localhost/fcm/%s/%s/%s", gcmID, userID, topic) req, err := http.NewRequest(http.MethodPost, u, nil) a.NoError(err) w := httptest.NewRecorder() fcmConn.ServeHTTP(w, req) a.Equal(fmt.Sprintf(`{"subscribed":"/%s"}`, topic), string(w.Body.Bytes())) } func deleteSubscription(t *testing.T, fcmConn connector.ResponsiveConnector, userID, gcmID, topic string) { a := assert.New(t) u := fmt.Sprintf("http://localhost/fcm/%s/%s/%s", gcmID, userID, topic) req, err := http.NewRequest(http.MethodDelete, u, nil) a.NoError(err) w := httptest.NewRecorder() fcmConn.ServeHTTP(w, req) a.Equal(fmt.Sprintf(`{"unsubscribed":"/%s"}`, topic), string(w.Body.Bytes())) } func removeTrailingSlash(path string) string { if len(path) > 1 && path[len(path)-1] == '/' { return path[:len(path)-1] } return path } ================================================ FILE: server/fcm/json_error.go ================================================ package fcm type jsonError struct { json string } func (e *jsonError) Error() string { return e.json } ================================================ FILE: server/fcm/logger.go ================================================ package fcm import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithField("module", "fcm") ================================================ FILE: server/fcm/mocks_gcm_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/Bogh/gcm (interfaces: Sender) package fcm import ( gcm "github.com/Bogh/gcm" gomock "github.com/golang/mock/gomock" ) // Mock of Sender interface type MockSender struct { ctrl *gomock.Controller recorder *_MockSenderRecorder } // Recorder for MockSender (not exported) type _MockSenderRecorder struct { mock *MockSender } func NewMockSender(ctrl *gomock.Controller) *MockSender { mock := &MockSender{ctrl: ctrl} mock.recorder = &_MockSenderRecorder{mock} return mock } func (_m *MockSender) EXPECT() *_MockSenderRecorder { return _m.recorder } func (_m *MockSender) Send(_param0 *gcm.Message) (*gcm.Response, error) { ret := _m.ctrl.Call(_m, "Send", _param0) ret0, _ := ret[0].(*gcm.Response) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockSenderRecorder) Send(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Send", arg0) } ================================================ FILE: server/fcm/mocks_kvstore_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/kvstore (interfaces: KVStore) package fcm import ( gomock "github.com/golang/mock/gomock" ) // Mock of KVStore interface type MockKVStore struct { ctrl *gomock.Controller recorder *_MockKVStoreRecorder } // Recorder for MockKVStore (not exported) type _MockKVStoreRecorder struct { mock *MockKVStore } func NewMockKVStore(ctrl *gomock.Controller) *MockKVStore { mock := &MockKVStore{ctrl: ctrl} mock.recorder = &_MockKVStoreRecorder{mock} return mock } func (_m *MockKVStore) EXPECT() *_MockKVStoreRecorder { return _m.recorder } func (_m *MockKVStore) Delete(_param0 string, _param1 string) error { ret := _m.ctrl.Call(_m, "Delete", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockKVStoreRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Delete", arg0, arg1) } func (_m *MockKVStore) Get(_param0 string, _param1 string) ([]byte, bool, error) { ret := _m.ctrl.Call(_m, "Get", _param0, _param1) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(bool) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } func (_mr *_MockKVStoreRecorder) Get(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Get", arg0, arg1) } func (_m *MockKVStore) Iterate(_param0 string, _param1 string) chan [2]string { ret := _m.ctrl.Call(_m, "Iterate", _param0, _param1) ret0, _ := ret[0].(chan [2]string) return ret0 } func (_mr *_MockKVStoreRecorder) Iterate(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Iterate", arg0, arg1) } func (_m *MockKVStore) IterateKeys(_param0 string, _param1 string) chan string { ret := _m.ctrl.Call(_m, "IterateKeys", _param0, _param1) ret0, _ := ret[0].(chan string) return ret0 } func (_mr *_MockKVStoreRecorder) IterateKeys(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "IterateKeys", arg0, arg1) } func (_m *MockKVStore) Put(_param0 string, _param1 string, _param2 []byte) error { ret := _m.ctrl.Call(_m, "Put", _param0, _param1, _param2) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockKVStoreRecorder) Put(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Put", arg0, arg1, arg2) } ================================================ FILE: server/fcm/mocks_router_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/router (interfaces: Router) package fcm import ( "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/cluster" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" ) // Mock of Router interface type MockRouter struct { ctrl *gomock.Controller recorder *_MockRouterRecorder } // Recorder for MockRouter (not exported) type _MockRouterRecorder struct { mock *MockRouter } func NewMockRouter(ctrl *gomock.Controller) *MockRouter { mock := &MockRouter{ctrl: ctrl} mock.recorder = &_MockRouterRecorder{mock} return mock } func (_m *MockRouter) EXPECT() *_MockRouterRecorder { return _m.recorder } func (_m *MockRouter) AccessManager() (auth.AccessManager, error) { ret := _m.ctrl.Call(_m, "AccessManager") ret0, _ := ret[0].(auth.AccessManager) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) AccessManager() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "AccessManager") } func (_m *MockRouter) Cluster() *cluster.Cluster { ret := _m.ctrl.Call(_m, "Cluster") ret0, _ := ret[0].(*cluster.Cluster) return ret0 } func (_mr *_MockRouterRecorder) Cluster() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Cluster") } func (_m *MockRouter) Done() <-chan bool { ret := _m.ctrl.Call(_m, "Done") ret0, _ := ret[0].(<-chan bool) return ret0 } func (_mr *_MockRouterRecorder) Done() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Done") } func (_m *MockRouter) Fetch(_param0 *store.FetchRequest) error { ret := _m.ctrl.Call(_m, "Fetch", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockRouter) GetSubscribers(_param0 string) ([]byte, error) { ret := _m.ctrl.Call(_m, "GetSubscribers", _param0) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) GetSubscribers(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetSubscribers", arg0) } func (_m *MockRouter) HandleMessage(_param0 *protocol.Message) error { ret := _m.ctrl.Call(_m, "HandleMessage", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) HandleMessage(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "HandleMessage", arg0) } func (_m *MockRouter) KVStore() (kvstore.KVStore, error) { ret := _m.ctrl.Call(_m, "KVStore") ret0, _ := ret[0].(kvstore.KVStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) KVStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "KVStore") } func (_m *MockRouter) MessageStore() (store.MessageStore, error) { ret := _m.ctrl.Call(_m, "MessageStore") ret0, _ := ret[0].(store.MessageStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) MessageStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MessageStore") } func (_m *MockRouter) Subscribe(_param0 *router.Route) (*router.Route, error) { ret := _m.ctrl.Call(_m, "Subscribe", _param0) ret0, _ := ret[0].(*router.Route) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) Subscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Subscribe", arg0) } func (_m *MockRouter) Unsubscribe(_param0 *router.Route) { _m.ctrl.Call(_m, "Unsubscribe", _param0) } func (_mr *_MockRouterRecorder) Unsubscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Unsubscribe", arg0) } ================================================ FILE: server/fcm/mocks_store_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/store (interfaces: MessageStore) package fcm import ( gomock "github.com/golang/mock/gomock" protocol "github.com/smancke/guble/protocol" store "github.com/smancke/guble/server/store" ) // Mock of MessageStore interface type MockMessageStore struct { ctrl *gomock.Controller recorder *_MockMessageStoreRecorder } // Recorder for MockMessageStore (not exported) type _MockMessageStoreRecorder struct { mock *MockMessageStore } func NewMockMessageStore(ctrl *gomock.Controller) *MockMessageStore { mock := &MockMessageStore{ctrl: ctrl} mock.recorder = &_MockMessageStoreRecorder{mock} return mock } func (_m *MockMessageStore) EXPECT() *_MockMessageStoreRecorder { return _m.recorder } func (_m *MockMessageStore) DoInTx(_param0 string, _param1 func(uint64) error) error { ret := _m.ctrl.Call(_m, "DoInTx", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockMessageStoreRecorder) DoInTx(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "DoInTx", arg0, arg1) } func (_m *MockMessageStore) Fetch(_param0 *store.FetchRequest) { _m.ctrl.Call(_m, "Fetch", _param0) } func (_mr *_MockMessageStoreRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockMessageStore) GenerateNextMsgID(_param0 string, _param1 byte) (uint64, int64, error) { ret := _m.ctrl.Call(_m, "GenerateNextMsgID", _param0, _param1) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(int64) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } func (_mr *_MockMessageStoreRecorder) GenerateNextMsgID(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GenerateNextMsgID", arg0, arg1) } func (_m *MockMessageStore) MaxMessageID(_param0 string) (uint64, error) { ret := _m.ctrl.Call(_m, "MaxMessageID", _param0) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) MaxMessageID(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MaxMessageID", arg0) } func (_m *MockMessageStore) Partition(_param0 string) (store.MessagePartition, error) { ret := _m.ctrl.Call(_m, "Partition", _param0) ret0, _ := ret[0].(store.MessagePartition) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) Partition(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Partition", arg0) } func (_m *MockMessageStore) Partitions() ([]store.MessagePartition, error) { ret := _m.ctrl.Call(_m, "Partitions") ret0, _ := ret[0].([]store.MessagePartition) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) Partitions() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Partitions") } func (_m *MockMessageStore) Store(_param0 string, _param1 uint64, _param2 []byte) error { ret := _m.ctrl.Call(_m, "Store", _param0, _param1, _param2) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockMessageStoreRecorder) Store(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Store", arg0, arg1, arg2) } func (_m *MockMessageStore) StoreMessage(_param0 *protocol.Message, _param1 byte) (int, error) { ret := _m.ctrl.Call(_m, "StoreMessage", _param0, _param1) ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) StoreMessage(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "StoreMessage", arg0, arg1) } ================================================ FILE: server/fcm/testutil.go ================================================ package fcm import ( "encoding/json" "fmt" "time" "github.com/Bogh/gcm" "github.com/smancke/guble/server/connector" ) const ( SuccessFCMResponse = `{ "multicast_id":3, "success":1, "failure":0, "canonical_ids":0, "results":[ { "message_id":"da", "registration_id":"rId", "error":"" } ] }` ErrorFCMResponse = `{ "multicast_id":3, "success":0, "failure":1, "error":"InvalidRegistration", "canonical_ids":5, "results":[ { "message_id":"err", "registration_id":"fcmCanonicalID", "error":"InvalidRegistration" } ] }` ) func NewSenderWithMock(gcmSender gcm.Sender) *sender { return &sender{gcmSender: gcmSender} } type FCMSender func(message *gcm.Message) (*gcm.Response, error) func (fcms FCMSender) Send(message *gcm.Message) (*gcm.Response, error) { return fcms(message) } func CreateFcmSender(body string, doneC chan bool, to time.Duration) (connector.Sender, error) { response := new(gcm.Response) err := json.Unmarshal([]byte(body), response) if err != nil { return nil, err } return NewSenderWithMock(FCMSender(func(message *gcm.Message) (*gcm.Response, error) { defer func() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered") } }() doneC <- true }() <-time.After(to) return response, nil })), nil } ================================================ FILE: server/fcm_integration_test.go ================================================ package server import ( "bytes" "fmt" "io/ioutil" "net/http" "os" "strings" "testing" "time" "encoding/json" "github.com/smancke/guble/client" "github.com/smancke/guble/server/connector" "github.com/smancke/guble/server/fcm" "github.com/smancke/guble/server/service" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" ) var ( testHttpPort = 11000 timeoutForOneMessage = 50 * time.Millisecond ) type fcmMetricsMap struct { CurrentErrorsCount int `json:"current_errors_count"` CurrentMessagesCount int `json:"current_messages_count"` CurrentMessagesTotalLatencies int `json:"current_messages_total_latencies_nanos"` CurrentErrorsTotalLatencies int `json:"current_errors_total_latencies_nanos"` } type fcmMetrics struct { TotalSentMessages int `json:"fcm.total_sent_messages"` TotalSentMessageErrors int `json:"fcm.total_sent_message_errors"` Minute fcmMetricsMap `json:"fcm.minute"` Hour fcmMetricsMap `json:"fcm.hour"` Day fcmMetricsMap `json:"fcm.day"` } type routerMetrics struct { CurrentRoutes int `json:"router.current_routes"` CurrentSubscriptions int `json:"router.current_subscriptions"` } type expectedValues struct { ZeroLatencies bool MessageCount int CurrentRoutes int CurrentSubscriptions int } // Test that restarting the service continues to fetch messages from store for a subscription from lastID func TestFCMRestart(t *testing.T) { // defer testutil.EnableDebugForMethod()() defer testutil.ResetDefaultRegistryHealthCheck() a := assert.New(t) receiveC := make(chan bool) s, cleanup := serviceSetUp(t) defer cleanup() assertMetrics(a, s, expectedValues{true, 0, 0, 0}) var fcmConn connector.ResponsiveConnector var ok bool for _, iface := range s.ModulesSortedByStartOrder() { fcmConn, ok = iface.(connector.ResponsiveConnector) if ok { break } } a.True(ok, "There should be a module of type FCMConnector") // add a high timeout so the messages are processed slow sender, err := fcm.CreateFcmSender(fcm.SuccessFCMResponse, receiveC, 10*time.Millisecond) a.NoError(err) fcmConn.SetSender(sender) // create subscription on topic subscriptionSetUp(t, s) assertMetrics(a, s, expectedValues{true, 0, 1, 1}) c := clientSetUp(t, s) // send 3 messages in the router but read only one and close the service for i := 0; i < 3; i++ { c.Send(testTopic, "dummy body", "{dummy: value}") } // receive one message only from FCM select { case <-receiveC: case <-time.After(timeoutForOneMessage): a.Fail("Initial FCM message not received") } assertMetrics(a, s, expectedValues{false, 1, 1, 1}) close(receiveC) // restart the service a.NoError(s.Stop()) // remake the sender receiveC = make(chan bool) sender, err = fcm.CreateFcmSender(fcm.SuccessFCMResponse, receiveC, 10*time.Millisecond) a.NoError(err) fcmConn.SetSender(sender) time.Sleep(50 * time.Millisecond) testutil.ResetDefaultRegistryHealthCheck() a.NoError(s.Start()) //TODO Cosmin Bogdan add 2 calls to assertMetrics before and after the next block // read the other 2 messages for i := 0; i < 1; i++ { select { case <-receiveC: case <-time.After(2 * timeoutForOneMessage): a.Fail("FCM message not received") } } } func serviceSetUp(t *testing.T) (*service.Service, func()) { dir, errTempDir := ioutil.TempDir("", "guble_fcm_test") assert.NoError(t, errTempDir) *Config.KVS = "memory" *Config.MS = "file" *Config.Cluster.NodeID = 0 *Config.StoragePath = dir *Config.MetricsEndpoint = "/admin/metrics" *Config.FCM.Enabled = true *Config.FCM.APIKey = "WILL BE OVERWRITTEN" *Config.FCM.Prefix = "/fcm/" *Config.FCM.Workers = 1 // use only one worker so we can control the number of messages that go to FCM *Config.APNS.Enabled = false var s *service.Service for s == nil { testHttpPort++ logger.WithField("port", testHttpPort).Debug("trying to use HTTP Port") *Config.HttpListen = fmt.Sprintf("127.0.0.1:%d", testHttpPort) s = StartService() } return s, func() { errRemove := os.RemoveAll(dir) if errRemove != nil { logger.WithError(errRemove).WithField("module", "testing").Error("Could not remove directory") } } } func clientSetUp(t *testing.T, service *service.Service) client.Client { wsURL := "ws://" + service.WebServer().GetAddr() + "/stream/user/user01" c, err := client.Open(wsURL, "http://localhost/", 1000, false) assert.NoError(t, err) return c } func subscriptionSetUp(t *testing.T, service *service.Service) { a := assert.New(t) urlFormat := fmt.Sprintf("http://%s/fcm/%%d/gcmId%%d/%%s", service.WebServer().GetAddr()) // create GCM subscription response, errPost := http.Post( fmt.Sprintf(urlFormat, 1, 1, strings.TrimPrefix(testTopic, "/")), "text/plain", bytes.NewBufferString(""), ) a.NoError(errPost) a.Equal(response.StatusCode, 200) body, errReadAll := ioutil.ReadAll(response.Body) a.NoError(errReadAll) a.Equal(fmt.Sprintf(`{"subscribed":"%s"}`, testTopic), string(body)) } func assertMetrics(a *assert.Assertions, s *service.Service, expected expectedValues) { httpClient := &http.Client{} u := fmt.Sprintf("http://%s%s", s.WebServer().GetAddr(), defaultMetricsEndpoint) request, err := http.NewRequest(http.MethodGet, u, nil) a.NoError(err) response, err := httpClient.Do(request) a.NoError(err) defer response.Body.Close() a.Equal(http.StatusOK, response.StatusCode) bodyBytes, err := ioutil.ReadAll(response.Body) a.NoError(err) logger.WithField("body", string(bodyBytes)).Debug("metrics response") mFCM := &fcmMetrics{} err = json.Unmarshal(bodyBytes, mFCM) a.NoError(err) a.Equal(0, mFCM.TotalSentMessageErrors) a.Equal(expected.MessageCount, mFCM.TotalSentMessages) a.Equal(0, mFCM.Minute.CurrentErrorsCount) a.Equal(expected.MessageCount, mFCM.Minute.CurrentMessagesCount) a.Equal(0, mFCM.Minute.CurrentErrorsTotalLatencies) a.Equal(expected.ZeroLatencies, mFCM.Minute.CurrentMessagesTotalLatencies == 0) a.Equal(0, mFCM.Hour.CurrentErrorsCount) a.Equal(expected.MessageCount, mFCM.Hour.CurrentMessagesCount) a.Equal(0, mFCM.Hour.CurrentErrorsTotalLatencies) a.Equal(expected.ZeroLatencies, mFCM.Hour.CurrentMessagesTotalLatencies == 0) a.Equal(0, mFCM.Day.CurrentErrorsCount) a.Equal(expected.MessageCount, mFCM.Day.CurrentMessagesCount) a.Equal(0, mFCM.Day.CurrentErrorsTotalLatencies) a.Equal(expected.ZeroLatencies, mFCM.Day.CurrentMessagesTotalLatencies == 0) mRouter := &routerMetrics{} err = json.Unmarshal(bodyBytes, mRouter) a.NoError(err) a.Equal(expected.CurrentRoutes, mRouter.CurrentRoutes) a.Equal(expected.CurrentSubscriptions, mRouter.CurrentSubscriptions) } ================================================ FILE: server/gubled.go ================================================ package server import ( log "github.com/Sirupsen/logrus" "github.com/smancke/guble/logformatter" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/apns" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/cluster" "github.com/smancke/guble/server/fcm" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/metrics" "github.com/smancke/guble/server/rest" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/service" "github.com/smancke/guble/server/sms" "github.com/smancke/guble/server/store" "github.com/smancke/guble/server/store/dummystore" "github.com/smancke/guble/server/store/filestore" "github.com/smancke/guble/server/webserver" "github.com/smancke/guble/server/websocket" "fmt" "net" "os" "os/signal" "path" "runtime" "strconv" "syscall" "github.com/Bogh/gcm" "github.com/pkg/profile" "golang.org/x/crypto/ssh/terminal" ) const ( fileOption = "file" ) var AfterMessageDelivery = func(m *protocol.Message) { logger.WithField("message", m).Debug("message delivered") } // ValidateStoragePath validates the guble configuration with regard to the storagePath // (which can be used by MessageStore and/or KVStore implementations). var ValidateStoragePath = func() error { if *Config.KVS == fileOption || *Config.MS == fileOption { testfile := path.Join(*Config.StoragePath, "write-test-file") f, err := os.Create(testfile) if err != nil { logger.WithError(err).WithField("storagePath", *Config.StoragePath).Error("Storage path not present/writeable.") return err } f.Close() os.Remove(testfile) } return nil } // CreateAccessManager is a func which returns a auth.AccessManager implementation // (currently: AllowAllAccessManager). var CreateAccessManager = func() auth.AccessManager { return auth.NewAllowAllAccessManager(true) } // CreateKVStore is a func which returns a kvstore.KVStore implementation // (currently, based on guble configuration). var CreateKVStore = func() kvstore.KVStore { switch *Config.KVS { case "memory": return kvstore.NewMemoryKVStore() case "file": db := kvstore.NewSqliteKVStore(path.Join(*Config.StoragePath, "kv-store.db"), true) if err := db.Open(); err != nil { logger.WithError(err).Panic("Could not open sqlite database connection") } return db case "postgres": db := kvstore.NewPostgresKVStore(kvstore.PostgresConfig{ ConnParams: map[string]string{ "host": *Config.Postgres.Host, "port": strconv.Itoa(*Config.Postgres.Port), "user": *Config.Postgres.User, "password": *Config.Postgres.Password, "dbname": *Config.Postgres.DbName, "sslmode": "disable", }, MaxIdleConns: 1, MaxOpenConns: runtime.GOMAXPROCS(0), }) if err := db.Open(); err != nil { logger.WithError(err).Panic("Could not open postgres database connection") } return db default: panic(fmt.Errorf("Unknown key-value backend: %q", *Config.KVS)) } } // CreateMessageStore is a func which returns a store.MessageStore implementation // (currently, based on guble configuration). var CreateMessageStore = func() store.MessageStore { switch *Config.MS { case "none", "memory", "": return dummystore.New(kvstore.NewMemoryKVStore()) case "file": logger.WithField("storagePath", *Config.StoragePath).Info("Using FileMessageStore in directory") return filestore.New(*Config.StoragePath) default: panic(fmt.Errorf("Unknown message-store backend: %q", *Config.MS)) } } // CreateModules is a func which returns a slice of modules which should be used by the service // (currently, based on guble configuration); // see package `service` for terminological details. var CreateModules = func(router router.Router) []interface{} { var modules []interface{} if wsHandler, err := websocket.NewWSHandler(router, "/stream/"); err != nil { logger.WithError(err).Error("Error loading WSHandler module") } else { modules = append(modules, wsHandler) } modules = append(modules, rest.NewRestMessageAPI(router, "/api/")) if *Config.FCM.Enabled { logger.Info("Firebase Cloud Messaging: enabled") if *Config.FCM.APIKey == "" { logger.Panic("The API Key has to be provided when Firebase Cloud Messaging is enabled") } Config.FCM.AfterMessageDelivery = AfterMessageDelivery *Config.FCM.IntervalMetrics = true if Config.FCM.Endpoint != nil { gcm.GcmSendEndpoint = *Config.FCM.Endpoint } sender := fcm.NewSender(*Config.FCM.APIKey) if fcmConn, err := fcm.New(router, sender, Config.FCM); err != nil { logger.WithError(err).Error("Error creating FCM connector") } else { modules = append(modules, fcmConn) } } else { logger.Info("Firebase Cloud Messaging: disabled") } if *Config.APNS.Enabled { if *Config.APNS.Production { logger.Info("APNS: enabled in production mode") } else { logger.Info("APNS: enabled in development mode") } logger.Info("APNS: enabled") if *Config.APNS.CertificateFileName == "" && Config.APNS.CertificateBytes == nil { logger.Panic("The certificate (as filename or bytes) has to be provided when APNS is enabled") } if *Config.APNS.CertificatePassword == "" { logger.Panic("A non-empty password has to be provided when APNS is enabled") } if *Config.APNS.AppTopic == "" { logger.Panic("The Mobile App Topic (usually the bundle-id) has to be provided when APNS is enabled") } apnsSender, err := apns.NewSender(Config.APNS) if err != nil { logger.Panic("APNS Sender could not be created") } *Config.APNS.IntervalMetrics = true if apnsConn, err := apns.New(router, apnsSender, Config.APNS); err != nil { logger.WithError(err).Error("Error creating APNS connector") } else { modules = append(modules, apnsConn) } } else { logger.Info("APNS: disabled") } if *Config.SMS.Enabled { logger.Info("Nexmo SMS: enabled") if *Config.SMS.APIKey == "" || *Config.SMS.APISecret == "" { logger.Panic("The API Key has to be provided when NEXMO SMS connector is enabled") } nexmoSender, err := sms.NewNexmoSender(*Config.SMS.APIKey, *Config.SMS.APISecret) if err != nil { logger.WithError(err).Error("Error creating Nexmo Sender") } smsConn, err := sms.New(router, nexmoSender, Config.SMS) if err != nil { logger.WithError(err).Error("Error creating Nexmo Sender") } else { modules = append(modules, smsConn) } } else { logger.Info("SMS: disabled") } return modules } // Main is the entry-point of the guble server. func Main() { defer func() { if p := recover(); p != nil { logger.Fatal("Fatal error in gubled after recover") } }() parseConfig() if !terminal.IsTerminal(int(os.Stdout.Fd())) { log.SetFormatter(&logformatter.LogstashFormatter{Env: *Config.EnvName}) } level, err := log.ParseLevel(*Config.Log) if err != nil { logger.WithError(err).Fatal("Invalid log level") } log.SetLevel(level) switch *Config.Profile { case cpuProfile: logger.Info("starting to profile cpu") defer profile.Start(profile.CPUProfile).Stop() case memProfile: logger.Info("starting to profile memory") defer profile.Start(profile.MemProfile).Stop() case blockProfile: logger.Info("starting to profile blocking/contention") defer profile.Start(profile.BlockProfile).Stop() default: logger.Debug("no profiling was started") } if err := ValidateStoragePath(); err != nil { logger.Fatal("Fatal error in gubled in validation of storage path") } srv := StartService() if srv == nil { logger.Fatal("exiting because of unrecoverable error(s) when starting the service") } waitForTermination(func() { err := srv.Stop() if err != nil { logger.WithField("error", err.Error()).Error("errors occurred while stopping service") } }) } // StartService starts a server.Service after first creating the router (and its dependencies), the webserver. func StartService() *service.Service { //TODO StartService could return an error in case it fails to start accessManager := CreateAccessManager() messageStore := CreateMessageStore() kvStore := CreateKVStore() var cl *cluster.Cluster var err error if *Config.Cluster.NodeID > 0 { exitIfInvalidClusterParams(*Config.Cluster.NodeID, *Config.Cluster.NodePort, *Config.Cluster.Remotes) logger.Info("Starting in cluster-mode") cl, err = cluster.New(&cluster.Config{ ID: *Config.Cluster.NodeID, Port: *Config.Cluster.NodePort, Remotes: *Config.Cluster.Remotes, }) if err != nil { logger.WithField("err", err).Fatal("Module could not be started (cluster)") } } else { logger.Info("Starting in standalone-mode") } r := router.New(accessManager, messageStore, kvStore, cl) websrv := webserver.New(*Config.HttpListen) srv := service.New(r, websrv). HealthEndpoint(*Config.HealthEndpoint). MetricsEndpoint(*Config.MetricsEndpoint) srv.RegisterModules(0, 6, kvStore, messageStore) srv.RegisterModules(4, 3, CreateModules(r)...) if err = srv.Start(); err != nil { logger.WithField("error", err.Error()).Error("errors occurred while starting service") if err = srv.Stop(); err != nil { logger.WithField("error", err.Error()).Error("errors occurred when stopping service after it failed to start") } return nil } return srv } func exitIfInvalidClusterParams(nodeID uint8, nodePort int, remotes []*net.TCPAddr) { if (nodeID <= 0 && len(remotes) > 0) || (nodePort <= 0) { errorMessage := "Could not start in cluster-mode: invalid/incomplete parameters" logger.WithFields(log.Fields{ "nodeID": nodeID, "nodePort": nodePort, "numberOfRemotes": len(remotes), }).Fatal(errorMessage) } } func waitForTermination(callback func()) { signalC := make(chan os.Signal) signal.Notify(signalC, syscall.SIGINT, syscall.SIGTERM) sig := <-signalC logger.Infof("Got signal '%v' .. exiting gracefully now", sig) callback() metrics.LogOnDebugLevel() logger.Info("Exit gracefully now") os.Exit(0) } ================================================ FILE: server/gubled_test.go ================================================ package server import ( "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" "fmt" "io/ioutil" "os" "reflect" "strings" "testing" ) func TestValidateStoragePath(t *testing.T) { a := assert.New(t) valid := os.TempDir() invalid := os.TempDir() + "/non-existing-directory-for-guble-test" *Config.MS = "file" *Config.StoragePath = valid a.NoError(ValidateStoragePath()) *Config.StoragePath = invalid a.Error(ValidateStoragePath()) *Config.KVS = "file" a.Error(ValidateStoragePath()) } func TestCreateKVStoreBackend(t *testing.T) { a := assert.New(t) *Config.KVS = "memory" memory := CreateKVStore() a.Equal("*kvstore.MemoryKVStore", reflect.TypeOf(memory).String()) dir, _ := ioutil.TempDir("", "guble_test") defer os.RemoveAll(dir) *Config.KVS = "file" *Config.StoragePath = dir sqlite := CreateKVStore() a.Equal("*kvstore.SqliteKVStore", reflect.TypeOf(sqlite).String()) } func TestFCMOnlyStartedIfEnabled(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) routerMock := initRouterMock() routerMock.EXPECT().KVStore().Return(kvstore.NewMemoryKVStore(), nil) *Config.FCM.Enabled = true *Config.FCM.APIKey = "xyz" *Config.APNS.Enabled = false a.True(containsFCMModule(CreateModules(routerMock))) *Config.FCM.Enabled = false a.False(containsFCMModule(CreateModules(routerMock))) } func containsFCMModule(modules []interface{}) bool { for _, module := range modules { if reflect.TypeOf(module).String() == "*fcm.fcm" { return true } } return false } func TestPanicOnMissingFCMApiKey(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() defer func() { if r := recover(); r == nil { t.Log("expect panic, because the gcm api key was not supplied") t.Fail() } }() routerMock := initRouterMock() *Config.FCM.APIKey = "" *Config.FCM.Enabled = true CreateModules(routerMock) } func TestCreateStoreBackendPanicInvalidBackend(t *testing.T) { var p interface{} func() { defer func() { p = recover() }() *Config.KVS = "foo bar" CreateKVStore() }() assert.NotNil(t, p) } func TestStartServiceModules(t *testing.T) { defer testutil.ResetDefaultRegistryHealthCheck() a := assert.New(t) // when starting a simple valid service *Config.KVS = "memory" *Config.MS = "file" *Config.FCM.Enabled = false *Config.APNS.Enabled = false // using an available port for http testHttpPort++ logger.WithField("port", testHttpPort).Debug("trying to use HTTP Port") *Config.HttpListen = fmt.Sprintf(":%d", testHttpPort) s := StartService() // then the number and ordering of modules should be correct a.Equal(6, len(s.ModulesSortedByStartOrder())) var moduleNames []string for _, iface := range s.ModulesSortedByStartOrder() { name := reflect.TypeOf(iface).String() moduleNames = append(moduleNames, name) } a.Equal("*kvstore.MemoryKVStore *filestore.FileMessageStore *router.router *webserver.WebServer *websocket.WSHandler *rest.RestMessageAPI", strings.Join(moduleNames, " ")) } func initRouterMock() *MockRouter { routerMock := NewMockRouter(testutil.MockCtrl) routerMock.EXPECT().Cluster().Return(nil).AnyTimes() amMock := NewMockAccessManager(testutil.MockCtrl) msMock := NewMockMessageStore(testutil.MockCtrl) routerMock.EXPECT().AccessManager().Return(amMock, nil).AnyTimes() routerMock.EXPECT().MessageStore().Return(msMock, nil).AnyTimes() return routerMock } ================================================ FILE: server/integration_test.go ================================================ package server import ( "github.com/smancke/guble/client" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/service" "github.com/stretchr/testify/assert" "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "strings" "testing" "time" "github.com/smancke/guble/restclient" "github.com/smancke/guble/testutil" ) func initServerAndClients(t *testing.T) (*service.Service, client.Client, client.Client, func()) { *Config.HttpListen = "localhost:0" *Config.KVS = "memory" s := StartService() time.Sleep(time.Millisecond * 100) var err error client1, err := client.Open("ws://"+s.WebServer().GetAddr()+"/stream/user/user1", "http://localhost", 1, false) assert.NoError(t, err) checkConnectedNotificationJSON(t, "user1", expectStatusMessage(t, client1, protocol.SUCCESS_CONNECTED, "You are connected to the server."), ) client2, err := client.Open("ws://"+s.WebServer().GetAddr()+"/stream/user/user2", "http://localhost", 1, false) assert.NoError(t, err) checkConnectedNotificationJSON(t, "user2", expectStatusMessage(t, client2, protocol.SUCCESS_CONNECTED, "You are connected to the server."), ) return s, client1, client2, func() { if client1 != nil { client1.Close() } if client2 != nil { client2.Close() } s.Stop() } } func expectStatusMessage(t *testing.T, client client.Client, name string, arg string) string { select { case notify := <-client.StatusMessages(): assert.Equal(t, name, notify.Name) assert.Equal(t, arg, notify.Arg) return notify.Json case <-time.After(time.Second * 10): t.Logf("no notification of type %s after 2 second", name) t.Fail() return "" } } func checkConnectedNotificationJSON(t *testing.T, user string, connectedJSON string) { m := make(map[string]string) err := json.Unmarshal([]byte(connectedJSON), &m) assert.NoError(t, err) assert.Equal(t, user, m["UserId"]) assert.True(t, len(m["ApplicationId"]) > 0) _, e := time.Parse(time.RFC3339, m["Time"]) assert.NoError(t, e) } //Used only for test and unmarshalling of the json response type Subscriber struct { DeviceToken string `json:"device_token"` UserID string `json:"user_id"` } func TestSubscribersIntegration(t *testing.T) { defer testutil.SkipIfShort(t) defer testutil.SkipIfDisabled(t) defer testutil.ResetDefaultRegistryHealthCheck() defer testutil.EnableDebugForMethod()() a := assert.New(t) s, cleanup := serviceSetUp(t) defer cleanup() subscribeMultipleClients(t, s, 4) a.Nil(nil) restClient := restclient.New(fmt.Sprintf("http://%s/api", s.WebServer().GetAddr())) content, err := restClient.GetSubscribers(testTopic) a.NoError(err) routeParams := make([]*Subscriber, 0) err = json.Unmarshal(content, &routeParams) a.Equal(4, len(routeParams), "Should have 4 subscribers") for i, rp := range routeParams { a.Equal(fmt.Sprintf("gcmId%d", i), rp.DeviceToken) a.Equal(fmt.Sprintf("user%d", i), rp.UserID) } a.NoError(err) } func subscribeMultipleClients(t *testing.T, service *service.Service, noOfClients int) { a := assert.New(t) // create FCM subscription for topic for i := 0; i < noOfClients; i++ { urlFormat := fmt.Sprintf("http://%s/fcm/gcmId%%d/user%%d/%%s", service.WebServer().GetAddr()) url := fmt.Sprintf(urlFormat, i, i, strings.TrimPrefix(testTopic, "/")) response, errPost := http.Post( url, "text/plain", bytes.NewBufferString(""), ) logger.WithField("url", url).Debug("subscribe") a.NoError(errPost) a.Equal(response.StatusCode, 200) body, errReadAll := ioutil.ReadAll(response.Body) a.NoError(errReadAll) a.Equal(fmt.Sprintf(`{"subscribed":"%s"}`, testTopic), string(body)) } } ================================================ FILE: server/kvstore/common_test.go ================================================ package kvstore import ( "github.com/stretchr/testify/assert" "crypto/rand" "io/ioutil" "os" "testing" "time" ) var test1 = []byte("Test1") var test2 = []byte("Test2") var test3 = []byte("Test3") func CommonTestPutGetDelete(t *testing.T, kvs1 KVStore, kvs2 KVStore) { a := assert.New(t) a.NoError(kvs1.Put("s1", "a", test1)) a.NoError(kvs1.Put("s1", "b", test2)) a.NoError(kvs1.Put("s2", "a", test3)) assertGet(a, kvs2, "s1", "a", test1) assertGet(a, kvs2, "s1", "b", test2) assertGet(a, kvs2, "s2", "a", test3) assertGetNoExist(a, kvs2, "no", "thing") kvs2.Delete("s1", "b") assertGetNoExist(a, kvs1, "s1", "b") assertGet(a, kvs1, "s1", "a", test1) assertGet(a, kvs1, "s2", "a", test3) kvs2.Delete("s1", "a") assertGetNoExist(a, kvs1, "s1", "a") assertGet(a, kvs1, "s2", "a", test3) kvs2.Delete("s2", "a") assertGetNoExist(a, kvs1, "s2", "a") } func CommonTestIterate(t *testing.T, kvs1 KVStore, kvs2 KVStore) { a := assert.New(t) a.NoError(kvs1.Put("s1", "bli", test1)) a.NoError(kvs1.Put("s1", "bla", test2)) a.NoError(kvs1.Put("s1", "buu", test3)) a.NoError(kvs1.Put("s2", "bli", test2)) assertChannelContainsEntries(a, kvs2.Iterate("s1", "bl"), [2]string{"bli", string(test1)}, [2]string{"bla", string(test2)}) assertChannelContainsEntries(a, kvs2.Iterate("s1", ""), [2]string{"bli", string(test1)}, [2]string{"bla", string(test2)}, [2]string{"buu", string(test3)}) assertChannelContainsEntries(a, kvs2.Iterate("s1", "bla"), [2]string{"bla", string(test2)}) assertChannelContainsEntries(a, kvs2.Iterate("s1", "nothing")) assertChannelContainsEntries(a, kvs2.Iterate("s2", ""), [2]string{"bli", string(test2)}) } func assertChannelContainsEntries(a *assert.Assertions, entryC chan [2]string, expectedEntries ...[2]string) { var allEntries [][2]string WAITLOOP: for { select { case entry, ok := <-entryC: if !ok { break WAITLOOP } allEntries = append(allEntries, entry) case <-time.After(time.Second): a.Fail("timeout") } } a.Equal(len(expectedEntries), len(allEntries)) for _, expected := range expectedEntries { a.Contains(allEntries, expected) } } func CommonTestIterateKeys(t *testing.T, kvs1 KVStore, kvs2 KVStore) { a := assert.New(t) a.NoError(kvs1.Put("s1", "bli", test1)) a.NoError(kvs1.Put("s1", "bla", test2)) a.NoError(kvs1.Put("s1", "buu", test3)) a.NoError(kvs1.Put("s2", "bli", test2)) assertChannelContains(a, kvs2.IterateKeys("s1", "bl"), "bli", "bla") assertChannelContains(a, kvs2.IterateKeys("s1", ""), "bli", "bla", "buu") assertChannelContains(a, kvs2.IterateKeys("s1", "bla"), "bla") assertChannelContains(a, kvs2.IterateKeys("s1", "nothing")) assertChannelContains(a, kvs2.IterateKeys("s2", ""), "bli") } func assertChannelContains(a *assert.Assertions, entryC chan string, expectedEntries ...string) { var allEntries []string WAITLOOP: for { select { case entry, ok := <-entryC: if !ok { break WAITLOOP } allEntries = append(allEntries, entry) case <-time.After(time.Second): a.Fail("timeout") } } a.Equal(len(expectedEntries), len(allEntries)) for _, expected := range expectedEntries { a.Contains(allEntries, expected) } } func CommonBenchmarkPutGet(b *testing.B, s KVStore) { a := assert.New(b) b.ResetTimer() for n := 0; n < b.N; n++ { data := randString(20) s.Put("bench", data, []byte(data)) val, exist, err := s.Get("bench", data) a.NoError(err) a.True(exist) a.Equal(data, string(val)) } b.StopTimer() } func assertGet(a *assert.Assertions, s KVStore, schema string, key string, expectedValue []byte) { val, exist, err := s.Get(schema, key) a.NoError(err) a.True(exist) a.Equal(expectedValue, val) } func assertGetNoExist(a *assert.Assertions, s KVStore, schema string, key string) { val, exist, err := s.Get(schema, key) a.NoError(err) a.False(exist) a.Nil(val) } func randString(n int) string { const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" var bytes = make([]byte, n) rand.Read(bytes) for i, b := range bytes { bytes[i] = alphanum[b%byte(len(alphanum))] } return string(bytes) } func tempFilename() string { file, err := ioutil.TempFile("/tmp", "guble_store_unittest") if err != nil { panic(err) } file.Close() os.Remove(file.Name()) return file.Name() } ================================================ FILE: server/kvstore/gorm.go ================================================ package kvstore import ( log "github.com/Sirupsen/logrus" "github.com/jinzhu/gorm" "errors" "time" ) const ( responseChannelSize = 100 ) type kvEntry struct { Schema string `gorm:"primary_key"sql:"type:varchar(200)"` Key string `gorm:"primary_key"sql:"type:varchar(200)"` Value []byte `sql:"type:bytea"` UpdatedAt time.Time `` } type kvStore struct { db *gorm.DB logger *log.Entry } func (store *kvStore) Stop() error { if store.db != nil { err := store.db.Close() store.db = nil return err } return nil } func (store *kvStore) Check() error { if store.db == nil { errorMessage := "Error: Database is not initialized (nil)" store.logger.Error(errorMessage) return errors.New(errorMessage) } if err := store.db.DB().Ping(); err != nil { store.logger.WithField("error", err.Error()).Error("Error pinging database") return err } return nil } func (store *kvStore) Put(schema, key string, value []byte) error { if err := store.Delete(schema, key); err != nil { return err } entry := &kvEntry{Schema: schema, Key: key, Value: value, UpdatedAt: time.Now()} return store.db.Create(entry).Error } func (store *kvStore) Get(schema, key string) ([]byte, bool, error) { entry := &kvEntry{} if err := store.db.First(&entry, "schema = ? and key = ?", schema, key).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, false, nil } return nil, false, err } return entry.Value, true, nil } func (store *kvStore) Iterate(schema string, keyPrefix string) chan [2]string { responseC := make(chan [2]string, responseChannelSize) go func() { rows, err := store.db.Raw("select key, value from kv_entry where schema = ? and key LIKE ?", schema, keyPrefix+"%"). Rows() if err != nil { store.logger.WithField("error", err.Error()).Error("Error fetching keys from database") } else { defer rows.Close() for rows.Next() { var key, value string rows.Scan(&key, &value) responseC <- [2]string{key, value} } } close(responseC) }() return responseC } func (store *kvStore) IterateKeys(schema string, keyPrefix string) chan string { responseC := make(chan string, responseChannelSize) go func() { rows, err := store.db.Raw("select key from kv_entry where schema = ? and key LIKE ?", schema, keyPrefix+"%"). Rows() if err != nil { store.logger.WithField("error", err.Error()).Error("Error fetching keys from database") } else { defer rows.Close() for rows.Next() { var value string rows.Scan(&value) responseC <- value } } close(responseC) }() return responseC } func (store *kvStore) Delete(schema, key string) error { return store.db.Delete(&kvEntry{Schema: schema, Key: key}).Error } ================================================ FILE: server/kvstore/kvstore.go ================================================ package kvstore // KVStore is an interface for a persistence backend, storing key-value pairs. type KVStore interface { // Put stores an entry in the key-value store Put(schema, key string, value []byte) error // Get fetches one entry Get(schema, key string) (value []byte, exist bool, err error) // Delete an entry Delete(schema, key string) error // Iterate iterates over all entries in the key value store. // The result will be sent to the channel, which is closed after the last entry. // For simplicity, the return type is an string array with key, value. // If you have binary values, you can safely cast back to []byte. Iterate(schema, keyPrefix string) (entries chan [2]string) // IterateKeys iterates over all keys in the key value store. // The keys will be sent to the channel, which is closed after the last entry. IterateKeys(schema, keyPrefix string) (keys chan string) } ================================================ FILE: server/kvstore/memory.go ================================================ package kvstore import ( "strings" "sync" ) // MemoryKVStore is a struct representing an in-memory key-value store. type MemoryKVStore struct { data map[string]map[string][]byte mutex sync.RWMutex } // NewMemoryKVStore returns a new configured MemoryKVStore. func NewMemoryKVStore() *MemoryKVStore { return &MemoryKVStore{ data: make(map[string]map[string][]byte), } } // Put implements the `kvstore` Put func. func (kvStore *MemoryKVStore) Put(schema, key string, value []byte) error { kvStore.mutex.Lock() defer kvStore.mutex.Unlock() s := kvStore.getSchema(schema) s[key] = value return nil } // Get implements the `kvstore` Get func. func (kvStore *MemoryKVStore) Get(schema, key string) ([]byte, bool, error) { kvStore.mutex.Lock() defer kvStore.mutex.Unlock() s := kvStore.getSchema(schema) if v, ok := s[key]; ok { return v, true, nil } return nil, false, nil } // Delete implements the `kvstore` Delete func. func (kvStore *MemoryKVStore) Delete(schema, key string) error { kvStore.mutex.Lock() defer kvStore.mutex.Unlock() s := kvStore.getSchema(schema) delete(s, key) return nil } // Iterate iterates over the key-value pairs in the schema, with keys matching the keyPrefix. // TODO: this can lead to a deadlock, if the consumer modifies the store while receiving and the channel blocks func (kvStore *MemoryKVStore) Iterate(schema string, keyPrefix string) chan [2]string { responseChan := make(chan [2]string, 100) kvStore.mutex.Lock() s := kvStore.getSchema(schema) kvStore.mutex.Unlock() go func() { kvStore.mutex.Lock() for key, value := range s { if strings.HasPrefix(key, keyPrefix) { responseChan <- [2]string{key, string(value)} } } kvStore.mutex.Unlock() close(responseChan) }() return responseChan } // IterateKeys iterates over the keys in the schema, matching the keyPrefix. // TODO: this can lead to a deadlock, if the consumer modifies the store while receiving and the channel blocks func (kvStore *MemoryKVStore) IterateKeys(schema string, keyPrefix string) chan string { responseChan := make(chan string, 100) kvStore.mutex.Lock() s := kvStore.getSchema(schema) kvStore.mutex.Unlock() go func() { kvStore.mutex.Lock() for key := range s { if strings.HasPrefix(key, keyPrefix) { responseChan <- key } } kvStore.mutex.Unlock() close(responseChan) }() return responseChan } func (kvStore *MemoryKVStore) getSchema(schema string) map[string][]byte { if s, ok := kvStore.data[schema]; ok { return s } s := make(map[string][]byte) kvStore.data[schema] = s return s } ================================================ FILE: server/kvstore/memory_test.go ================================================ package kvstore import ( "testing" ) func TestMemoryPutGetDelete(t *testing.T) { mkvs := NewMemoryKVStore() CommonTestPutGetDelete(t, mkvs, mkvs) } func TestMemoryIterateKeys(t *testing.T) { mkvs := NewMemoryKVStore() CommonTestIterateKeys(t, mkvs, mkvs) } func TestMemoryIterate(t *testing.T) { mkvs := NewMemoryKVStore() CommonTestIterate(t, mkvs, mkvs) } func BenchmarkMemoryPutGet(b *testing.B) { CommonBenchmarkPutGet(b, NewMemoryKVStore()) } ================================================ FILE: server/kvstore/postgres.go ================================================ package kvstore import ( log "github.com/Sirupsen/logrus" "github.com/jinzhu/gorm" // use gorm's postgres dialect _ "github.com/jinzhu/gorm/dialects/postgres" ) const postgresGormLogMode = false // PostgresKVStore extends a gorm-based kvStore with a Postgresql-specific configuration. type PostgresKVStore struct { *kvStore config PostgresConfig } // NewPostgresKVStore returns a new configured PostgresKVStore (not opened yet). func NewPostgresKVStore(postgresConfig PostgresConfig) *PostgresKVStore { return &PostgresKVStore{ kvStore: &kvStore{logger: log.WithFields(log.Fields{"module": "kv-postgres"})}, config: postgresConfig, } } // Open a connection to Postgresql database, or return an error. func (kvStore *PostgresKVStore) Open() error { logger := kvStore.logger.WithField("config", kvStore.config) logger.Info("Opening database") gormdb, err := gorm.Open("postgres", kvStore.config.connectionString()) if err != nil { logger.WithField("err", err).Error("Error opening database") return err } if err := gormdb.DB().Ping(); err != nil { kvStore.logger.WithField("error", err.Error()).Error("Error pinging database") } else { kvStore.logger.Info("Ping reply from database") } gormdb.LogMode(postgresGormLogMode) gormdb.SingularTable(true) //TODO MARIAN REMOVE THIS AFTER BUG gormdb.DB().SetMaxIdleConns(-1) gormdb.DB().SetMaxOpenConns(kvStore.config.MaxOpenConns) //TODO MARIAN maybe config //gormdb.DB().SetConnMaxLifetime(2 * time.Minute) if err := gormdb.AutoMigrate(&kvEntry{}).Error; err != nil { logger.WithField("err", err).Error("Error in schema migration") return err } logger.Info("Ensured database schema") kvStore.db = gormdb return nil } ================================================ FILE: server/kvstore/postgres_config.go ================================================ package kvstore import "strings" // PostgresConfig is a map-based configuration of a Postgresql connection (dbname, host etc.), // extended with gorm-specific parameters (e.g. number of open / idle connections). type PostgresConfig struct { ConnParams map[string]string MaxIdleConns int MaxOpenConns int } func (pc PostgresConfig) connectionString() string { var params []string for key, value := range pc.ConnParams { params = append(params, key+"="+value) } return strings.Join(params, " ") } ================================================ FILE: server/kvstore/postgres_config_test.go ================================================ package kvstore import ( "github.com/stretchr/testify/assert" "testing" ) func TestPostgresConfig_String(t *testing.T) { a := assert.New(t) pc0 := PostgresConfig{map[string]string{}, 1, 1} a.Equal(pc0.connectionString(), "") pc1 := PostgresConfig{map[string]string{"key": "value"}, 1, 1} a.Equal(pc1.connectionString(), "key=value") pc2 := PostgresConfig{map[string]string{"key": "value", "password": "secret"}, 1, 1} s := pc2.connectionString() a.True(s == "key=value password=secret" || s == "password=secret key=value") } ================================================ FILE: server/kvstore/postgres_test.go ================================================ package kvstore import ( "github.com/stretchr/testify/assert" "testing" ) func BenchmarkPostgresKVStore_PutGet(b *testing.B) { kvs := NewPostgresKVStore(aPostgresConfig()) kvs.Open() CommonBenchmarkPutGet(b, kvs) } func TestPostgresKVStore_PutGetDelete(t *testing.T) { kvs := NewPostgresKVStore(aPostgresConfig()) kvs.Open() CommonTestPutGetDelete(t, kvs, kvs) } func TestPostgresKVStore_Iterate(t *testing.T) { kvs := NewPostgresKVStore(aPostgresConfig()) kvs.Open() CommonTestIterate(t, kvs, kvs) } func TestPostgresKVStore_IterateKeys(t *testing.T) { kvs := NewPostgresKVStore(aPostgresConfig()) kvs.Open() CommonTestIterateKeys(t, kvs, kvs) } func TestPostgresKVStore_Check(t *testing.T) { a := assert.New(t) kvs := NewPostgresKVStore(aPostgresConfig()) kvs.Open() err := kvs.Check() a.NoError(err, "Db ping should work") kvs.Stop() err = kvs.Check() a.NotNil(err, "Check should fail because db was already closed") } func TestPostgresKVStore_Open(t *testing.T) { kvs := NewPostgresKVStore(invalidPostgresConfig()) err := kvs.Open() assert.NotNil(t, err) } // This config assumes a postgresql running locally func aPostgresConfig() PostgresConfig { return PostgresConfig{ ConnParams: map[string]string{ "host": "localhost", "user": "postgres", "password": "", "dbname": "guble", "sslmode": "disable", }, MaxIdleConns: 1, MaxOpenConns: 1, } } func invalidPostgresConfig() PostgresConfig { return PostgresConfig{ ConnParams: map[string]string{ "host": "localhost", "user": "", "password": "", "dbname": "", "sslmode": "disable", }, MaxIdleConns: 1, MaxOpenConns: 1, } } ================================================ FILE: server/kvstore/sqlite.go ================================================ package kvstore import ( // use this as gorm's sqlite dialect / implementation _ "github.com/mattn/go-sqlite3" "github.com/jinzhu/gorm" log "github.com/Sirupsen/logrus" "fmt" "io/ioutil" "os" "path" "path/filepath" ) const ( sqliteMaxIdleConns = 2 sqliteMaxOpenConns = 5 sqliteGormLogMode = false ) var writeTestFilename = "db_testfile" // SqliteKVStore is a struct representing a sqlite database which embeds a kvStore. type SqliteKVStore struct { *kvStore filename string syncOnWrite bool } // NewSqliteKVStore returns a new configured SqliteKVStore (not opened yet). func NewSqliteKVStore(filename string, syncOnWrite bool) *SqliteKVStore { return &SqliteKVStore{ kvStore: &kvStore{logger: log.WithFields(log.Fields{ "module": "kv-sqlite", "filename": filename, "syncOnWrite": syncOnWrite, })}, filename: filename, syncOnWrite: syncOnWrite, } } // Open opens the database file. If the directory does not exist, it will be created. func (kvStore *SqliteKVStore) Open() error { directoryPath := filepath.Dir(kvStore.filename) if err := ensureWriteableDirectory(directoryPath); err != nil { kvStore.logger.WithError(err).Error("DB Directory is not writeable") return err } kvStore.logger.Info("Opening database") gormdb, err := gorm.Open("sqlite3", kvStore.filename) if err != nil { kvStore.logger.WithError(err).Error("Error opening database") return err } if err := gormdb.DB().Ping(); err != nil { kvStore.logger.WithError(err).Error("Error pinging database") return err } kvStore.logger.Info("Ping reply from database") gormdb.LogMode(sqliteGormLogMode) gormdb.SingularTable(true) gormdb.DB().SetMaxIdleConns(sqliteMaxIdleConns) gormdb.DB().SetMaxOpenConns(sqliteMaxOpenConns) if err := gormdb.AutoMigrate(&kvEntry{}).Error; err != nil { kvStore.logger.WithError(err).Error("Error in schema migration") return err } kvStore.logger.Info("Ensured database schema") if !kvStore.syncOnWrite { kvStore.logger.Info("Setting db: PRAGMA synchronous = OFF") if err := gormdb.Exec("PRAGMA synchronous = OFF").Error; err != nil { kvStore.logger.WithError(err).Error("Error setting PRAGMA synchronous = OFF") return err } } kvStore.db = gormdb return nil } func ensureWriteableDirectory(dir string) error { dirInfo, errStat := os.Stat(dir) if os.IsNotExist(errStat) { if errMkdir := os.MkdirAll(dir, 0755); errMkdir != nil { return errMkdir } dirInfo, errStat = os.Stat(dir) } if errStat != nil || !dirInfo.IsDir() { return fmt.Errorf("kv-sqlite: not a directory %v", dir) } writeTest := path.Join(dir, writeTestFilename) if err := ioutil.WriteFile(writeTest, []byte("writeTest"), 0644); err != nil { return err } if err := os.Remove(writeTest); err != nil { return err } return nil } ================================================ FILE: server/kvstore/sqlite_test.go ================================================ package kvstore import ( "github.com/stretchr/testify/assert" "os" "testing" ) func BenchmarkSqlitePutGet(b *testing.B) { f := tempFilename() defer os.Remove(f) db := NewSqliteKVStore(f, false) db.Open() CommonBenchmarkPutGet(b, db) } func TestSqlitePutGetDelete(t *testing.T) { f := tempFilename() defer os.Remove(f) db := NewSqliteKVStore(f, false) db.Open() CommonTestPutGetDelete(t, db, db) } func TestSqliteIterate(t *testing.T) { f := tempFilename() defer os.Remove(f) db := NewSqliteKVStore(f, false) db.Open() CommonTestIterate(t, db, db) } func TestSqliteIterateKeys(t *testing.T) { f := tempFilename() defer os.Remove(f) db := NewSqliteKVStore(f, false) db.Open() CommonTestIterateKeys(t, db, db) } func TestCheck_SqlKVStore(t *testing.T) { a := assert.New(t) f := tempFilename() defer os.Remove(f) kvs := NewSqliteKVStore(f, false) kvs.Open() err := kvs.Check() a.Nil(err, "Db ping should work") kvs.Stop() err = kvs.Check() a.NotNil(err, "Check should fail because db was already closed") } ================================================ FILE: server/logger.go ================================================ package server import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithFields(log.Fields{ "module": "server", }) ================================================ FILE: server/metrics/average.go ================================================ package metrics import ( "fmt" ) type average struct { value string } func newAverage(total, cases, scale int64, defaultAverageJSONValue string) average { if cases <= 0 || scale <= 0 { return average{defaultAverageJSONValue} } return average{fmt.Sprintf("%v", float64(total)/float64(cases*scale))} } func (a average) String() string { return a.value } ================================================ FILE: server/metrics/average_test.go ================================================ package metrics import ( "github.com/stretchr/testify/assert" "testing" ) func TestAverage_String(t *testing.T) { assert.Equal(t, "x", newAverage(0, 0, 0, "x").String()) assert.Equal(t, "x", newAverage(0, 1, 0, "x").String()) assert.Equal(t, "0", newAverage(0, 1, 1, "").String()) assert.Equal(t, "1", newAverage(1, 1, 1, "").String()) assert.Equal(t, "2", newAverage(40, 2, 10, "").String()) } ================================================ FILE: server/metrics/disabled.go ================================================ // +build disablemetrics package metrics import ( "expvar" "time" ) type dummyInt struct{} // Dummy functions on dummyInt func (v *dummyInt) Add(delta int64) {} func (v *dummyInt) Set(value int64) {} // NewInt returns a dummyInt, depending on the build tag declared at the beginning of this file. func NewInt(name string) Int { return &dummyInt{} } type dummyMap struct{} // Dummy functions on dummyMap func (v *dummyMap) Init() *expvar.Map { return nil } func (v *dummyMap) Get(key string) expvar.Var { return nil } func (v *dummyMap) Set(key string, av expvar.Var) {} func (v *dummyMap) Add(key string, delta int64) {} // NewMap returns a dummyMap, depending on the build tag declared at the beginning of this file. func NewMap(name string) Map { return &dummyMap{} } func RegisterInterval(m Map, td time.Duration, reset func(Map, time.Time), processAndReset func(Map, time.Duration, time.Time)) { } ================================================ FILE: server/metrics/enabled.go ================================================ // +build !disablemetrics package metrics import ( "context" "expvar" "time" ) // NewInt returns an expvar Int, depending on the absence of build tag declared at the beginning of this file func NewInt(name string) Int { return expvar.NewInt(name) } func NewMap(name string) Map { return expvar.NewMap(name) } func RegisterInterval(ctx context.Context, m Map, td time.Duration, reset func(Map, time.Time), processAndReset func(Map, time.Duration, time.Time)) { reset(m, time.Now()) go func(m Map, td time.Duration, processAndReset func(Map, time.Duration, time.Time)) { for { select { case t := <-time.Tick(td): processAndReset(m, td, t) case <-ctx.Done(): return } } }(m, td, processAndReset) } ================================================ FILE: server/metrics/enabled_test.go ================================================ package metrics import ( "github.com/stretchr/testify/assert" "expvar" "testing" ) func TestNewInt(t *testing.T) { _, ok := NewInt("a_name").(expvar.Var) assert.True(t, ok) } ================================================ FILE: server/metrics/int.go ================================================ package metrics // Int is an interface for some of the operations defined on expvar.Int type Int interface { Add(int64) Set(int64) } ================================================ FILE: server/metrics/map.go ================================================ package metrics import ( "expvar" "strconv" "time" ) // Map is an interface for some of the operations defined on expvar.Map type Map interface { Init() *expvar.Map Get(key string) expvar.Var Set(key string, av expvar.Var) Add(key string, delta int64) } func SetRate(m Map, key string, value expvar.Var, timeframe, unit time.Duration) { if value != nil { v, err := strconv.ParseInt(value.String(), 10, 64) if err != nil { m.Set(key, zeroValue) } m.Set(key, newRate(v, timeframe, unit)) } else { m.Set(key, zeroValue) } } func SetAverage(m Map, key string, totalVar, casesVar expvar.Var, scale int64, defaultValue string) { if totalVar != nil && casesVar != nil { total, err1 := strconv.ParseInt(totalVar.String(), 10, 64) cases, err2 := strconv.ParseInt(casesVar.String(), 10, 64) if err1 != nil || err2 != nil { m.Set(key, zeroValue) } m.Set(key, newAverage(total, cases, scale, defaultValue)) } else { m.Set(key, zeroValue) } } func AddToMaps(key string, value int64, maps ...Map) { for _, m := range maps { m.Add(key, value) } } ================================================ FILE: server/metrics/metrics.go ================================================ // Package metrics implements simple general counter-metrics. // Metrics are enabled by default. If you want to disable metrics, build with: // go build -tags disablemetrics package metrics import ( log "github.com/Sirupsen/logrus" "expvar" "fmt" "io" "net/http" "runtime" ) var ( logger = log.WithField("module", "metrics") numGoroutines = expvar.NewInt("num_goroutines") ) const ( DefaultAverageLatencyJSONValue = "\"\"" MilliPerNano = 1000000 ) // HttpHandler is a HTTP handler writing the current metrics to the http.ResponseWriter func HttpHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json; charset=utf-8") writeMetrics(rw) } func writeMetrics(w io.Writer) { numGoroutines.Set(int64(runtime.NumGoroutine())) fmt.Fprint(w, "{\n") first := true expvar.Do(func(kv expvar.KeyValue) { if !first { fmt.Fprint(w, ",\n") } first = false fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value) }) fmt.Fprint(w, "\n}\n") } // LogOnDebugLevel logs all the current metrics, if logging is on Debug level. func LogOnDebugLevel() { if log.GetLevel() == log.DebugLevel { fields := log.Fields{} expvar.Do(func(kv expvar.KeyValue) { fields[kv.Key] = kv.Value }) logger.WithFields(fields).Debug("current values of metrics") } } ================================================ FILE: server/metrics/metrics_test.go ================================================ package metrics import ( "github.com/stretchr/testify/assert" log "github.com/Sirupsen/logrus" "bytes" "io/ioutil" "net/http" "net/http/httptest" "testing" ) func TestHttpHandler_MetricsNotEnabled(t *testing.T) { a := assert.New(t) req, _ := http.NewRequest("GET", "", nil) w := httptest.NewRecorder() HttpHandler(w, req) a.Equal(http.StatusOK, w.Code) b, err := ioutil.ReadAll(w.Body) a.NoError(err) a.True(len(b) > 0) log.Debugf("%s", b) } func TestLogOnDebugLevel_Debug(t *testing.T) { a := assert.New(t) bufferDebug := bytes.NewBuffer([]byte{}) log.SetOutput(bufferDebug) log.SetLevel(log.DebugLevel) LogOnDebugLevel() logContent, err := ioutil.ReadAll(bufferDebug) log.Debugf("%s", logContent) a.NoError(err) a.Contains(string(logContent), "cmdline") a.Contains(string(logContent), "memstats") } func TestLogOnDebugLevel_Info(t *testing.T) { a := assert.New(t) bufferInfo := bytes.NewBuffer([]byte{}) log.SetOutput(bufferInfo) log.SetLevel(log.InfoLevel) logContent, err := ioutil.ReadAll(bufferInfo) a.NoError(err) LogOnDebugLevel() a.True(len(logContent) == 0) } ================================================ FILE: server/metrics/ns.go ================================================ package metrics const sep = "." //NS is a namespace type NS string func (ns NS) NewInt(key string) Int { return NewInt(string(ns) + sep + key) } func (ns NS) NewMap(key string) Map { return NewMap(string(ns) + sep + key) } func (ns NS) NewNS(childKey string) NS { return NS(string(ns) + sep + childKey) } ================================================ FILE: server/metrics/rate.go ================================================ package metrics import ( "fmt" "time" ) type rate struct { value string } func newRate(value int64, timeframe, scale time.Duration) rate { if value <= 0 || timeframe <= 0 || scale <= 0 { return rate{"0"} } return rate{fmt.Sprintf("%v", float64(value*scale.Nanoseconds())/float64(timeframe.Nanoseconds()))} } func (r rate) String() string { return r.value } ================================================ FILE: server/metrics/rate_test.go ================================================ package metrics import ( "github.com/stretchr/testify/assert" "testing" ) func TestRate_String(t *testing.T) { assert.Equal(t, "0", newRate(0, 0, 0).String()) assert.Equal(t, "0", newRate(1, 0, 0).String()) assert.Equal(t, "1", newRate(1, 1, 1).String()) assert.Equal(t, "1.5", newRate(90, 60000, 1000).String()) assert.Equal(t, "1.6666666666666667", newRate(100, 60000, 1000).String()) } ================================================ FILE: server/metrics/time.go ================================================ package metrics import ( "fmt" "time" ) type Time struct { timeValue time.Time } func NewTime(timeValue time.Time) Time { return Time{timeValue: timeValue} } func (t Time) String() string { return fmt.Sprintf("\"%v\"", t.timeValue) } ================================================ FILE: server/metrics/zero.go ================================================ package metrics type zeroVar struct { } func (z zeroVar) String() string { return "0" } var zeroValue zeroVar ================================================ FILE: server/mocks_apns_pusher_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/apns (interfaces: Pusher) package server import ( gomock "github.com/golang/mock/gomock" apns2 "github.com/sideshow/apns2" ) // Mock of Pusher interface type MockPusher struct { ctrl *gomock.Controller recorder *_MockPusherRecorder } // Recorder for MockPusher (not exported) type _MockPusherRecorder struct { mock *MockPusher } func NewMockPusher(ctrl *gomock.Controller) *MockPusher { mock := &MockPusher{ctrl: ctrl} mock.recorder = &_MockPusherRecorder{mock} return mock } func (_m *MockPusher) EXPECT() *_MockPusherRecorder { return _m.recorder } func (_m *MockPusher) Push(_param0 *apns2.Notification) (*apns2.Response, error) { ret := _m.ctrl.Call(_m, "Push", _param0) ret0, _ := ret[0].(*apns2.Response) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockPusherRecorder) Push(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Push", arg0) } ================================================ FILE: server/mocks_auth_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/auth (interfaces: AccessManager) package server import ( gomock "github.com/golang/mock/gomock" protocol "github.com/smancke/guble/protocol" auth "github.com/smancke/guble/server/auth" ) // Mock of AccessManager interface type MockAccessManager struct { ctrl *gomock.Controller recorder *_MockAccessManagerRecorder } // Recorder for MockAccessManager (not exported) type _MockAccessManagerRecorder struct { mock *MockAccessManager } func NewMockAccessManager(ctrl *gomock.Controller) *MockAccessManager { mock := &MockAccessManager{ctrl: ctrl} mock.recorder = &_MockAccessManagerRecorder{mock} return mock } func (_m *MockAccessManager) EXPECT() *_MockAccessManagerRecorder { return _m.recorder } func (_m *MockAccessManager) IsAllowed(_param0 auth.AccessType, _param1 string, _param2 protocol.Path) bool { ret := _m.ctrl.Call(_m, "IsAllowed", _param0, _param1, _param2) ret0, _ := ret[0].(bool) return ret0 } func (_mr *_MockAccessManagerRecorder) IsAllowed(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "IsAllowed", arg0, arg1, arg2) } ================================================ FILE: server/mocks_router_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/router (interfaces: Router) package server import ( "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/cluster" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" ) // Mock of Router interface type MockRouter struct { ctrl *gomock.Controller recorder *_MockRouterRecorder } // Recorder for MockRouter (not exported) type _MockRouterRecorder struct { mock *MockRouter } func NewMockRouter(ctrl *gomock.Controller) *MockRouter { mock := &MockRouter{ctrl: ctrl} mock.recorder = &_MockRouterRecorder{mock} return mock } func (_m *MockRouter) EXPECT() *_MockRouterRecorder { return _m.recorder } func (_m *MockRouter) AccessManager() (auth.AccessManager, error) { ret := _m.ctrl.Call(_m, "AccessManager") ret0, _ := ret[0].(auth.AccessManager) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) AccessManager() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "AccessManager") } func (_m *MockRouter) Cluster() *cluster.Cluster { ret := _m.ctrl.Call(_m, "Cluster") ret0, _ := ret[0].(*cluster.Cluster) return ret0 } func (_mr *_MockRouterRecorder) Cluster() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Cluster") } func (_m *MockRouter) Done() <-chan bool { ret := _m.ctrl.Call(_m, "Done") ret0, _ := ret[0].(<-chan bool) return ret0 } func (_mr *_MockRouterRecorder) Done() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Done") } func (_m *MockRouter) Fetch(_param0 *store.FetchRequest) error { ret := _m.ctrl.Call(_m, "Fetch", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockRouter) GetSubscribers(_param0 string) ([]byte, error) { ret := _m.ctrl.Call(_m, "GetSubscribers", _param0) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) GetSubscribers(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetSubscribers", arg0) } func (_m *MockRouter) HandleMessage(_param0 *protocol.Message) error { ret := _m.ctrl.Call(_m, "HandleMessage", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) HandleMessage(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "HandleMessage", arg0) } func (_m *MockRouter) KVStore() (kvstore.KVStore, error) { ret := _m.ctrl.Call(_m, "KVStore") ret0, _ := ret[0].(kvstore.KVStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) KVStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "KVStore") } func (_m *MockRouter) MessageStore() (store.MessageStore, error) { ret := _m.ctrl.Call(_m, "MessageStore") ret0, _ := ret[0].(store.MessageStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) MessageStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MessageStore") } func (_m *MockRouter) Subscribe(_param0 *router.Route) (*router.Route, error) { ret := _m.ctrl.Call(_m, "Subscribe", _param0) ret0, _ := ret[0].(*router.Route) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) Subscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Subscribe", arg0) } func (_m *MockRouter) Unsubscribe(_param0 *router.Route) { _m.ctrl.Call(_m, "Unsubscribe", _param0) } func (_mr *_MockRouterRecorder) Unsubscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Unsubscribe", arg0) } ================================================ FILE: server/mocks_store_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/store (interfaces: MessageStore) package server import ( gomock "github.com/golang/mock/gomock" protocol "github.com/smancke/guble/protocol" store "github.com/smancke/guble/server/store" ) // Mock of MessageStore interface type MockMessageStore struct { ctrl *gomock.Controller recorder *_MockMessageStoreRecorder } // Recorder for MockMessageStore (not exported) type _MockMessageStoreRecorder struct { mock *MockMessageStore } func NewMockMessageStore(ctrl *gomock.Controller) *MockMessageStore { mock := &MockMessageStore{ctrl: ctrl} mock.recorder = &_MockMessageStoreRecorder{mock} return mock } func (_m *MockMessageStore) EXPECT() *_MockMessageStoreRecorder { return _m.recorder } func (_m *MockMessageStore) DoInTx(_param0 string, _param1 func(uint64) error) error { ret := _m.ctrl.Call(_m, "DoInTx", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockMessageStoreRecorder) DoInTx(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "DoInTx", arg0, arg1) } func (_m *MockMessageStore) Fetch(_param0 *store.FetchRequest) { _m.ctrl.Call(_m, "Fetch", _param0) } func (_mr *_MockMessageStoreRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockMessageStore) GenerateNextMsgID(_param0 string, _param1 byte) (uint64, int64, error) { ret := _m.ctrl.Call(_m, "GenerateNextMsgID", _param0, _param1) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(int64) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } func (_mr *_MockMessageStoreRecorder) GenerateNextMsgID(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GenerateNextMsgID", arg0, arg1) } func (_m *MockMessageStore) MaxMessageID(_param0 string) (uint64, error) { ret := _m.ctrl.Call(_m, "MaxMessageID", _param0) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) MaxMessageID(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MaxMessageID", arg0) } func (_m *MockMessageStore) Partition(_param0 string) (store.MessagePartition, error) { ret := _m.ctrl.Call(_m, "Partition", _param0) ret0, _ := ret[0].(store.MessagePartition) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) Partition(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Partition", arg0) } func (_m *MockMessageStore) Partitions() ([]store.MessagePartition, error) { ret := _m.ctrl.Call(_m, "Partitions") ret0, _ := ret[0].([]store.MessagePartition) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) Partitions() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Partitions") } func (_m *MockMessageStore) Store(_param0 string, _param1 uint64, _param2 []byte) error { ret := _m.ctrl.Call(_m, "Store", _param0, _param1, _param2) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockMessageStoreRecorder) Store(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Store", arg0, arg1, arg2) } func (_m *MockMessageStore) StoreMessage(_param0 *protocol.Message, _param1 byte) (int, error) { ret := _m.ctrl.Call(_m, "StoreMessage", _param0, _param1) ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) StoreMessage(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "StoreMessage", arg0, arg1) } ================================================ FILE: server/redundancy_test.go ================================================ package server import ( "testing" "time" "github.com/smancke/guble/server/fcm" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" ) func Test_Subscribe_on_random_node(t *testing.T) { testutil.SkipIfShort(t) a := assert.New(t) node1 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8080", NodeID: 1, NodePort: 20000, Remotes: "localhost:20000", }) a.NotNil(node1) defer node1.cleanup(true) node2 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8081", NodeID: 2, NodePort: 20001, Remotes: "localhost:20000", }) a.NotNil(node2) defer node2.cleanup(true) node1.FCM.setupRoundTripper(20*time.Millisecond, 10, fcm.SuccessFCMResponse) node2.FCM.setupRoundTripper(20*time.Millisecond, 10, fcm.SuccessFCMResponse) // subscribe on first node node1.Subscribe(testTopic, "1") // connect a client and send a message client1, err := node1.client("user1", 1000, true) a.NoError(err) err = client1.Send(testTopic, "body", "{jsonHeader:1}") a.NoError(err) // only one message should be received but only on the first node. // Every message should be delivered only once. node1.FCM.checkReceived(1) node2.FCM.checkReceived(0) } func Test_Subscribe_working_After_Node_Restart(t *testing.T) { // defer testutil.EnableDebugForMethod()() testutil.SkipIfDisabled(t) testutil.SkipIfShort(t) a := assert.New(t) nodeConfig1 := testClusterNodeConfig{ HttpListen: "localhost:8082", NodeID: 1, NodePort: 20002, Remotes: "localhost:20002", } node1 := newTestClusterNode(t, nodeConfig1) a.NotNil(node1) node2 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8083", NodeID: 2, NodePort: 20003, Remotes: "localhost:20002", }) a.NotNil(node2) defer node2.cleanup(true) node1.FCM.setupRoundTripper(20*time.Millisecond, 10, fcm.SuccessFCMResponse) node2.FCM.setupRoundTripper(20*time.Millisecond, 10, fcm.SuccessFCMResponse) // subscribe on first node node1.Subscribe(testTopic, "1") // connect a clinet and send a message client1, err := node1.client("user1", 1000, true) a.NoError(err) err = client1.Send(testTopic, "body", "{jsonHeader:1}") a.NoError(err) // one message should be received but only on the first node. // Every message should be delivered only once. node1.FCM.checkReceived(1) node2.FCM.checkReceived(0) // stop a node, cleanup without removing directories node1.cleanup(false) time.Sleep(time.Millisecond * 150) // restart the service restartedNode1 := newTestClusterNode(t, nodeConfig1) a.NotNil(restartedNode1) defer restartedNode1.cleanup(true) restartedNode1.FCM.setupRoundTripper(20*time.Millisecond, 10, fcm.SuccessFCMResponse) // send a message to the former subscription. client1, err = restartedNode1.client("user1", 1000, true) a.NoError(err) time.Sleep(time.Second) err = client1.Send(testTopic, "body", "{jsonHeader:1}") a.NoError(err, "Subscription should work even after node restart") // only one message should be received but only on the first node. // Every message should be delivered only once. restartedNode1.FCM.checkReceived(1) node2.FCM.checkReceived(0) } func Test_Independent_Receiving(t *testing.T) { testutil.SkipIfDisabled(t) testutil.SkipIfShort(t) a := assert.New(t) node1 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8084", NodeID: 1, NodePort: 20004, Remotes: "localhost:20004", }) a.NotNil(node1) defer node1.cleanup(true) node2 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8085", NodeID: 2, NodePort: 20005, Remotes: "localhost:20004", }) a.NotNil(node2) defer node2.cleanup(true) node1.FCM.setupRoundTripper(20*time.Millisecond, 10, fcm.SuccessFCMResponse) node2.FCM.setupRoundTripper(20*time.Millisecond, 10, fcm.SuccessFCMResponse) // subscribe on first node node1.Subscribe(testTopic, "1") // connect a client and send a message client1, err := node1.client("user1", 1000, true) err = client1.Send(testTopic, "body", "{jsonHeader:1}") a.NoError(err) // only one message should be received but only on the first node. // Every message should be delivered only once. node1.FCM.checkReceived(1) node2.FCM.checkReceived(0) // reset the counter node1.FCM.reset() // NOW connect to second node client2, err := node2.client("user2", 1000, true) a.NoError(err) err = client2.Send(testTopic, "body", "{jsonHeader:1}") a.NoError(err) // only one message should be received but only on the second node. // Every message should be delivered only once. node1.FCM.checkReceived(0) node2.FCM.checkReceived(1) } func Test_NoReceiving_After_Unsubscribe(t *testing.T) { testutil.SkipIfDisabled(t) testutil.SkipIfShort(t) a := assert.New(t) node1 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8086", NodeID: 1, NodePort: 20006, Remotes: "localhost:20006", }) a.NotNil(node1) defer node1.cleanup(true) node2 := newTestClusterNode(t, testClusterNodeConfig{ HttpListen: "localhost:8087", NodeID: 2, NodePort: 20007, Remotes: "localhost:20006", }) a.NotNil(node2) defer node2.cleanup(true) node1.FCM.setupRoundTripper(20*time.Millisecond, 10, fcm.SuccessFCMResponse) node2.FCM.setupRoundTripper(20*time.Millisecond, 10, fcm.SuccessFCMResponse) // subscribe on first node node1.Subscribe(testTopic, "1") time.Sleep(50 * time.Millisecond) // connect a client and send a message client1, err := node1.client("user1", 1000, true) err = client1.Send(testTopic, "body", "{jsonHeader:1}") a.NoError(err) // only one message should be received but only on the first node. // Every message should be delivered only once. node1.FCM.checkReceived(1) node2.FCM.checkReceived(0) // Unsubscribe node2.Unsubscribe(testTopic, "1") time.Sleep(50 * time.Millisecond) // reset the counter node1.FCM.reset() // and send a message again. No one should receive it err = client1.Send(testTopic, "body", "{jsonHeader:1}") a.NoError(err) // only one message should be received but only on the second node. // Every message should be delivered only once. node1.FCM.checkReceived(0) node2.FCM.checkReceived(0) } ================================================ FILE: server/rest/mocks_router_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/router (interfaces: Router) package rest import ( "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/cluster" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" ) // Mock of Router interface type MockRouter struct { ctrl *gomock.Controller recorder *_MockRouterRecorder } // Recorder for MockRouter (not exported) type _MockRouterRecorder struct { mock *MockRouter } func NewMockRouter(ctrl *gomock.Controller) *MockRouter { mock := &MockRouter{ctrl: ctrl} mock.recorder = &_MockRouterRecorder{mock} return mock } func (_m *MockRouter) EXPECT() *_MockRouterRecorder { return _m.recorder } func (_m *MockRouter) AccessManager() (auth.AccessManager, error) { ret := _m.ctrl.Call(_m, "AccessManager") ret0, _ := ret[0].(auth.AccessManager) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) AccessManager() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "AccessManager") } func (_m *MockRouter) Cluster() *cluster.Cluster { ret := _m.ctrl.Call(_m, "Cluster") ret0, _ := ret[0].(*cluster.Cluster) return ret0 } func (_mr *_MockRouterRecorder) Cluster() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Cluster") } func (_m *MockRouter) Done() <-chan bool { ret := _m.ctrl.Call(_m, "Done") ret0, _ := ret[0].(<-chan bool) return ret0 } func (_mr *_MockRouterRecorder) Done() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Done") } func (_m *MockRouter) Fetch(_param0 *store.FetchRequest) error { ret := _m.ctrl.Call(_m, "Fetch", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockRouter) GetSubscribers(_param0 string) ([]byte, error) { ret := _m.ctrl.Call(_m, "GetSubscribers", _param0) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) GetSubscribers(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetSubscribers", arg0) } func (_m *MockRouter) HandleMessage(_param0 *protocol.Message) error { ret := _m.ctrl.Call(_m, "HandleMessage", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) HandleMessage(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "HandleMessage", arg0) } func (_m *MockRouter) KVStore() (kvstore.KVStore, error) { ret := _m.ctrl.Call(_m, "KVStore") ret0, _ := ret[0].(kvstore.KVStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) KVStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "KVStore") } func (_m *MockRouter) MessageStore() (store.MessageStore, error) { ret := _m.ctrl.Call(_m, "MessageStore") ret0, _ := ret[0].(store.MessageStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) MessageStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MessageStore") } func (_m *MockRouter) Subscribe(_param0 *router.Route) (*router.Route, error) { ret := _m.ctrl.Call(_m, "Subscribe", _param0) ret0, _ := ret[0].(*router.Route) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) Subscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Subscribe", arg0) } func (_m *MockRouter) Unsubscribe(_param0 *router.Route) { _m.ctrl.Call(_m, "Unsubscribe", _param0) } func (_mr *_MockRouterRecorder) Unsubscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Unsubscribe", arg0) } ================================================ FILE: server/rest/rest_message_api.go ================================================ package rest import ( "errors" "fmt" "github.com/azer/snakecase" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/router" "github.com/rs/xid" "bytes" "io/ioutil" "net/http" "strings" log "github.com/Sirupsen/logrus" ) const ( xHeaderPrefix = "x-guble-" filterPrefix = "filter" subscribersPrefix = "/subscribers" ) var errNotFound = errors.New("Not Found.") // RestMessageAPI is a struct representing a router's connector for a REST API. type RestMessageAPI struct { router router.Router prefix string } // NewRestMessageAPI returns a new RestMessageAPI. func NewRestMessageAPI(router router.Router, prefix string) *RestMessageAPI { return &RestMessageAPI{router, prefix} } // GetPrefix returns the prefix. // It is a part of the service.endpoint implementation. func (api *RestMessageAPI) GetPrefix() string { return api.prefix } // ServeHTTP is an http.Handler. // It is a part of the service.endpoint implementation. func (api *RestMessageAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodHead { return } if r.Method == http.MethodGet { log.WithField("url", r.URL.Path).Debug("GET") topic, err := api.extractTopic(r.URL.Path, subscribersPrefix) if err != nil { log.WithError(err).Error("Extracting topic failed") if err == errNotFound { http.NotFound(w, r) return } http.Error(w, "Server error.", http.StatusInternalServerError) return } resp, err := api.router.GetSubscribers(topic) w.Header().Set("Content-Type", "application/json") _, err = w.Write(resp) if err != nil { log.WithField("error", err.Error()).Error("Writing to byte stream failed") http.Error(w, "Server error.", http.StatusInternalServerError) return } return } if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } body, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, "Can not read body", http.StatusBadRequest) return } topic, err := api.extractTopic(r.URL.Path, "/message") if err != nil { if err == errNotFound { http.NotFound(w, r) return } http.Error(w, "Server error.", http.StatusInternalServerError) return } msg := &protocol.Message{ Path: protocol.Path(topic), Body: body, UserID: q(r, "userId"), ApplicationID: xid.New().String(), HeaderJSON: headersToJSON(r.Header), } // add filters api.setFilters(r, msg) api.router.HandleMessage(msg) fmt.Fprintf(w, "OK") } func (api *RestMessageAPI) extractTopic(path string, requestTypeTopicPrefix string) (string, error) { p := removeTrailingSlash(api.prefix) + requestTypeTopicPrefix if !strings.HasPrefix(path, p) { return "", errNotFound } // Remove "`api.prefix` + /message" and we remain with the topic topic := strings.TrimPrefix(path, p) if topic == "/" || topic == "" { return "", errNotFound } return topic, nil } // setFilters sets a field found in the format `filterCamelCaseField` in the // query of the request to underscore format on the message filters func (api *RestMessageAPI) setFilters(r *http.Request, msg *protocol.Message) { for name, values := range r.URL.Query() { if strings.HasPrefix(name, filterPrefix) && len(values) > 0 { msg.SetFilter(filterName(name), values[0]) } } } // returns a query parameter func q(r *http.Request, name string) string { params := r.URL.Query()[name] if len(params) > 0 { return params[0] } return "" } // transform from filterCamelCase to camel_case func filterName(name string) string { return snakecase.SnakeCase(strings.TrimPrefix(name, filterPrefix)) } func headersToJSON(header http.Header) string { buff := &bytes.Buffer{} buff.WriteString("{") count := 0 for key, valueList := range header { if strings.HasPrefix(strings.ToLower(key), xHeaderPrefix) && len(valueList) > 0 { if count > 0 { buff.WriteString(",") } buff.WriteString(`"`) buff.WriteString(key[len(xHeaderPrefix):]) buff.WriteString(`":`) buff.WriteString(`"`) buff.WriteString(valueList[0]) buff.WriteString(`"`) count++ } } buff.WriteString("}") return string(buff.Bytes()) } func removeTrailingSlash(path string) string { if len(path) > 1 && path[len(path)-1] == '/' { return path[:len(path)-1] } return path } ================================================ FILE: server/rest/rest_message_api_test.go ================================================ package rest import ( "github.com/smancke/guble/protocol" "github.com/smancke/guble/testutil" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "bytes" "encoding/json" "io/ioutil" "net/http" "net/http/httptest" "net/url" "testing" "time" ) var testBytes = []byte("test") func TestServerHTTP(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) // given: a rest api with a message sink routerMock := NewMockRouter(ctrl) api := NewRestMessageAPI(routerMock, "/api") u, _ := url.Parse("http://localhost/api/message/my/topic?userId=marvin&messageId=42") // and a http context req := &http.Request{ Method: http.MethodPost, URL: u, Body: ioutil.NopCloser(bytes.NewReader(testBytes)), Header: http.Header{}, } w := &httptest.ResponseRecorder{} // then i expect routerMock.EXPECT().HandleMessage(gomock.Any()).Do(func(msg *protocol.Message) { a.Equal(testBytes, msg.Body) a.Equal("{}", msg.HeaderJSON) a.Equal("/my/topic", string(msg.Path)) a.True(len(msg.ApplicationID) > 0) a.Nil(msg.Filters) a.Equal("marvin", msg.UserID) }) // when: I POST a message api.ServeHTTP(w, req) } // Server should return an 405 Method Not Allowed in case method request is not POST func TestServeHTTP_GetError(t *testing.T) { a := assert.New(t) defer testutil.EnableDebugForMethod()() api := NewRestMessageAPI(nil, "/api") u, _ := url.Parse("http://localhost/api/message/my/topic?userId=marvin&messageId=42") // and a http context req := &http.Request{ Method: http.MethodGet, URL: u, Body: ioutil.NopCloser(bytes.NewReader(testBytes)), Header: http.Header{}, } w := &httptest.ResponseRecorder{} // when: I POST a message api.ServeHTTP(w, req) //then a.Equal(http.StatusNotFound, w.Code) } // Server should return an 405 Method Not Allowed in case method request is not POST func TestServeHTTP_GetSubscribers(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() //defer testutil.EnableDebugForMethod()() a := assert.New(t) routerMock := NewMockRouter(testutil.MockCtrl) api := NewRestMessageAPI(routerMock, "/api") routerMock.EXPECT().GetSubscribers(gomock.Any()).Return([]byte("{}"), nil) u, _ := url.Parse("http://localhost/api/subscribers/mytopic") // and a http context req := &http.Request{ Method: http.MethodGet, URL: u, } w := &httptest.ResponseRecorder{} // when: I POST a message api.ServeHTTP(w, req) //then a.Equal(http.StatusOK, w.Code) } func TestHeadersToJSON(t *testing.T) { a := assert.New(t) // empty header a.Equal(`{}`, headersToJSON(http.Header{})) // simple head jsonString := headersToJSON(http.Header{ xHeaderPrefix + "a": []string{"b"}, "foo": []string{"b"}, xHeaderPrefix + "x": []string{"y"}, "bar": []string{"b"}, }) header := make(map[string]string) err := json.Unmarshal([]byte(jsonString), &header) a.NoError(err) a.Equal(2, len(header)) a.Equal("b", header["a"]) a.Equal("y", header["x"]) } func TestRemoveTrailingSlash(t *testing.T) { assert.Equal(t, "/foo", removeTrailingSlash("/foo/")) assert.Equal(t, "/foo", removeTrailingSlash("/foo")) assert.Equal(t, "/", removeTrailingSlash("/")) } func TestExtractTopic(t *testing.T) { a := assert.New(t) api := NewRestMessageAPI(nil, "/api") cases := []struct { path, topic string err error }{ {"/api/message/my/topic", "/my/topic", nil}, {"/api/message/", "", errNotFound}, {"/api/message", "", errNotFound}, {"/api/invalid/request", "", errNotFound}, } for _, c := range cases { topic, err := api.extractTopic(c.path, "/message") m := "Assertion failed for path: " + c.path if c.err == nil { a.Equal(c.topic, topic, m) } else { a.NotNil(err, m) a.Equal(c.err, err, m) } } } func TestRestMessageAPI_setFilters(t *testing.T) { a := assert.New(t) body := bytes.NewBufferString("") req, err := http.NewRequest( http.MethodPost, "http://localhost/api/message/topic?filterUserID=user01&filterDeviceID=ABC&filterDummyCamelCase=dummy_value", body) a.NoError(err) api := &RestMessageAPI{} msg := &protocol.Message{} api.setFilters(req, msg) a.NotNil(msg.Filters) if a.Contains(msg.Filters, "user_id") { a.Equal("user01", msg.Filters["user_id"]) } if a.Contains(msg.Filters, "device_id") { a.Equal("ABC", msg.Filters["device_id"]) } if a.Contains(msg.Filters, "dummy_camel_case") { a.Equal("dummy_value", msg.Filters["dummy_camel_case"]) } } func TestRestMessageAPI_SetFiltersWhenServing(t *testing.T) { testutil.SkipIfDisabled(t) _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) body := bytes.NewBufferString("") req, err := http.NewRequest( http.MethodPost, "http://localhost/test/message/topic?filterUserID=user01&filterDeviceID=ABC&filterDummyCamelCase=dummy_value", body) a.NoError(err) routerMock := NewMockRouter(testutil.MockCtrl) api := NewRestMessageAPI(routerMock, "/test/") recorder := httptest.NewRecorder() routerMock.EXPECT().HandleMessage(gomock.Any()).Do(func(msg *protocol.Message) error { a.NotNil(msg.Filters) if a.Contains(msg.Filters, "user_id") { a.Equal("user01", msg.Filters["user_id"]) } if a.Contains(msg.Filters, "device_id") { a.Equal("ABC", msg.Filters["device_id"]) } if a.Contains(msg.Filters, "dummy_camel_case") { a.Equal("dummy_value", msg.Filters["dummy_camel_case"]) } return nil }) api.ServeHTTP(recorder, req) time.Sleep(10 * time.Millisecond) } ================================================ FILE: server/router/errors.go ================================================ package router import ( "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "errors" "fmt" ) var ( // ErrServiceNotProvided is returned when the service required is not set. ErrServiceNotProvided = errors.New("Service not provided.") // ErrInvalidRoute is returned by the `Deliver` method of a `Route` when it has been closed // due to slow processing ErrInvalidRoute = errors.New("Route is invalid. Channel is closed.") // ErrChannelFull is returned when trying to `Deliver` a message with a queue size of zero // and the channel is full ErrChannelFull = errors.New("Route channel is full. Route is closed.") // ErrQueueFull is returned when trying to `Deliver` a message in a full queued route ErrQueueFull = errors.New("Route queue is full. Route is closed.") ) // PermissionDeniedError is returned when AccessManager denies a user request for a topic type PermissionDeniedError struct { // userId of request UserID string // accessType requested(READ/WRITE) AccessType auth.AccessType // requested topic Path protocol.Path } func (e *PermissionDeniedError) Error() string { return fmt.Sprintf("Access Denied for user=[%s] on path=[%s] for Operation=[%s]", e.UserID, e.Path, e.AccessType) } // ModuleStoppingError is returned when the module is stopping type ModuleStoppingError struct { Name string } func (m *ModuleStoppingError) Error() string { return fmt.Sprintf("Service %s is stopping", m.Name) } ================================================ FILE: server/router/logger.go ================================================ package router import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithField("module", "router") ================================================ FILE: server/router/message_queue.go ================================================ package router import ( "github.com/smancke/guble/protocol" "sync" ) const ( defaultQueueCap = 50 ) type queue struct { mu sync.Mutex queue []*protocol.Message } // newQueue creates a *queue that will have the capacity specified by size. // If `size` is negative use the defaultQueueCap. func newQueue(size int) *queue { if size < 0 { size = defaultQueueCap } return &queue{ queue: make([]*protocol.Message, 0, size), } } func (q *queue) push(m *protocol.Message) { q.mu.Lock() defer q.mu.Unlock() q.queue = append(q.queue, m) } // remove the first item from the queue if exists func (q *queue) remove() { q.mu.Lock() defer q.mu.Unlock() if len(q.queue) == 0 { return } q.queue = q.queue[1:] } // poll returns the first item from the queue without removing it func (q *queue) poll() (*protocol.Message, error) { q.mu.Lock() defer q.mu.Unlock() if len(q.queue) == 0 { return nil, errEmptyQueue } return q.queue[0], nil } func (q *queue) size() int { q.mu.Lock() defer q.mu.Unlock() return len(q.queue) } ================================================ FILE: server/router/mocks_auth_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/auth (interfaces: AccessManager) package router import ( gomock "github.com/golang/mock/gomock" protocol "github.com/smancke/guble/protocol" auth "github.com/smancke/guble/server/auth" ) // Mock of AccessManager interface type MockAccessManager struct { ctrl *gomock.Controller recorder *_MockAccessManagerRecorder } // Recorder for MockAccessManager (not exported) type _MockAccessManagerRecorder struct { mock *MockAccessManager } func NewMockAccessManager(ctrl *gomock.Controller) *MockAccessManager { mock := &MockAccessManager{ctrl: ctrl} mock.recorder = &_MockAccessManagerRecorder{mock} return mock } func (_m *MockAccessManager) EXPECT() *_MockAccessManagerRecorder { return _m.recorder } func (_m *MockAccessManager) IsAllowed(_param0 auth.AccessType, _param1 string, _param2 protocol.Path) bool { ret := _m.ctrl.Call(_m, "IsAllowed", _param0, _param1, _param2) ret0, _ := ret[0].(bool) return ret0 } func (_mr *_MockAccessManagerRecorder) IsAllowed(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "IsAllowed", arg0, arg1, arg2) } ================================================ FILE: server/router/mocks_checker_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/docker/distribution/health (interfaces: Checker) package router import ( gomock "github.com/golang/mock/gomock" ) // Mock of Checker interface type MockChecker struct { ctrl *gomock.Controller recorder *_MockCheckerRecorder } // Recorder for MockChecker (not exported) type _MockCheckerRecorder struct { mock *MockChecker } func NewMockChecker(ctrl *gomock.Controller) *MockChecker { mock := &MockChecker{ctrl: ctrl} mock.recorder = &_MockCheckerRecorder{mock} return mock } func (_m *MockChecker) EXPECT() *_MockCheckerRecorder { return _m.recorder } func (_m *MockChecker) Check() error { ret := _m.ctrl.Call(_m, "Check") ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockCheckerRecorder) Check() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Check") } ================================================ FILE: server/router/mocks_kvstore_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/kvstore (interfaces: KVStore) package router import ( gomock "github.com/golang/mock/gomock" ) // Mock of KVStore interface type MockKVStore struct { ctrl *gomock.Controller recorder *_MockKVStoreRecorder } // Recorder for MockKVStore (not exported) type _MockKVStoreRecorder struct { mock *MockKVStore } func NewMockKVStore(ctrl *gomock.Controller) *MockKVStore { mock := &MockKVStore{ctrl: ctrl} mock.recorder = &_MockKVStoreRecorder{mock} return mock } func (_m *MockKVStore) EXPECT() *_MockKVStoreRecorder { return _m.recorder } func (_m *MockKVStore) Delete(_param0 string, _param1 string) error { ret := _m.ctrl.Call(_m, "Delete", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockKVStoreRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Delete", arg0, arg1) } func (_m *MockKVStore) Get(_param0 string, _param1 string) ([]byte, bool, error) { ret := _m.ctrl.Call(_m, "Get", _param0, _param1) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(bool) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } func (_mr *_MockKVStoreRecorder) Get(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Get", arg0, arg1) } func (_m *MockKVStore) Iterate(_param0 string, _param1 string) chan [2]string { ret := _m.ctrl.Call(_m, "Iterate", _param0, _param1) ret0, _ := ret[0].(chan [2]string) return ret0 } func (_mr *_MockKVStoreRecorder) Iterate(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Iterate", arg0, arg1) } func (_m *MockKVStore) IterateKeys(_param0 string, _param1 string) chan string { ret := _m.ctrl.Call(_m, "IterateKeys", _param0, _param1) ret0, _ := ret[0].(chan string) return ret0 } func (_mr *_MockKVStoreRecorder) IterateKeys(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "IterateKeys", arg0, arg1) } func (_m *MockKVStore) Put(_param0 string, _param1 string, _param2 []byte) error { ret := _m.ctrl.Call(_m, "Put", _param0, _param1, _param2) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockKVStoreRecorder) Put(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Put", arg0, arg1, arg2) } ================================================ FILE: server/router/mocks_router_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/router (interfaces: Router) package router import ( "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/cluster" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/store" ) // Mock of Router interface type MockRouter struct { ctrl *gomock.Controller recorder *_MockRouterRecorder } // Recorder for MockRouter (not exported) type _MockRouterRecorder struct { mock *MockRouter } func NewMockRouter(ctrl *gomock.Controller) *MockRouter { mock := &MockRouter{ctrl: ctrl} mock.recorder = &_MockRouterRecorder{mock} return mock } func (_m *MockRouter) EXPECT() *_MockRouterRecorder { return _m.recorder } func (_m *MockRouter) AccessManager() (auth.AccessManager, error) { ret := _m.ctrl.Call(_m, "AccessManager") ret0, _ := ret[0].(auth.AccessManager) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) AccessManager() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "AccessManager") } func (_m *MockRouter) Cluster() *cluster.Cluster { ret := _m.ctrl.Call(_m, "Cluster") ret0, _ := ret[0].(*cluster.Cluster) return ret0 } func (_mr *_MockRouterRecorder) Cluster() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Cluster") } func (_m *MockRouter) Done() <-chan bool { ret := _m.ctrl.Call(_m, "Done") ret0, _ := ret[0].(<-chan bool) return ret0 } func (_mr *_MockRouterRecorder) Done() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Done") } func (_m *MockRouter) Fetch(_param0 *store.FetchRequest) error { ret := _m.ctrl.Call(_m, "Fetch", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockRouter) GetSubscribers(_param0 string) ([]byte, error) { ret := _m.ctrl.Call(_m, "GetSubscribers", _param0) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) GetSubscribers(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetSubscribers", arg0) } func (_m *MockRouter) HandleMessage(_param0 *protocol.Message) error { ret := _m.ctrl.Call(_m, "HandleMessage", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) HandleMessage(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "HandleMessage", arg0) } func (_m *MockRouter) KVStore() (kvstore.KVStore, error) { ret := _m.ctrl.Call(_m, "KVStore") ret0, _ := ret[0].(kvstore.KVStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) KVStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "KVStore") } func (_m *MockRouter) MessageStore() (store.MessageStore, error) { ret := _m.ctrl.Call(_m, "MessageStore") ret0, _ := ret[0].(store.MessageStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) MessageStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MessageStore") } func (_m *MockRouter) Subscribe(_param0 *Route) (*Route, error) { ret := _m.ctrl.Call(_m, "Subscribe", _param0) ret0, _ := ret[0].(*Route) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) Subscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Subscribe", arg0) } func (_m *MockRouter) Unsubscribe(_param0 *Route) { _m.ctrl.Call(_m, "Unsubscribe", _param0) } func (_mr *_MockRouterRecorder) Unsubscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Unsubscribe", arg0) } ================================================ FILE: server/router/mocks_store_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/store (interfaces: MessageStore) package router import ( gomock "github.com/golang/mock/gomock" protocol "github.com/smancke/guble/protocol" store "github.com/smancke/guble/server/store" ) // Mock of MessageStore interface type MockMessageStore struct { ctrl *gomock.Controller recorder *_MockMessageStoreRecorder } // Recorder for MockMessageStore (not exported) type _MockMessageStoreRecorder struct { mock *MockMessageStore } func NewMockMessageStore(ctrl *gomock.Controller) *MockMessageStore { mock := &MockMessageStore{ctrl: ctrl} mock.recorder = &_MockMessageStoreRecorder{mock} return mock } func (_m *MockMessageStore) EXPECT() *_MockMessageStoreRecorder { return _m.recorder } func (_m *MockMessageStore) DoInTx(_param0 string, _param1 func(uint64) error) error { ret := _m.ctrl.Call(_m, "DoInTx", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockMessageStoreRecorder) DoInTx(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "DoInTx", arg0, arg1) } func (_m *MockMessageStore) Fetch(_param0 *store.FetchRequest) { _m.ctrl.Call(_m, "Fetch", _param0) } func (_mr *_MockMessageStoreRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockMessageStore) GenerateNextMsgID(_param0 string, _param1 byte) (uint64, int64, error) { ret := _m.ctrl.Call(_m, "GenerateNextMsgID", _param0, _param1) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(int64) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } func (_mr *_MockMessageStoreRecorder) GenerateNextMsgID(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GenerateNextMsgID", arg0, arg1) } func (_m *MockMessageStore) MaxMessageID(_param0 string) (uint64, error) { ret := _m.ctrl.Call(_m, "MaxMessageID", _param0) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) MaxMessageID(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MaxMessageID", arg0) } func (_m *MockMessageStore) Partition(_param0 string) (store.MessagePartition, error) { ret := _m.ctrl.Call(_m, "Partition", _param0) ret0, _ := ret[0].(store.MessagePartition) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) Partition(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Partition", arg0) } func (_m *MockMessageStore) Partitions() ([]store.MessagePartition, error) { ret := _m.ctrl.Call(_m, "Partitions") ret0, _ := ret[0].([]store.MessagePartition) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) Partitions() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Partitions") } func (_m *MockMessageStore) Store(_param0 string, _param1 uint64, _param2 []byte) error { ret := _m.ctrl.Call(_m, "Store", _param0, _param1, _param2) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockMessageStoreRecorder) Store(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Store", arg0, arg1, arg2) } func (_m *MockMessageStore) StoreMessage(_param0 *protocol.Message, _param1 byte) (int, error) { ret := _m.ctrl.Call(_m, "StoreMessage", _param0, _param1) ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) StoreMessage(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "StoreMessage", arg0, arg1) } ================================================ FILE: server/router/route.go ================================================ package router import ( "errors" "fmt" "runtime" "strings" "sync" "time" log "github.com/Sirupsen/logrus" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/store" ) var ( errEmptyQueue = errors.New("Empty queue") errTimeout = errors.New("Channel sending timeout") ErrMissingFetchRequest = errors.New("Missing FetchRequest configuration.") ) // Route represents a topic for subscription that has a channel to receive messages. type Route struct { RouteConfig messagesC chan *protocol.Message // queue that will store the messages in correct order. // The queue can have a settable size; // if it reaches the capacity the route is closed. queue *queue closeC chan struct{} // Indicates if the consumer go routine is running consuming bool invalid bool mu sync.RWMutex logger *log.Entry } // NewRoute creates a new route pointer func NewRoute(config RouteConfig) *Route { route := &Route{ RouteConfig: config, queue: newQueue(config.queueSize), messagesC: make(chan *protocol.Message, config.ChannelSize), closeC: make(chan struct{}), logger: logger.WithFields(log.Fields{"path": config.Path, "params": config.RouteParams}), } return route } // Key returns a string that uniquely identifies the route // by concatenating the route Path and the route params // Example: // /topic user_id:user1 application_id:app1 func (r *Route) Key() string { return strings.Join([]string{ string(r.Path), r.RouteParams.Key(), }, " ") } func (r *Route) String() string { return fmt.Sprintf("Path: %s , Params: %s", r.Path, r.RouteParams) } // Deliver takes a messages and adds it to the queue to be delivered into the channel // isFromStore boolean specifies if the messages are being fetched or are from the router // In case they are fetched from the store the route won't close if it's full func (r *Route) Deliver(msg *protocol.Message, isFromStore bool) error { loggerMessage := r.logger.WithField("message", msg) if r.isInvalid() { loggerMessage.Error("Cannot deliver because route is invalid") mTotalDeliverMessageErrors.Add(1) return ErrInvalidRoute } if !r.messageFilter(msg) { loggerMessage.Debug("Message filter didn't match route") mTotalNotMatchedByFilters.Add(1) return nil } // not an infinite queue if r.queueSize >= 0 { // if size is zero the sending is direct if r.queueSize == 0 { return r.sendDirect(msg, isFromStore) } else if r.queue.size() >= r.queueSize { loggerMessage.Error("Closing route because queue is full") r.Close() mTotalDeliverMessageErrors.Add(1) return ErrQueueFull } } r.queue.push(msg) loggerMessage.WithField("queue_size", r.queue.size()).Debug("Deliver") r.consume() return nil } // MessagesChannel returns the route channel to send or receive messages. func (r *Route) MessagesChannel() <-chan *protocol.Message { return r.messagesC } // Provide accepts a router to use for fetching/subscribing and a boolean // indicating if it should close the route after fetching without subscribing // The method is blocking until fetch is finished or route is subscribed func (r *Route) Provide(router Router, subscribe bool) error { if r.FetchRequest != nil { err := r.handleFetch(router) if err != nil { return err } } else if !subscribe { return ErrMissingFetchRequest } if !subscribe { return nil } return r.handleSubscribe(router) } func (r *Route) handleFetch(router Router) error { if r.isInvalid() { return ErrInvalidRoute } r.FetchRequest.Partition = r.Path.Partition() ms, err := router.MessageStore() if err != nil { return err } var ( lastID uint64 received int ) REFETCH: // check if we need to continue fetching maxID, err := ms.MaxMessageID(r.FetchRequest.Partition) if err != nil { return err } if r.FetchRequest.StartID > maxID && r.FetchRequest.Direction == store.DirectionForward { return nil } if received >= r.FetchRequest.Count || lastID >= maxID || (r.FetchRequest.EndID > 0 && r.FetchRequest.EndID <= lastID) { return nil } r.FetchRequest.Init() if err := router.Fetch(r.FetchRequest); err != nil { return err } count := r.FetchRequest.Ready() r.logger.WithField("count", count).Debug("Receiving messages") for { select { case fetchedMessage, open := <-r.FetchRequest.Messages(): if !open { r.logger.Debug("Fetch channel closed.") goto REFETCH } r.logger.WithField("fetchedMessageID", fetchedMessage.ID).Debug("Fetched message") message, err := protocol.ParseMessage(fetchedMessage.Message) if err != nil { return err } r.logger.WithField("messageID", message.ID).Debug("Sending fetched message in channel") if err := r.Deliver(message, true); err != nil { return err } lastID = message.ID received++ case err := <-r.FetchRequest.Errors(): return err case <-router.Done(): r.logger.Debug("Stopping fetch because the router is shutting down") return nil } } } func (r *Route) handleSubscribe(router Router) error { _, err := router.Subscribe(r) return err } // Close closes the route channel. func (r *Route) Close() error { r.mu.Lock() defer r.mu.Unlock() r.logger.Debug("Closing route") // route already closed if r.invalid { return ErrInvalidRoute } r.invalid = true close(r.messagesC) close(r.closeC) return ErrInvalidRoute } // Equal will check if the route path is matched and all the parameters or just a // subset of specific parameters between the routes func (r *Route) Equal(other *Route, keys ...string) bool { return r.RouteConfig.Equal(other.RouteConfig, keys...) } // IsInvalid returns true if the route is invalid, has been closed previously func (r *Route) isInvalid() bool { r.mu.RLock() defer r.mu.RUnlock() return r.invalid } func (r *Route) setInvalid(invalid bool) { r.mu.Lock() defer r.mu.Unlock() r.invalid = invalid } func (r *Route) isConsuming() bool { r.mu.RLock() defer r.mu.RUnlock() return r.consuming } func (r *Route) setConsuming(consuming bool) { r.mu.Lock() defer r.mu.Unlock() r.consuming = consuming } // consume starts a goroutine to consume the queue and pass the messages to route // channel. Stops if there are no items in the queue. func (r *Route) consume() { if r.isConsuming() { return } r.setConsuming(true) r.logger.Debug("Consuming route queue") go func() { defer r.setConsuming(false) var ( msg *protocol.Message err error ) for { if r.isInvalid() { r.logger.Debug("Stopping to consume because route is invalid.") mTotalDeliverMessageErrors.Add(1) return } msg, err = r.queue.poll() if err != nil { if err == errEmptyQueue { r.logger.Debug("Empty queue") return } r.logger.WithField("error", err).Error("Error fetching a message from queue") continue } if err = r.send(msg); err != nil { r.logger.WithField("message", msg).Error("Error sending message through route") if err == errTimeout || err == ErrInvalidRoute { // channel been closed, ending the consumer return } } // remove the first item from the queue r.queue.remove() } }() runtime.Gosched() } // send message through the channel func (r *Route) send(msg *protocol.Message) error { defer r.invalidRecover() r.logger.WithField("message", msg).Debug("Sending message through route channel") // no timeout, means we don't close the channel if r.timeout == -1 { r.messagesC <- msg r.logger.WithField("size", len(r.messagesC)).Debug("Channel size") return nil } select { case r.messagesC <- msg: return nil case <-r.closeC: return ErrInvalidRoute case <-time.After(r.timeout): r.logger.Debug("Closing route because of timeout") r.Close() return errTimeout } } // invalidRecover is used to recover in case we end up sending on a closed channel func (r *Route) invalidRecover() error { if rc := recover(); rc != nil && r.isInvalid() { r.logger.WithField("error", rc).Debug("Recovered closed route") return ErrInvalidRoute } return nil } // sendDirect sends the message directly in the channel func (r *Route) sendDirect(msg *protocol.Message, store bool) error { if store { r.messagesC <- msg return nil } select { case r.messagesC <- msg: return nil default: r.logger.Debug("Closing route because of full channel") r.Close() return ErrChannelFull } } ================================================ FILE: server/router/route_config.go ================================================ package router import ( "time" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/store" ) // Matcher is a func type that receives two route configurations pointers as parameters and // returns true if the routes are matching type Matcher func(RouteConfig, RouteConfig, ...string) bool type RouteConfig struct { RouteParams Path protocol.Path ChannelSize int // queueSize specifies the size of the internal queue slice // (how many items to hold before the channel is closed). // If set to `0` then the queue will have no capacity and the messages // are directly sent, without buffering. queueSize int // timeout defines how long to wait for the message to be read on the channel. // If timeout is reached the route is closed. timeout time.Duration // Matcher if set will be used to check equality of the routes Matcher Matcher `json:"-"` // FetchRequest to fetch messages before subscribing // The Partition field of the FetchRequest is overrided with the Partition of the Route topic FetchRequest *store.FetchRequest `json:"-"` } func (rc *RouteConfig) Equal(other RouteConfig, keys ...string) bool { if rc.Matcher != nil { return rc.Matcher(*rc, other, keys...) } return rc.Path == other.Path && rc.RouteParams.Equal(other.RouteParams, keys...) } // messageFilter returns true if the route matches message filters func (rc *RouteConfig) messageFilter(m *protocol.Message) bool { if m.Filters == nil { return true } return rc.Filter(m.Filters) } // Filter returns true if all filters are matched on the route func (rc *RouteConfig) Filter(filters map[string]string) bool { for key, value := range filters { if rc.Get(key) != value { return false } } return true } ================================================ FILE: server/router/route_config_test.go ================================================ package router import ( "testing" "github.com/smancke/guble/protocol" "github.com/stretchr/testify/assert" ) type routeConfig struct { path string fields map[string]string } func TestRouteConfig_Equal(t *testing.T) { a := assert.New(t) testcases := map[string]struct { // first route definition first routeConfig // second route definition second routeConfig Matcher Matcher // keys to pass on matching keys []string // expected result result bool }{ "full equal": { first: routeConfig{ path: "/path", fields: map[string]string{ "field1": "value1", "field2": "value2", }, }, second: routeConfig{ path: "/path", fields: map[string]string{ "field1": "value1", "field2": "value2", }, }, result: true, }, "full equal with matcher": { first: routeConfig{ path: "/path", fields: map[string]string{ "field1": "value1", "field2": "value2", }, }, second: routeConfig{ path: "/path", fields: map[string]string{ "field1": "value1", "field2": "value2", }, }, Matcher: func(config RouteConfig, other RouteConfig, keys ...string) bool { return config.Path == other.Path }, result: true, }, "make sure matcher is called": { first: routeConfig{ path: "/path", fields: map[string]string{ "field1": "value1", "field2": "value2", }, }, second: routeConfig{ path: "/incorrect-path", fields: map[string]string{ "field1": "value1", "field2": "value2", }, }, Matcher: func(config RouteConfig, other RouteConfig, keys ...string) bool { return true }, result: true, }, "partial match": { first: routeConfig{ path: "/path", fields: map[string]string{ "field1": "value1", "field2": "value2", }, }, second: routeConfig{ path: "/path", fields: map[string]string{ "field1": "value1", "field3": "value3", }, }, keys: []string{"field1"}, result: true, }, "unequal path with keys": { first: routeConfig{ path: "/path", fields: map[string]string{ "field1": "value1", "field2": "value2", }, }, second: routeConfig{ path: "/different-path", fields: map[string]string{ "field1": "value1", "field3": "value3", }, }, keys: []string{"field1"}, result: false, }, } for name, c := range testcases { first := RouteConfig{ Path: protocol.Path(c.first.path), RouteParams: RouteParams(c.first.fields), Matcher: c.Matcher, } second := RouteConfig{ Path: protocol.Path(c.second.path), RouteParams: RouteParams(c.second.fields), Matcher: c.Matcher, } a.Equal(c.result, first.Equal(second, c.keys...), "Failed forward check for case: "+name) a.Equal(c.result, second.Equal(first, c.keys...), "Failed backwards check for case: "+name) } } func TestRouteConfig_messageFilter(t *testing.T) { a := assert.New(t) routeConfig := RouteConfig{ RouteParams: RouteParams{ "field1": "value1", "field2": "value2", }, } testcases := map[string]struct { // filters on the message filters map[string]string // expected result result bool }{ "no filter": { filters: nil, result: true, }, "partial filter": { filters: map[string]string{ "field1": "value1", }, result: true, }, "full filter": { filters: map[string]string{ "field1": "value1", "field2": "value2", }, result: true, }, "one invalid filter": { filters: map[string]string{ "field1": "value1", "field2": "value3", }, result: false, }, "both invalid": { filters: map[string]string{ "field1": "value3", "field2": "value4", }, result: false, }, "partial invalid": { filters: map[string]string{ "field2": "value4", }, result: false, }, } for name, c := range testcases { m := &protocol.Message{Filters: c.filters} a.Equal(c.result, routeConfig.messageFilter(m), "Failed filter: "+name) } } ================================================ FILE: server/router/route_params.go ================================================ package router import ( "fmt" "sort" "strings" ) type RouteParams map[string]string func (rp *RouteParams) String() string { s := make([]string, 0, len(*rp)) for k, v := range *rp { s = append(s, fmt.Sprintf("%s:%s", k, v)) } return strings.Join(s, " ") } func (rp *RouteParams) Key() string { // The generated key must be the same always s := make([]string, 0, len(*rp)) for _, k := range rp.orderedKeys() { s = append(s, fmt.Sprintf("%s:%s", k, (*rp)[k])) } return strings.Join(s, " ") } // orderedKeys returns a slice of ordered func (rp *RouteParams) orderedKeys() []string { keys := make([]string, len(*rp)) i := 0 for k := range *rp { keys[i] = k i++ } sort.Strings(keys) return keys } // Equal verifies if the `receiver` params are the same as `other` params. // The `keys` param specifies which keys to check in case the match has to be // done only on a separate set of keys and not on all keys. func (rp *RouteParams) Equal(other RouteParams, keys ...string) bool { if len(keys) > 0 { return rp.partialEqual(other, keys) } if len(*rp) != len(other) { return false } for k, v := range *rp { if v2, ok := other[k]; !ok { return false } else if v != v2 { return false } } return true } func (rp *RouteParams) partialEqual(other RouteParams, fields []string) bool { for _, key := range fields { if v, ok := other[key]; !ok { return false } else if v != (*rp)[key] { return false } } return true } func (rp *RouteParams) Get(key string) string { return (*rp)[key] } func (rp *RouteParams) Set(key, value string) { (*rp)[key] = value } func (rp *RouteParams) Copy() RouteParams { nrp := make(RouteParams, len(*rp)) for k, v := range *rp { nrp[k] = v } return nrp } ================================================ FILE: server/router/route_test.go ================================================ package router import ( "fmt" "strconv" "strings" "testing" "time" "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/store" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" ) var ( dummyPath = protocol.Path("/dummy") dummyMessageWithID = &protocol.Message{ID: 1, Path: dummyPath, Body: []byte("dummy body")} dummyMessageBytes = `/dummy,MESSAGE_ID,user01,phone01,{},1420110000,1 {"Content-Type": "text/plain", "Correlation-Id": "7sdks723ksgqn"} Hello World` chanSize = 10 queueSize = 5 ) // Send messages in a zero queued route and expect the route to be closed // Same test exists for the router // see router_test.go:TestRoute_IsRemovedIfChannelIsFull func TestRouteDeliver_sendDirect(t *testing.T) { a := assert.New(t) r := testRoute() for i := 0; i < chanSize; i++ { err := r.Deliver(dummyMessageWithID, false) a.NoError(err) } done := make(chan bool) go func() { r.Deliver(dummyMessageWithID, false) done <- true }() select { case <-done: case <-time.After(10 * time.Millisecond): a.Fail("Message not getting sent!") } for i := 0; i < chanSize; i++ { select { case _, open := <-r.MessagesChannel(): a.True(open) case <-time.After(time.Millisecond * 10): a.Fail("error not enough messages in channel") } } // and the channel is closed select { case _, open := <-r.MessagesChannel(): a.False(open) default: logger.Debug("len(r.C): %v", len(r.MessagesChannel())) a.Fail("channel was not closed") } a.True(r.invalid) a.False(r.consuming) a.Equal(0, r.queue.size()) } func TestRouteDeliver_Invalid(t *testing.T) { a := assert.New(t) r := testRoute() r.invalid = true err := r.Deliver(dummyMessageWithID, true) a.Equal(ErrInvalidRoute, err) } func TestRouteDeliver_QueueSize(t *testing.T) { a := assert.New(t) // create a route with a queue size r := testRoute() r.queueSize = queueSize // fill the channel buffer and the queue for i := 0; i < chanSize+queueSize; i++ { r.Deliver(dummyMessageWithID, true) } // and the route should close itself if the queue is overflowed done := make(chan bool) go func() { err := r.Deliver(dummyMessageWithID, true) a.NotNil(err) done <- true }() select { case <-done: case <-time.After(40 * time.Millisecond): a.Fail("Message not delivering.") } time.Sleep(10 * time.Millisecond) a.True(r.isInvalid()) a.False(r.isConsuming()) } func TestRouteDeliver_WithTimeout(t *testing.T) { a := assert.New(t) // create a route with timeout and infinite queue size r := testRoute() r.queueSize = -1 // infinite queue size r.timeout = 10 * time.Millisecond // fill the channel buffer for i := 0; i < chanSize; i++ { r.Deliver(dummyMessageWithID, true) } // delivering one more message should result in a closed route done := make(chan bool) go func() { err := r.Deliver(dummyMessageWithID, true) a.NoError(err) done <- true }() select { case <-done: case <-time.After(40 * time.Millisecond): a.Fail("Message not delivering.") } time.Sleep(30 * time.Millisecond) err := r.Deliver(dummyMessageWithID, true) a.Equal(ErrInvalidRoute, err) a.True(r.invalid) a.False(r.consuming) } func TestRoute_CloseTwice(t *testing.T) { a := assert.New(t) r := testRoute() err := r.Close() a.Equal(ErrInvalidRoute, err) err = r.Close() a.Equal(ErrInvalidRoute, err) } func TestQueue_ShiftEmpty(t *testing.T) { q := newQueue(5) q.remove() assert.Equal(t, 0, q.size()) } func testRoute() *Route { options := RouteConfig{ RouteParams: RouteParams{ "application_id": "appID", "user_id": "userID", }, Path: protocol.Path(dummyPath), ChannelSize: chanSize, } return NewRoute(options) } func TestRoute_messageFilter(t *testing.T) { a := assert.New(t) route := NewRoute(RouteConfig{ Path: "/topic", ChannelSize: 1, RouteParams: RouteParams{ "field1": "value1", "field2": "value2", }, }) msg := &protocol.Message{ ID: 1, Path: "/topic", } route.Deliver(msg, false) // test message is received on the channel a.True(isMessageReceived(route, msg)) msg = &protocol.Message{ ID: 1, Path: "/topic", } msg.SetFilter("field1", "value1") route.Deliver(msg, true) a.True(isMessageReceived(route, msg)) msg = &protocol.Message{ ID: 1, Path: "/topic", } msg.SetFilter("field1", "value1") msg.SetFilter("field2", "value2") route.Deliver(msg, true) a.True(isMessageReceived(route, msg)) msg = &protocol.Message{ ID: 1, Path: "/topic", } msg.SetFilter("field1", "value1") msg.SetFilter("field2", "value2") msg.SetFilter("field3", "value3") route.Deliver(msg, true) a.False(isMessageReceived(route, msg)) msg = &protocol.Message{ ID: 1, Path: "/topic", } msg.SetFilter("field3", "value3") route.Deliver(msg, true) a.False(isMessageReceived(route, msg)) } func isMessageReceived(route *Route, msg *protocol.Message) bool { select { case m, opened := <-route.MessagesChannel(): if !opened { return false } return m == msg case <-time.After(20 * time.Millisecond): } return false } func TestRoute_Provide_ErrMissingFetchRequest(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) routerMock := NewMockRouter(ctrl) route := NewRoute(RouteConfig{ Path: "/fetch_request", }) err := route.Provide(routerMock, false) a.Error(err) a.Equal(ErrMissingFetchRequest, err) } func TestRoute_Provide_Fetch(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) msMock := NewMockMessageStore(ctrl) routerMock := NewMockRouter(ctrl) routerMock.EXPECT().MessageStore().Return(msMock, nil) route := NewRoute(RouteConfig{ Path: protocol.Path("/fetch_request"), ChannelSize: 5, FetchRequest: store.NewFetchRequest("", 0, 0, store.DirectionForward, -1), }) msMock.EXPECT().MaxMessageID("fetch_request").Return(uint64(2), nil).Times(2) routerMock.EXPECT().Done().Return(make(chan bool)).AnyTimes() routerMock.EXPECT().Fetch(gomock.Any()).Do(func(req *store.FetchRequest) { a.Equal(req.Partition, "fetch_request") a.Equal(uint64(0), req.StartID) a.Equal(uint64(0), req.EndID) a.Equal(store.DirectionForward, req.Direction) go func() { req.StartC <- 2 // send to messages req.Push(1, []byte(strings.Replace(dummyMessageBytes, "MESSAGE_ID", strconv.Itoa(1), 1))) req.Push(2, []byte(strings.Replace(dummyMessageBytes, "MESSAGE_ID", strconv.Itoa(2), 1))) req.Done() }() }) done := make(chan struct{}) go func() { receivedMessages := 0 for i := 1; i <= 2; i++ { select { case m, opened := <-route.MessagesChannel(): if opened { receivedMessages++ a.Equal(uint64(i), m.ID) } case <-time.After(50 * time.Millisecond): a.Fail("Message not received") } } a.Equal(2, receivedMessages) close(done) }() err := route.Provide(routerMock, false) a.NoError(err) <-done } func TestRoute_Provide_WithSubscribe(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) msMock := NewMockMessageStore(ctrl) routerMock := NewMockRouter(ctrl) routerMock.EXPECT().MessageStore().Return(msMock, nil) route := NewRoute(RouteConfig{ Path: protocol.Path("/fetch_request"), ChannelSize: 4, FetchRequest: store.NewFetchRequest("", 0, 0, store.DirectionForward, -1), }) routerMock.EXPECT().Done().Return(make(chan bool)).AnyTimes() routerMock.EXPECT().Fetch(gomock.Any()).Do(func(req *store.FetchRequest) { a.Equal(req.Partition, "fetch_request") a.Equal(uint64(0), req.StartID) a.Equal(uint64(0), req.EndID) a.Equal(store.DirectionForward, req.Direction) go func() { req.StartC <- 2 // send to messages req.Push(1, []byte(strings.Replace(dummyMessageBytes, "MESSAGE_ID", strconv.Itoa(1), 1))) req.Push(2, []byte(strings.Replace(dummyMessageBytes, "MESSAGE_ID", strconv.Itoa(2), 1))) req.Done() }() }) msMock.EXPECT().MaxMessageID(gomock.Eq("fetch_request")).Return(uint64(2), nil).Times(2) routerMock.EXPECT().Subscribe(gomock.Any()).Do(func(r *Route) (*Route, error) { a.Equal(route, r) for i := 3; i <= 4; i++ { r.Deliver(&protocol.Message{ ID: uint64(i), Path: "/fetch_request", Body: []byte("dummy"), }, true) } return r, nil }) done := make(chan struct{}) go func() { receivedMessages := 0 for i := 1; i <= 4; i++ { select { case m, opened := <-route.MessagesChannel(): if opened { receivedMessages++ a.Equal(uint64(i), m.ID) } case <-time.After(50 * time.Millisecond): a.Fail("Message not received") } } a.Equal(4, receivedMessages) close(done) }() err := route.Provide(routerMock, true) a.NoError(err) <-done } type startable interface { Start() error } type stopable interface { Stop() error } // Test that the route will fetch in case new messages arrived that match the // fetch request func TestRoute_Provide_MultipleFetch(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) memoryKV := kvstore.NewMemoryKVStore() msMock := NewMockMessageStore(ctrl) router := New(auth.AllowAllAccessManager(true), msMock, memoryKV, nil) if startable, ok := router.(startable); ok { startable.Start() if stopable, ok := router.(stopable); ok { defer stopable.Stop() } } path := protocol.Path("/fetch_request") route := NewRoute(RouteConfig{ Path: path, ChannelSize: 4, FetchRequest: store.NewFetchRequest("", 0, 0, store.DirectionForward, -1), }) block := make(chan struct{}) maxIDExpect := msMock.EXPECT().MaxMessageID(gomock.Any()). Return(uint64(2), nil) msMock.EXPECT().Fetch(gomock.Any()).Do(func(req *store.FetchRequest) { a.Equal("fetch_request", req.Partition) // block the fetch request until pushing some new messages in the router go func() { <-block req.StartC <- 2 req.Push(1, []byte(strings.Replace(dummyMessageBytes, "MESSAGE_ID", strconv.Itoa(1), 1))) req.Push(2, []byte(strings.Replace(dummyMessageBytes, "MESSAGE_ID", strconv.Itoa(2), 1))) req.Done() }() }).After(maxIDExpect) msMock.EXPECT().MaxMessageID(gomock.Any()). Return(uint64(4), nil).Times(2) msMock.EXPECT().Fetch(gomock.Any()).Do(func(req *store.FetchRequest) { a.Equal("fetch_request", req.Partition) go func() { req.StartC <- 2 // block the fetch request until pushing some new messages in the router req.Push(3, []byte(strings.Replace(dummyMessageBytes, "MESSAGE_ID", strconv.Itoa(3), 1))) req.Push(4, []byte(strings.Replace(dummyMessageBytes, "MESSAGE_ID", strconv.Itoa(4), 1))) req.Done() }() }) msMock.EXPECT().StoreMessage(gomock.Any(), gomock.Any()).AnyTimes() router.HandleMessage(&protocol.Message{ID: 3, Path: path, Body: []byte("dummy body")}) router.HandleMessage(&protocol.Message{ID: 4, Path: path, Body: []byte("dummy body")}) done := make(chan struct{}) go func() { receivedMessages := 0 for i := 1; i <= 4; i++ { select { case m, opened := <-route.MessagesChannel(): if opened { receivedMessages++ a.Equal(uint64(i), m.ID) } case <-time.After(50 * time.Millisecond): a.Fail(fmt.Sprintf("Message not received: %d", i)) } } a.Equal(4, receivedMessages) close(done) }() close(block) err := route.Provide(router, true) a.NoError(err) <-done } func TestRoute_Provide_EndIDSubscribe(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) msMock := NewMockMessageStore(ctrl) routerMock := NewMockRouter(ctrl) routerMock.EXPECT().MessageStore().Return(msMock, nil) route := NewRoute(RouteConfig{ Path: protocol.Path("/fetch_request"), ChannelSize: 5, FetchRequest: store.NewFetchRequest("", 8, 10, store.DirectionForward, -1), }) msMock.EXPECT().MaxMessageID("fetch_request").Return(uint64(12), nil).Times(2) routerMock.EXPECT().Done().Return(make(chan bool)).AnyTimes() routerMock.EXPECT().Fetch(gomock.Any()).Do(func(req *store.FetchRequest) { a.Equal(req.Partition, "fetch_request") a.Equal(uint64(8), req.StartID) a.Equal(uint64(10), req.EndID) a.Equal(store.DirectionForward, req.Direction) go func() { req.StartC <- 3 // send the messages req.Push(8, []byte(strings.Replace(dummyMessageBytes, "MESSAGE_ID", strconv.Itoa(8), 1))) req.Push(9, []byte(strings.Replace(dummyMessageBytes, "MESSAGE_ID", strconv.Itoa(9), 1))) req.Push(10, []byte(strings.Replace(dummyMessageBytes, "MESSAGE_ID", strconv.Itoa(10), 1))) req.Done() }() }) routerMock.EXPECT().Subscribe(gomock.Eq(route)).Return(route, nil) done := make(chan struct{}) go func() { receivedMessages := 0 for i := 8; i <= 10; i++ { select { case m, opened := <-route.MessagesChannel(): if opened { receivedMessages++ a.Equal(uint64(i), m.ID) } case <-time.After(50 * time.Millisecond): a.Fail("Message not received") } } a.Equal(3, receivedMessages) close(done) }() err := route.Provide(routerMock, true) a.NoError(err) <-done } ================================================ FILE: server/router/router.go ================================================ package router import ( "fmt" "runtime" "strings" "sync" log "github.com/Sirupsen/logrus" "github.com/docker/distribution/health" "encoding/json" "net/http" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/cluster" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/store" ) const ( overloadedHandleChannelRatio = 0.9 handleChannelCapacity = 500 subscribeChannelCapacity = 10 unsubscribeChannelCapacity = 10 prefix = "/admin/router" ) // Router interface provides a mechanism for PubSub messaging type Router interface { Subscribe(r *Route) (*Route, error) Unsubscribe(r *Route) HandleMessage(message *protocol.Message) error Fetch(*store.FetchRequest) error GetSubscribers(topic string) ([]byte, error) AccessManager() (auth.AccessManager, error) MessageStore() (store.MessageStore, error) KVStore() (kvstore.KVStore, error) Cluster() *cluster.Cluster Done() <-chan bool } // Helper struct to pass `Route` to subscription channel and provide a notification channel. type subRequest struct { route *Route doneC chan bool } type router struct { routes map[protocol.Path][]*Route // mapping the path to the route slice handleC chan *protocol.Message subscribeC chan subRequest unsubscribeC chan subRequest stopC chan bool // Channel that signals stop of the router stopping bool // Flag: the router is in stopping process and no incoming messages are accepted wg sync.WaitGroup // Add any operation that we need to wait upon here accessManager auth.AccessManager messageStore store.MessageStore kvStore kvstore.KVStore cluster *cluster.Cluster sync.RWMutex } // New returns a pointer to Router func New(accessManager auth.AccessManager, messageStore store.MessageStore, kvStore kvstore.KVStore, cluster *cluster.Cluster) Router { return &router{ routes: make(map[protocol.Path][]*Route), handleC: make(chan *protocol.Message, handleChannelCapacity), subscribeC: make(chan subRequest, subscribeChannelCapacity), unsubscribeC: make(chan subRequest, unsubscribeChannelCapacity), stopC: make(chan bool, 1), accessManager: accessManager, messageStore: messageStore, kvStore: kvStore, cluster: cluster, } } func (router *router) Start() error { router.panicIfInternalDependenciesAreNil() logger.Info("Starting router") resetRouterMetrics() router.wg.Add(1) router.setStopping(false) go func() { for { if router.stopping && router.channelsAreEmpty() { router.closeRoutes() router.wg.Done() return } func() { defer protocol.PanicLogger() select { case message := <-router.handleC: router.handleMessage(message) runtime.Gosched() case subscriber := <-router.subscribeC: router.subscribe(subscriber.route) subscriber.doneC <- true case unsubscriber := <-router.unsubscribeC: router.unsubscribe(unsubscriber.route) unsubscriber.doneC <- true case <-router.Done(): router.setStopping(true) } }() } }() return nil } // Stop stops the router by closing the stop channel, and waiting on the WaitGroup func (router *router) Stop() error { logger.Info("Stopping router") router.stopC <- true router.wg.Wait() return nil } func (router *router) Check() error { if router.accessManager == nil || router.messageStore == nil || router.kvStore == nil { logger.WithError(ErrServiceNotProvided).Error("Some mandatory services are not provided") return ErrServiceNotProvided } if checkable, ok := router.messageStore.(health.Checker); ok { err := checkable.Check() if err != nil { logger.WithField("error", err.Error()).Error("MessageStore check failed") return err } } if checkable, ok := router.kvStore.(health.Checker); ok { err := checkable.Check() if err != nil { logger.WithField("error", err.Error()).Error("KVStore check failed") return err } } return nil } // HandleMessage stores the message in the MessageStore(and gets a new ID for it if the message was created locally) // and then passes it to the internal channel, and asynchronously to the cluster (if available). func (router *router) HandleMessage(message *protocol.Message) error { logger.WithFields(log.Fields{ "userID": message.UserID, "path": message.Path}).Debug("HandleMessage") mTotalMessagesIncoming.Add(1) if err := router.isStopping(); err != nil { logger.WithField("error", err.Error()).Error("Router is stopping") return err } if !router.accessManager.IsAllowed(auth.WRITE, message.UserID, message.Path) { return &PermissionDeniedError{UserID: message.UserID, AccessType: auth.WRITE, Path: message.Path} } var nodeID uint8 if router.cluster != nil { nodeID = router.cluster.Config.ID } mTotalMessagesIncomingBytes.Add(int64(len(message.Bytes()))) size, err := router.messageStore.StoreMessage(message, nodeID) if err != nil { logger.WithField("error", err.Error()).Error("Error storing message") mTotalMessageStoreErrors.Add(1) return err } mTotalMessagesStoredBytes.Add(int64(size)) router.handleOverloadedChannel() router.handleC <- message if router.cluster != nil && message.NodeID == router.cluster.Config.ID { go router.cluster.BroadcastMessage(message) } return nil } func (router *router) Subscribe(r *Route) (*Route, error) { logger.WithFields(log.Fields{ "accessManager": router.accessManager, "route": r, }).Debug("Subscribe") if err := router.isStopping(); err != nil { return nil, err } userID := r.Get("user_id") routePath := r.Path accessAllowed := router.accessManager.IsAllowed(auth.READ, userID, routePath) if !accessAllowed { return r, &PermissionDeniedError{UserID: userID, AccessType: auth.READ, Path: routePath} } req := subRequest{ route: r, doneC: make(chan bool), } router.subscribeC <- req <-req.doneC return r, nil } // Subscribe adds a route to the subscribers. If there is already a route with same Application Id and Path, it will be replaced. func (router *router) Unsubscribe(r *Route) { logger.WithFields(log.Fields{ "accessManager": router.accessManager, "route": r, }).Debug("Unsubscribe") req := subRequest{ route: r, doneC: make(chan bool), } router.unsubscribeC <- req <-req.doneC } func (router *router) GetSubscribers(topicPath string) ([]byte, error) { subscribers := make([]RouteParams, 0) routes, present := router.routes[protocol.Path(topicPath)] if present { for index, currRoute := range routes { logger.WithFields(log.Fields{ "index": index, "routeParams": currRoute.RouteParams, }).Debug("Added route to slice") subscribers = append(subscribers, currRoute.RouteParams) } } return json.Marshal(subscribers) } func (router *router) subscribe(r *Route) { logger.WithField("route", r).Debug("Internal subscribe") mTotalSubscriptionAttempts.Add(1) routePath := r.Path slice, present := router.routes[routePath] var removed bool if present { // Try to remove, to avoid double subscriptions of the same app slice, removed = removeIfMatching(slice, r) } else { // Path not present yet. Initialize the slice slice = make([]*Route, 0, 1) router.routes[routePath] = slice mCurrentRoutes.Add(1) } router.routes[routePath] = append(slice, r) if removed { mTotalDuplicateSubscriptionsAttempts.Add(1) } else { mTotalSubscriptions.Add(1) mCurrentSubscriptions.Add(1) } } func (router *router) unsubscribe(r *Route) { logger.WithField("route", r).Debug("Internal unsubscribe") mTotalUnsubscriptionAttempts.Add(1) routePath := r.Path slice, present := router.routes[routePath] if !present { mTotalInvalidTopicOnUnsubscriptionAttempts.Add(1) return } var removed bool router.routes[routePath], removed = removeIfMatching(slice, r) if removed { mTotalUnsubscriptions.Add(1) mCurrentSubscriptions.Add(-1) } else { mTotalInvalidUnsubscriptionAttempts.Add(1) } if len(router.routes[routePath]) == 0 { delete(router.routes, routePath) mCurrentRoutes.Add(-1) } } func (router *router) panicIfInternalDependenciesAreNil() { if router.accessManager == nil || router.kvStore == nil || router.messageStore == nil { panic(fmt.Sprintf("router: the internal dependencies marked with `true` are not set: AccessManager=%v, KVStore=%v, MessageStore=%v", router.accessManager == nil, router.kvStore == nil, router.messageStore == nil)) } } func (router *router) channelsAreEmpty() bool { return len(router.handleC) == 0 && len(router.subscribeC) == 0 && len(router.unsubscribeC) == 0 } func (router *router) setStopping(v bool) { router.Lock() defer router.Unlock() router.stopping = v } func (router *router) Done() <-chan bool { return router.stopC } func (router *router) isStopping() error { router.RLock() defer router.RUnlock() if router.stopping { return &ModuleStoppingError{"Router"} } return nil } func (router *router) handleMessage(message *protocol.Message) { flog := logger.WithFields(log.Fields{ "topic": message.Path, "metadata": message.Metadata(), "filters": message.Filters, }) flog.Debug("Called routeMessage for data") mTotalMessagesRouted.Add(1) matched := false for path, pathRoutes := range router.routes { if matchesTopic(message.Path, path) { matched = true for _, route := range pathRoutes { if err := route.Deliver(message, false); err == ErrInvalidRoute { // Unsubscribe invalid routes router.unsubscribe(route) } } } } if !matched { flog.Debug("No route matched.") mTotalMessagesNotMatchingTopic.Add(1) } } func (router *router) closeRoutes() { logger.Debug("closeRoutes") for _, currentRouteList := range router.routes { for _, route := range currentRouteList { router.unsubscribe(route) log.WithFields(log.Fields{"module": "router", "route": route.String()}).Debug("Closing route") route.Close() } } } func (router *router) handleOverloadedChannel() { if float32(len(router.handleC))/float32(cap(router.handleC)) > overloadedHandleChannelRatio { logger.WithFields(log.Fields{ "currentLength": len(router.handleC), "maxCapacity": cap(router.handleC), }).Warn("handleC channel is almost full") mTotalOverloadedHandleChannel.Add(1) } } // matchesTopic checks whether the supplied routePath matches the message topic func matchesTopic(messagePath, routePath protocol.Path) bool { messagePathLen := len(string(messagePath)) routePathLen := len(string(routePath)) return strings.HasPrefix(string(messagePath), string(routePath)) && (messagePathLen == routePathLen || (messagePathLen > routePathLen && string(messagePath)[routePathLen] == '/')) } // removeIfMatching removes a route from the supplied list, based on same ApplicationID id and same path (if existing) // returns: the (possibly updated) slide, and a boolean value (true if route was removed, false otherwise) func removeIfMatching(slice []*Route, route *Route) ([]*Route, bool) { position := -1 for p, r := range slice { if r.Equal(route) { position = p break } } if position == -1 { return slice, false } return append(slice[:position], slice[position+1:]...), true } func (router *router) Fetch(req *store.FetchRequest) error { logger.Debug("Fetch") if err := router.isStopping(); err != nil { return err } router.messageStore.Fetch(req) return nil } // AccessManager returns the `accessManager` provided for the router func (router *router) AccessManager() (auth.AccessManager, error) { if router.accessManager == nil { return nil, ErrServiceNotProvided } return router.accessManager, nil } // MessageStore returns the `messageStore` provided for the router func (router *router) MessageStore() (store.MessageStore, error) { if router.messageStore == nil { return nil, ErrServiceNotProvided } return router.messageStore, nil } // KVStore returns the `kvStore` provided for the router func (router *router) KVStore() (kvstore.KVStore, error) { if router.kvStore == nil { return nil, ErrServiceNotProvided } return router.kvStore, nil } // Cluster returns the `cluster` provided for the router, or nil if no cluster was set-up func (router *router) Cluster() *cluster.Cluster { return router.cluster } func (router *router) ServeHTTP(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") if req.Method != http.MethodGet { http.Error(w, `{"error": Error method not allowed.Only HTTP GET is accepted}`, http.StatusMethodNotAllowed) return } err := json.NewEncoder(w).Encode(router.routes) if err != nil { http.Error(w, `{"error":Error encoding data.}`, http.StatusInternalServerError) logger.WithField("error", err.Error()).Error("Error encoding data.") return } } func (router *router) GetPrefix() string { return prefix } ================================================ FILE: server/router/router_metrics.go ================================================ package router import ( "github.com/smancke/guble/server/metrics" ) var ( mTotalSubscriptionAttempts = metrics.NewInt("router.total_subscription_attempts") mTotalDuplicateSubscriptionsAttempts = metrics.NewInt("router.total_subscription_attempts_duplicate") mTotalSubscriptions = metrics.NewInt("router.total_subscriptions") mTotalUnsubscriptionAttempts = metrics.NewInt("router.total_unsubscription_attempts") mTotalInvalidTopicOnUnsubscriptionAttempts = metrics.NewInt("router.total_unsubscription_attempts_invalid_topic") mTotalInvalidUnsubscriptionAttempts = metrics.NewInt("router.total_unsubscription_attempts_invalid") mTotalUnsubscriptions = metrics.NewInt("router.total_unsubscriptions") mCurrentSubscriptions = metrics.NewInt("router.current_subscriptions") mCurrentRoutes = metrics.NewInt("router.current_routes") mTotalMessagesIncoming = metrics.NewInt("router.total_messages_incoming") mTotalMessagesIncomingBytes = metrics.NewInt("router.total_messages_bytes_incoming") mTotalMessagesStoredBytes = metrics.NewInt("router.total_messages_bytes_stored") mTotalMessagesRouted = metrics.NewInt("router.total_messages_routed") mTotalOverloadedHandleChannel = metrics.NewInt("router.total_overloaded_handle_channel") mTotalMessagesNotMatchingTopic = metrics.NewInt("router.total_messages_not_matching_topic") mTotalMessageStoreErrors = metrics.NewInt("router.total_errors_message_store") mTotalDeliverMessageErrors = metrics.NewInt("router.total_errors_deliver_message") mTotalNotMatchedByFilters = metrics.NewInt("router.total_not_matched_by_filters") ) func resetRouterMetrics() { mTotalSubscriptionAttempts.Set(0) mTotalDuplicateSubscriptionsAttempts.Set(0) mTotalSubscriptions.Set(0) mTotalUnsubscriptionAttempts.Set(0) mTotalInvalidTopicOnUnsubscriptionAttempts.Set(0) mTotalUnsubscriptions.Set(0) mTotalInvalidUnsubscriptionAttempts.Set(0) mCurrentSubscriptions.Set(0) mCurrentRoutes.Set(0) mTotalMessagesIncoming.Set(0) mTotalMessagesRouted.Set(0) mTotalOverloadedHandleChannel.Set(0) mTotalMessagesNotMatchingTopic.Set(0) mTotalDeliverMessageErrors.Set(0) mTotalMessageStoreErrors.Set(0) mTotalMessagesIncomingBytes.Set(0) mTotalMessagesStoredBytes.Set(0) mTotalNotMatchedByFilters.Set(0) } ================================================ FILE: server/router/router_test.go ================================================ package router import ( "errors" "testing" "time" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/store" "github.com/smancke/guble/server/store/dummystore" "github.com/smancke/guble/testutil" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" ) var aTestByteMessage = []byte("Hello World!") type msChecker struct { *MockMessageStore *MockChecker } func newMSChecker() *msChecker { return &msChecker{ NewMockMessageStore(testutil.MockCtrl), NewMockChecker(testutil.MockCtrl), } } type kvsChecker struct { *MockKVStore *MockChecker } func newKVSChecker() *kvsChecker { return &kvsChecker{ NewMockKVStore(testutil.MockCtrl), NewMockChecker(testutil.MockCtrl), } } func TestRouter_AddAndRemoveRoutes(t *testing.T) { a := assert.New(t) // Given a Router router, _, _, _ := aStartedRouter() // when i add two routes in the same path routeBlah1, _ := router.Subscribe(NewRoute( RouteConfig{ RouteParams: RouteParams{"application_id": "appid01", "user_id": "user01"}, Path: protocol.Path("/blah"), ChannelSize: chanSize, }, )) routeBlah2, _ := router.Subscribe(NewRoute( RouteConfig{ RouteParams: RouteParams{"application_id": "appid02", "user_id": "user01"}, Path: protocol.Path("/blah"), ChannelSize: chanSize, }, )) // and one route in another path routeFoo, _ := router.Subscribe(NewRoute( RouteConfig{ RouteParams: RouteParams{"application_id": "appid01", "user_id": "user01"}, Path: protocol.Path("/foo"), ChannelSize: chanSize, }, )) // then // the routes are stored a.Equal(2, len(router.routes[protocol.Path("/blah")])) a.True(routeBlah1.Equal(router.routes[protocol.Path("/blah")][0])) a.True(routeBlah2.Equal(router.routes[protocol.Path("/blah")][1])) a.Equal(1, len(router.routes[protocol.Path("/foo")])) a.True(routeFoo.Equal(router.routes[protocol.Path("/foo")][0])) // when i remove routes router.Unsubscribe(routeBlah1) router.Unsubscribe(routeFoo) // then they are gone a.Equal(1, len(router.routes[protocol.Path("/blah")])) a.True(routeBlah2.Equal(router.routes[protocol.Path("/blah")][0])) a.Nil(router.routes[protocol.Path("/foo")]) } func TestRouter_SubscribeNotAllowed(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) am := NewMockAccessManager(ctrl) msMock := NewMockMessageStore(ctrl) kvsMock := NewMockKVStore(ctrl) am.EXPECT().IsAllowed(auth.READ, "user01", protocol.Path("/blah")).Return(false) router := New(am, msMock, kvsMock, nil).(*router) router.Start() _, e := router.Subscribe(NewRoute( RouteConfig{ RouteParams: RouteParams{"application_id": "appid01", "user_id": "user01"}, Path: protocol.Path("/blah"), ChannelSize: chanSize, }, )) // default TestAccessManager denies all a.NotNil(e) // now add permissions am.EXPECT().IsAllowed(auth.READ, "user01", protocol.Path("/blah")).Return(true) // and user shall be allowed to subscribe _, e = router.Subscribe(NewRoute( RouteConfig{ RouteParams: RouteParams{"application_id": "appid01", "user_id": "user01"}, Path: protocol.Path("/blah"), ChannelSize: chanSize, }, )) a.Nil(e) } func TestRouter_HandleMessageNotAllowed(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) amMock := NewMockAccessManager(ctrl) msMock := NewMockMessageStore(ctrl) kvsMock := NewMockKVStore(ctrl) // Given a Router with route router, r := aRouterRoute(chanSize) router.accessManager = amMock router.messageStore = msMock router.kvStore = kvsMock amMock.EXPECT().IsAllowed(auth.WRITE, r.Get("user_id"), r.Path).Return(false) // when i send a message to the route err := router.HandleMessage(&protocol.Message{ Path: r.Path, Body: aTestByteMessage, UserID: r.Get("user_id"), }) // an error shall be returned a.Error(err) // and when permission is granted id, ts := uint64(2), time.Now().Unix() amMock.EXPECT().IsAllowed(auth.WRITE, r.Get("user_id"), r.Path).Return(true) msMock.EXPECT(). StoreMessage(gomock.Any(), gomock.Any()). Do(func(m *protocol.Message, nodeID uint8) (int, error) { m.ID = id m.Time = ts m.NodeID = nodeID return len(m.Bytes()), nil }) // sending message err = router.HandleMessage(&protocol.Message{ Path: r.Path, Body: aTestByteMessage, UserID: r.Get("user_id"), }) // shall give no error a.NoError(err) } func TestRouter_ReplacingOfRoutesMatchingAppID(t *testing.T) { a := assert.New(t) // Given a Router with a route router, _, _, _ := aStartedRouter() matcherFunc := func(route, other RouteConfig, keys ...string) bool { return route.Path == other.Path && route.Get("application_id") == other.Get("application_id") } router.Subscribe(NewRoute( RouteConfig{ RouteParams: RouteParams{"application_id": "appid01", "user_id": "user01"}, Path: protocol.Path("/blah"), Matcher: matcherFunc, }, )) // when: i add another route with the same Application Id and Same Path router.Subscribe(NewRoute( RouteConfig{ RouteParams: RouteParams{"application_id": "appid01", "user_id": "newUserId"}, Path: protocol.Path("/blah"), Matcher: matcherFunc, }, )) // then: the router only contains the new route a.Equal(1, len(router.routes)) a.Equal(1, len(router.routes["/blah"])) a.Equal("newUserId", router.routes["/blah"][0].Get("user_id")) } func TestRouter_SimpleMessageSending(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) // Given a Router with route router, r := aRouterRoute(chanSize) msMock := NewMockMessageStore(ctrl) router.messageStore = msMock id, ts := uint64(2), time.Now().Unix() msMock.EXPECT(). StoreMessage(gomock.Any(), gomock.Any()). Do(func(m *protocol.Message, nodeID uint8) (int, error) { m.ID = id m.Time = ts m.NodeID = nodeID return len(m.Bytes()), nil }) // when i send a message to the route router.HandleMessage(&protocol.Message{Path: r.Path, Body: aTestByteMessage}) // then I can receive it a short time later assertChannelContainsMessage(a, r.MessagesChannel(), aTestByteMessage) } func TestRouter_RoutingWithSubTopics(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) // Given a Router with route router, _, _, _ := aStartedRouter() msMock := NewMockMessageStore(ctrl) router.messageStore = msMock // expect a message to `blah` partition first and `blahblub` second firstStore := msMock.EXPECT(). StoreMessage(gomock.Any(), gomock.Any()). Do(func(m *protocol.Message, nodeID uint8) (int, error) { a.Equal("/blah/blub", string(m.Path)) return 0, nil }) msMock.EXPECT(). StoreMessage(gomock.Any(), gomock.Any()).After(firstStore). Do(func(m *protocol.Message, nodeID uint8) (int, error) { a.Equal("/blahblub", string(m.Path)) return 0, nil }) r, _ := router.Subscribe(NewRoute( RouteConfig{ RouteParams: RouteParams{"application_id": "appid01", "user_id": "user01"}, Path: protocol.Path("/blah"), ChannelSize: chanSize, }, )) // when i send a message to a subroute router.HandleMessage(&protocol.Message{Path: "/blah/blub", Body: aTestByteMessage}) // then I can receive the message assertChannelContainsMessage(a, r.MessagesChannel(), aTestByteMessage) // but, when i send a message to a resource, which is just a substring router.HandleMessage(&protocol.Message{Path: "/blahblub", Body: aTestByteMessage}) // then the message gets not delivered a.Equal(0, len(r.MessagesChannel())) } func TestMatchesTopic(t *testing.T) { for _, test := range []struct { messagePath protocol.Path routePath protocol.Path matches bool }{ {"/foo", "/foo", true}, {"/foo/xyz", "/foo", true}, {"/foo", "/bar", false}, {"/fooxyz", "/foo", false}, {"/foo", "/bar/xyz", false}, } { if !test.matches == matchesTopic(test.messagePath, test.routePath) { t.Errorf("error: expected %v, but: matchesTopic(%q, %q) = %v", test.matches, test.messagePath, test.routePath, matchesTopic(test.messagePath, test.routePath)) } } } func TestRoute_IsRemovedIfChannelIsFull(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) // Given a Router with route router, r := aRouterRoute(chanSize) r.timeout = 5 * time.Millisecond msMock := NewMockMessageStore(ctrl) router.messageStore = msMock msMock.EXPECT(). StoreMessage(gomock.Any(), gomock.Any()). Do(func(m *protocol.Message, nodeID uint8) (int, error) { a.Equal(r.Path, m.Path) return 0, nil }).MaxTimes(chanSize + 1) // where the channel is full of messages for i := 0; i < chanSize; i++ { router.HandleMessage(&protocol.Message{Path: r.Path, Body: aTestByteMessage}) } // when I send one more message done := make(chan bool) go func() { router.HandleMessage(&protocol.Message{Path: r.Path, Body: aTestByteMessage}) done <- true }() // then: it returns immediately select { case <-done: case <-time.After(time.Millisecond * 10): a.Fail("Not returning!") } time.Sleep(time.Millisecond) // fetch messages from the channel for i := 0; i < chanSize; i++ { select { case _, open := <-r.MessagesChannel(): a.True(open) case <-time.After(time.Millisecond * 10): a.Fail("error not enough messages in channel") } } // and the channel is closed select { case _, open := <-r.MessagesChannel(): a.False(open) default: logger.Debug("len(r.C): %v", len(r.MessagesChannel())) a.Fail("channel was not closed") } } // Router should handle the buffered messages also after the closing of the route func TestRouter_CleanShutdown(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) var ID uint64 msMock := NewMockMessageStore(ctrl) msMock.EXPECT().Store("blah", gomock.Any(), gomock.Any()). Return(nil). Do(func(partition string, callback func(msgID uint64) []byte) error { ID++ callback(ID) return nil }). AnyTimes() router, _, _, _ := aStartedRouter() router.messageStore = msMock route, err := router.Subscribe(NewRoute( RouteConfig{ RouteParams: RouteParams{"application_id": "appid01", "user_id": "user01"}, Path: protocol.Path("/blah"), ChannelSize: 3, }, )) a.Nil(err) doneC := make(chan bool) // read the messages until done is closed go func() { for { _, ok := <-route.MessagesChannel() select { case <-doneC: return default: a.True(ok) } } }() // Send messages in the router until error go func() { for { errHandle := router.HandleMessage(&protocol.Message{ Path: protocol.Path("/blah"), Body: aTestByteMessage, }) if errHandle != nil { mse, ok := errHandle.(*ModuleStoppingError) a.True(ok) a.Equal("Router", mse.Name) return } // if doneC channel has been closed and no error then we must fail the test select { case _, ok := <-doneC: if !ok { a.Fail("Expected error from router handle message") } default: } } }() close(doneC) err = router.Stop() a.Nil(err) // wait for above goroutine to finish <-time.After(100 * time.Millisecond) } func TestRouter_Check(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) amMock := NewMockAccessManager(ctrl) msMock := NewMockMessageStore(ctrl) kvsMock := NewMockKVStore(ctrl) msCheckerMock := newMSChecker() kvsCheckerMock := newKVSChecker() // Given a Multiplexer with route router, _, _, _ := aStartedRouter() // Test 0: Router is healthy by default a.Nil(router.Check()) // Test 1a: Given accessManager is nil, then router's Check returns error router.accessManager = nil router.messageStore = msMock router.kvStore = kvsMock a.NotNil(router.Check()) // Test 1b: Given messageStore is nil, then router's Check returns error router.accessManager = amMock router.messageStore = nil router.kvStore = kvsMock a.NotNil(router.Check()) // Test 1c: Given kvStore is nil, then router's Check return error router.accessManager = amMock router.messageStore = msMock router.kvStore = nil a.NotNil(router.Check()) // Test 2: Given mocked store dependencies, both healthy router.accessManager = amMock router.messageStore = msCheckerMock router.kvStore = kvsCheckerMock msCheckerMock.MockChecker.EXPECT().Check().Return(nil) kvsCheckerMock.MockChecker.EXPECT().Check().Return(nil) // Then the aggregated router health check will return "no error" / nil a.Nil(router.Check()) // Test 3: Given a mocked messageStore which returns error on Check(), // Then router's aggregated Check() should return error msCheckerMock.MockChecker.EXPECT().Check().Return(errors.New("Storage is almost full")) a.NotNil(router.Check()) // Test 4: Given a mocked kvStore which returns an error on Check() // and a healthy messageStore, // Then router's aggregated Check should return error msCheckerMock.MockChecker.EXPECT().Check().Return(nil) kvsCheckerMock.MockChecker.EXPECT().Check().Return(errors.New("DB closed")) a.NotNil(router.Check()) } func TestPanicOnInternalDependencies(t *testing.T) { defer testutil.ExpectPanic(t) router := New(nil, nil, nil, nil).(*router) router.panicIfInternalDependenciesAreNil() } func aStartedRouter() (*router, auth.AccessManager, store.MessageStore, kvstore.KVStore) { am := auth.NewAllowAllAccessManager(true) kvs := kvstore.NewMemoryKVStore() ms := dummystore.New(kvs) router := New(am, ms, kvs, nil).(*router) router.Start() return router, am, ms, kvs } func aRouterRoute(unused int) (*router, *Route) { router, _, _, _ := aStartedRouter() route, _ := router.Subscribe(NewRoute( RouteConfig{ RouteParams: RouteParams{"application_id": "appid01", "user_id": "user01"}, Path: protocol.Path("/blah"), ChannelSize: chanSize, }, )) return router, route } func assertChannelContainsMessage(a *assert.Assertions, c <-chan *protocol.Message, msg []byte) { select { case m := <-c: a.Equal(string(msg), string(m.Body)) case <-time.After(time.Millisecond * 5): a.Fail("No message received") } } ================================================ FILE: server/service/logger.go ================================================ package service import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithFields(log.Fields{ "module": "service", }) ================================================ FILE: server/service/mocks_checker_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/docker/distribution/health (interfaces: Checker) package service import ( gomock "github.com/golang/mock/gomock" ) // Mock of Checker interface type MockChecker struct { ctrl *gomock.Controller recorder *_MockCheckerRecorder } // Recorder for MockChecker (not exported) type _MockCheckerRecorder struct { mock *MockChecker } func NewMockChecker(ctrl *gomock.Controller) *MockChecker { mock := &MockChecker{ctrl: ctrl} mock.recorder = &_MockCheckerRecorder{mock} return mock } func (_m *MockChecker) EXPECT() *_MockCheckerRecorder { return _m.recorder } func (_m *MockChecker) Check() error { ret := _m.ctrl.Call(_m, "Check") ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockCheckerRecorder) Check() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Check") } ================================================ FILE: server/service/mocks_router_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/router (interfaces: Router) package service import ( "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/cluster" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" ) // Mock of Router interface type MockRouter struct { ctrl *gomock.Controller recorder *_MockRouterRecorder } // Recorder for MockRouter (not exported) type _MockRouterRecorder struct { mock *MockRouter } func NewMockRouter(ctrl *gomock.Controller) *MockRouter { mock := &MockRouter{ctrl: ctrl} mock.recorder = &_MockRouterRecorder{mock} return mock } func (_m *MockRouter) EXPECT() *_MockRouterRecorder { return _m.recorder } func (_m *MockRouter) AccessManager() (auth.AccessManager, error) { ret := _m.ctrl.Call(_m, "AccessManager") ret0, _ := ret[0].(auth.AccessManager) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) AccessManager() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "AccessManager") } func (_m *MockRouter) Cluster() *cluster.Cluster { ret := _m.ctrl.Call(_m, "Cluster") ret0, _ := ret[0].(*cluster.Cluster) return ret0 } func (_mr *_MockRouterRecorder) Cluster() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Cluster") } func (_m *MockRouter) Done() <-chan bool { ret := _m.ctrl.Call(_m, "Done") ret0, _ := ret[0].(<-chan bool) return ret0 } func (_mr *_MockRouterRecorder) Done() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Done") } func (_m *MockRouter) Fetch(_param0 *store.FetchRequest) error { ret := _m.ctrl.Call(_m, "Fetch", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockRouter) GetSubscribers(_param0 string) ([]byte, error) { ret := _m.ctrl.Call(_m, "GetSubscribers", _param0) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) GetSubscribers(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetSubscribers", arg0) } func (_m *MockRouter) HandleMessage(_param0 *protocol.Message) error { ret := _m.ctrl.Call(_m, "HandleMessage", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) HandleMessage(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "HandleMessage", arg0) } func (_m *MockRouter) KVStore() (kvstore.KVStore, error) { ret := _m.ctrl.Call(_m, "KVStore") ret0, _ := ret[0].(kvstore.KVStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) KVStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "KVStore") } func (_m *MockRouter) MessageStore() (store.MessageStore, error) { ret := _m.ctrl.Call(_m, "MessageStore") ret0, _ := ret[0].(store.MessageStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) MessageStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MessageStore") } func (_m *MockRouter) Subscribe(_param0 *router.Route) (*router.Route, error) { ret := _m.ctrl.Call(_m, "Subscribe", _param0) ret0, _ := ret[0].(*router.Route) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) Subscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Subscribe", arg0) } func (_m *MockRouter) Unsubscribe(_param0 *router.Route) { _m.ctrl.Call(_m, "Unsubscribe", _param0) } func (_mr *_MockRouterRecorder) Unsubscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Unsubscribe", arg0) } ================================================ FILE: server/service/module.go ================================================ package service import ( "net/http" "sort" ) // Startable interface for modules which provide a start mechanism type Startable interface { Start() error } // Stopable interface for modules which provide a stop mechanism type Stopable interface { Stop() error } // Endpoint adds a HTTP handler for the `GetPrefix()` to the webserver type Endpoint interface { http.Handler GetPrefix() string } type module struct { iface interface{} startLevel int stopLevel int } type by func(m1, m2 *module) bool type moduleSorter struct { modules []module by func(m1, m2 *module) bool } func (criteria by) sort(modules []module) { ms := &moduleSorter{ modules: modules, by: criteria, } sort.Sort(ms) } // functions implementing the sort.Interface func (s *moduleSorter) Len() int { return len(s.modules) } func (s *moduleSorter) Swap(i, j int) { s.modules[i], s.modules[j] = s.modules[j], s.modules[i] } func (s *moduleSorter) Less(i, j int) bool { return s.by(&s.modules[i], &s.modules[j]) } var ascendingStartOrder = func(m1, m2 *module) bool { return m1.startLevel < m2.startLevel } var ascendingStopOrder = func(m1, m2 *module) bool { return m1.stopLevel < m2.stopLevel } ================================================ FILE: server/service/service.go ================================================ package service import ( log "github.com/Sirupsen/logrus" "github.com/docker/distribution/health" "github.com/smancke/guble/server/metrics" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/webserver" "github.com/hashicorp/go-multierror" "net/http" "reflect" "time" ) const ( defaultHealthFrequency = time.Second * 60 defaultHealthThreshold = 1 ) // Service is the main struct for controlling a guble server type Service struct { webserver *webserver.WebServer router router.Router modules []module healthEndpoint string healthFrequency time.Duration healthThreshold int metricsEndpoint string } // New creates a new Service, using the given Router and WebServer. // If the router has already a configured Cluster, it is registered as a service module. // The Router and Webserver are then registered as modules. func New(router router.Router, webserver *webserver.WebServer) *Service { s := &Service{ webserver: webserver, router: router, healthFrequency: defaultHealthFrequency, healthThreshold: defaultHealthThreshold, } cluster := router.Cluster() if cluster != nil { s.RegisterModules(1, 5, cluster) router.Cluster().Router = router } s.RegisterModules(2, 2, s.router) s.RegisterModules(3, 4, s.webserver) return s } // RegisterModules adds more modules (which can be Startable, Stopable, Endpoint etc.) to the service, // with their start and stop ordering across all the service's modules. func (s *Service) RegisterModules(startOrder int, stopOrder int, ifaces ...interface{}) { logger.WithFields(log.Fields{ "numberOfNewModules": len(ifaces), "numberOfExistingModules": len(s.modules), }).Info("RegisterModules") for _, i := range ifaces { m := module{ iface: i, startLevel: startOrder, stopLevel: stopOrder, } s.modules = append(s.modules, m) } } // HealthEndpoint sets the endpoint used for health. Parameter for disabling the endpoint is: "". Returns the updated service. func (s *Service) HealthEndpoint(endpointPrefix string) *Service { s.healthEndpoint = endpointPrefix return s } // MetricsEndpoint sets the endpoint used for metrics. Parameter for disabling the endpoint is: "". Returns the updated service. func (s *Service) MetricsEndpoint(endpointPrefix string) *Service { s.metricsEndpoint = endpointPrefix return s } // Start checks the modules for the following interfaces and registers and/or starts: // Startable: // health.Checker: // Endpoint: Register the handler function of the Endpoint in the http service at prefix func (s *Service) Start() error { var multierr *multierror.Error if s.healthEndpoint != "" { logger.WithField("healthEndpoint", s.healthEndpoint).Info("Health endpoint") s.webserver.Handle(s.healthEndpoint, http.HandlerFunc(health.StatusHandler)) } else { logger.Info("Health endpoint disabled") } if s.metricsEndpoint != "" { logger.WithField("metricsEndpoint", s.metricsEndpoint).Info("Metrics endpoint") s.webserver.Handle(s.metricsEndpoint, http.HandlerFunc(metrics.HttpHandler)) } else { logger.Info("Metrics endpoint disabled") } for order, iface := range s.ModulesSortedByStartOrder() { name := reflect.TypeOf(iface).String() if s, ok := iface.(Startable); ok { logger.WithFields(log.Fields{"name": name, "order": order}).Info("Starting module") if err := s.Start(); err != nil { logger.WithError(err).WithField("name", name).Error("Error while starting module") multierr = multierror.Append(multierr, err) } } else { logger.WithFields(log.Fields{"name": name, "order": order}).Debug("Module is not startable") } if c, ok := iface.(health.Checker); ok && s.healthEndpoint != "" { logger.WithField("name", name).Info("Registering module as Health-Checker") health.RegisterPeriodicThresholdFunc(name, s.healthFrequency, s.healthThreshold, health.CheckFunc(c.Check)) } if e, ok := iface.(Endpoint); ok { prefix := e.GetPrefix() logger.WithFields(log.Fields{"name": name, "prefix": prefix}).Info("Registering module as Endpoint") s.webserver.Handle(prefix, e) } } return multierr.ErrorOrNil() } // Stop stops the registered modules in their given order func (s *Service) Stop() error { var multierr *multierror.Error for order, iface := range s.modulesSortedBy(ascendingStopOrder) { name := reflect.TypeOf(iface).String() if s, ok := iface.(Stopable); ok { logger.WithFields(log.Fields{"name": name, "order": order}).Info("Stopping module") if err := s.Stop(); err != nil { multierr = multierror.Append(multierr, err) } } else { logger.WithFields(log.Fields{"name": name, "order": order}).Debug("Module is not stoppable") } } return multierr.ErrorOrNil() } // WebServer returns the service *webserver.WebServer instance func (s *Service) WebServer() *webserver.WebServer { return s.webserver } // ModulesSortedByStartOrder returns the registered modules sorted by their startOrder property func (s *Service) ModulesSortedByStartOrder() []interface{} { return s.modulesSortedBy(ascendingStartOrder) } // modulesSortedBy returns the registered modules sorted using a `by` criteria. func (s *Service) modulesSortedBy(criteria by) []interface{} { var sorted []interface{} by(criteria).sort(s.modules) for _, m := range s.modules { sorted = append(sorted, m.iface) } return sorted } ================================================ FILE: server/service/service_test.go ================================================ package service import ( "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/store" "github.com/smancke/guble/server/store/dummystore" "github.com/smancke/guble/server/webserver" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" "errors" "fmt" "io/ioutil" "net/http" "testing" "time" ) func TestStartingOfModules(t *testing.T) { defer testutil.ResetDefaultRegistryHealthCheck() a := assert.New(t) var p interface{} func() { defer func() { p = recover() }() service, _, _, _ := aMockedServiceWithMockedRouterStandalone() service.RegisterModules(0, 0, &testStartable{}) a.Equal(3, len(service.ModulesSortedByStartOrder())) service.Start() }() a.NotNil(p) } func TestStoppingOfModules(t *testing.T) { defer testutil.ResetDefaultRegistryHealthCheck() var p interface{} func() { defer func() { p = recover() }() service, _, _, _ := aMockedServiceWithMockedRouterStandalone() service.RegisterModules(0, 0, &testStopable{}) service.Stop() }() assert.NotNil(t, p) } func TestEndpointRegisterAndServing(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() defer testutil.ResetDefaultRegistryHealthCheck() a := assert.New(t) // given: service, _, _, _ := aMockedServiceWithMockedRouterStandalone() // when I register an endpoint at path /foo service.RegisterModules(0, 0, &testEndpoint{}) a.Equal(3, len(service.ModulesSortedByStartOrder())) service.Start() defer service.Stop() time.Sleep(time.Millisecond * 10) // then I can call the handler url := fmt.Sprintf("http://%s/foo", service.WebServer().GetAddr()) result, err := http.Get(url) a.NoError(err) body := make([]byte, 3) result.Body.Read(body) a.Equal("bar", string(body)) } func TestHealthUp(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() defer testutil.ResetDefaultRegistryHealthCheck() a := assert.New(t) // given: service, _, _, _ := aMockedServiceWithMockedRouterStandalone() service = service.HealthEndpoint("/health_url") a.Equal(2, len(service.ModulesSortedByStartOrder())) // when starting the service defer service.Stop() service.Start() time.Sleep(time.Millisecond * 10) // and when I call the health URL url := fmt.Sprintf("http://%s/health_url", service.WebServer().GetAddr()) result, err := http.Get(url) // then I get status 200 and JSON: {} a.NoError(err) body, err := ioutil.ReadAll(result.Body) a.NoError(err) a.Equal("{}", string(body)) } func TestHealthDown(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() defer testutil.ResetDefaultRegistryHealthCheck() a := assert.New(t) // given: service, _, _, _ := aMockedServiceWithMockedRouterStandalone() service = service.HealthEndpoint("/health_url") mockChecker := NewMockChecker(ctrl) mockChecker.EXPECT().Check().Return(errors.New("sick")).AnyTimes() // when starting the service with a short frequency defer service.Stop() service.healthFrequency = time.Millisecond * 3 service.RegisterModules(0, 0, mockChecker) a.Equal(3, len(service.ModulesSortedByStartOrder())) service.Start() time.Sleep(time.Millisecond * 10) // and when I can call the health URL url := fmt.Sprintf("http://%s/health_url", service.WebServer().GetAddr()) result, err := http.Get(url) // then I receive status 503 and a JSON error message a.NoError(err) a.Equal(503, result.StatusCode) body, err := ioutil.ReadAll(result.Body) a.NoError(err) a.Equal("{\"*service.MockChecker\":\"sick\"}", string(body)) } func TestMetricsEnabled(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() defer testutil.ResetDefaultRegistryHealthCheck() a := assert.New(t) // given: service, _, _, _ := aMockedServiceWithMockedRouterStandalone() service = service.MetricsEndpoint("/metrics_url") a.Equal(2, len(service.ModulesSortedByStartOrder())) // when starting the service defer service.Stop() service.Start() time.Sleep(time.Millisecond * 10) // and when I call the health URL url := fmt.Sprintf("http://%s/metrics_url", service.WebServer().GetAddr()) result, err := http.Get(url) // then I get status 200 and JSON: {} a.NoError(err) body, err := ioutil.ReadAll(result.Body) a.NoError(err) a.True(len(body) > 0) } func aMockedServiceWithMockedRouterStandalone() (*Service, kvstore.KVStore, store.MessageStore, *MockRouter) { kvStore := kvstore.NewMemoryKVStore() messageStore := dummystore.New(kvStore) routerMock := NewMockRouter(testutil.MockCtrl) routerMock.EXPECT().Cluster().Return(nil).MaxTimes(2) service := New(routerMock, webserver.New("localhost:0")) return service, kvStore, messageStore, routerMock } type testEndpoint struct { } func (*testEndpoint) GetPrefix() string { return "/foo" } func (*testEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "bar") return } type testStartable struct { } func (*testStartable) Start() error { panic(fmt.Errorf("In a panic when I should start")) } type testStopable struct { } func (*testStopable) Stop() error { panic(fmt.Errorf("In a panic when I should stop")) } ================================================ FILE: server/sms/logger.go ================================================ package sms import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithField("module", "sms") ================================================ FILE: server/sms/mocks_router_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/router (interfaces: Router) package sms import ( "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/cluster" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" ) // Mock of Router interface type MockRouter struct { ctrl *gomock.Controller recorder *_MockRouterRecorder } // Recorder for MockRouter (not exported) type _MockRouterRecorder struct { mock *MockRouter } func NewMockRouter(ctrl *gomock.Controller) *MockRouter { mock := &MockRouter{ctrl: ctrl} mock.recorder = &_MockRouterRecorder{mock} return mock } func (_m *MockRouter) EXPECT() *_MockRouterRecorder { return _m.recorder } func (_m *MockRouter) AccessManager() (auth.AccessManager, error) { ret := _m.ctrl.Call(_m, "AccessManager") ret0, _ := ret[0].(auth.AccessManager) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) AccessManager() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "AccessManager") } func (_m *MockRouter) Cluster() *cluster.Cluster { ret := _m.ctrl.Call(_m, "Cluster") ret0, _ := ret[0].(*cluster.Cluster) return ret0 } func (_mr *_MockRouterRecorder) Cluster() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Cluster") } func (_m *MockRouter) Done() <-chan bool { ret := _m.ctrl.Call(_m, "Done") ret0, _ := ret[0].(<-chan bool) return ret0 } func (_mr *_MockRouterRecorder) Done() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Done") } func (_m *MockRouter) Fetch(_param0 *store.FetchRequest) error { ret := _m.ctrl.Call(_m, "Fetch", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockRouter) GetSubscribers(_param0 string) ([]byte, error) { ret := _m.ctrl.Call(_m, "GetSubscribers", _param0) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) GetSubscribers(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetSubscribers", arg0) } func (_m *MockRouter) HandleMessage(_param0 *protocol.Message) error { ret := _m.ctrl.Call(_m, "HandleMessage", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) HandleMessage(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "HandleMessage", arg0) } func (_m *MockRouter) KVStore() (kvstore.KVStore, error) { ret := _m.ctrl.Call(_m, "KVStore") ret0, _ := ret[0].(kvstore.KVStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) KVStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "KVStore") } func (_m *MockRouter) MessageStore() (store.MessageStore, error) { ret := _m.ctrl.Call(_m, "MessageStore") ret0, _ := ret[0].(store.MessageStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) MessageStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MessageStore") } func (_m *MockRouter) Subscribe(_param0 *router.Route) (*router.Route, error) { ret := _m.ctrl.Call(_m, "Subscribe", _param0) ret0, _ := ret[0].(*router.Route) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) Subscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Subscribe", arg0) } func (_m *MockRouter) Unsubscribe(_param0 *router.Route) { _m.ctrl.Call(_m, "Unsubscribe", _param0) } func (_mr *_MockRouterRecorder) Unsubscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Unsubscribe", arg0) } ================================================ FILE: server/sms/mocks_sender_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/sms (interfaces: Sender) package sms import ( gomock "github.com/golang/mock/gomock" protocol "github.com/smancke/guble/protocol" ) // Mock of Sender interface type MockSender struct { ctrl *gomock.Controller recorder *_MockSenderRecorder } // Recorder for MockSender (not exported) type _MockSenderRecorder struct { mock *MockSender } func NewMockSender(ctrl *gomock.Controller) *MockSender { mock := &MockSender{ctrl: ctrl} mock.recorder = &_MockSenderRecorder{mock} return mock } func (_m *MockSender) EXPECT() *_MockSenderRecorder { return _m.recorder } func (_m *MockSender) Send(_param0 *protocol.Message) error { ret := _m.ctrl.Call(_m, "Send", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockSenderRecorder) Send(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Send", arg0) } ================================================ FILE: server/sms/mocks_store_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/store (interfaces: MessageStore) package sms import ( gomock "github.com/golang/mock/gomock" protocol "github.com/smancke/guble/protocol" store "github.com/smancke/guble/server/store" ) // Mock of MessageStore interface type MockMessageStore struct { ctrl *gomock.Controller recorder *_MockMessageStoreRecorder } // Recorder for MockMessageStore (not exported) type _MockMessageStoreRecorder struct { mock *MockMessageStore } func NewMockMessageStore(ctrl *gomock.Controller) *MockMessageStore { mock := &MockMessageStore{ctrl: ctrl} mock.recorder = &_MockMessageStoreRecorder{mock} return mock } func (_m *MockMessageStore) EXPECT() *_MockMessageStoreRecorder { return _m.recorder } func (_m *MockMessageStore) DoInTx(_param0 string, _param1 func(uint64) error) error { ret := _m.ctrl.Call(_m, "DoInTx", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockMessageStoreRecorder) DoInTx(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "DoInTx", arg0, arg1) } func (_m *MockMessageStore) Fetch(_param0 *store.FetchRequest) { _m.ctrl.Call(_m, "Fetch", _param0) } func (_mr *_MockMessageStoreRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockMessageStore) GenerateNextMsgID(_param0 string, _param1 byte) (uint64, int64, error) { ret := _m.ctrl.Call(_m, "GenerateNextMsgID", _param0, _param1) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(int64) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } func (_mr *_MockMessageStoreRecorder) GenerateNextMsgID(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GenerateNextMsgID", arg0, arg1) } func (_m *MockMessageStore) MaxMessageID(_param0 string) (uint64, error) { ret := _m.ctrl.Call(_m, "MaxMessageID", _param0) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) MaxMessageID(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MaxMessageID", arg0) } func (_m *MockMessageStore) Partition(_param0 string) (store.MessagePartition, error) { ret := _m.ctrl.Call(_m, "Partition", _param0) ret0, _ := ret[0].(store.MessagePartition) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) Partition(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Partition", arg0) } func (_m *MockMessageStore) Partitions() ([]store.MessagePartition, error) { ret := _m.ctrl.Call(_m, "Partitions") ret0, _ := ret[0].([]store.MessagePartition) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) Partitions() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Partitions") } func (_m *MockMessageStore) Store(_param0 string, _param1 uint64, _param2 []byte) error { ret := _m.ctrl.Call(_m, "Store", _param0, _param1, _param2) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockMessageStoreRecorder) Store(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Store", arg0, arg1, arg2) } func (_m *MockMessageStore) StoreMessage(_param0 *protocol.Message, _param1 byte) (int, error) { ret := _m.ctrl.Call(_m, "StoreMessage", _param0, _param1) ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) StoreMessage(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "StoreMessage", arg0, arg1) } ================================================ FILE: server/sms/nexmo_sms.go ================================================ package sms import "encoding/json" type NexmoSms struct { ApiKey string `json:"api_key,omitempty"` ApiSecret string `json:"api_secret,omitempty"` To string `json:"to"` From string `json:"from"` Text string `json:"text"` } func (sms *NexmoSms) EncodeNexmoSms(apiKey, apiSecret string) ([]byte, error) { sms.ApiKey = apiKey sms.ApiSecret = apiSecret d, err := json.Marshal(&sms) if err != nil { logger.WithField("error", err.Error()).Error("Could not encode sms as json") return nil, err } return d, nil } ================================================ FILE: server/sms/nexmo_sms_sender.go ================================================ package sms import ( "bytes" "encoding/json" "errors" "io/ioutil" "net/http" "strconv" "time" log "github.com/Sirupsen/logrus" "github.com/smancke/guble/protocol" ) var ( URL = "https://rest.nexmo.com/sms/json?" MaxIdleConnections = 100 RequestTimeout = 500 * time.Millisecond ) type ResponseCode int const ( ResponseSuccess ResponseCode = iota ResponseThrottled ResponseMissingParams ResponseInvalidParams ResponseInvalidCredentials ResponseInternalError ResponseInvalidMessage ResponseNumberBarred ResponsePartnerAcctBarred ResponsePartnerQuotaExceeded ResponseUnused ResponseRESTNotEnabled ResponseMessageTooLong ResponseCommunicationFailed ResponseInvalidSignature ResponseInvalidSenderAddress ResponseInvalidTTL ResponseFacilityNotAllowed ResponseInvalidMessageClass ) var ( ErrNoSMSSent = errors.New("No sms was sent to Nexmo") ErrIncompleteSMSSent = errors.New("Nexmo sms was only partial delivered.One or more part returned an error") ErrSMSResponseDecodingFailed = errors.New("Nexmo response decoding failed.") ErrNoRetry = errors.New("SMS failed. No retrying.") ) var nexmoResponseCodeMap = map[ResponseCode]string{ ResponseSuccess: "Success", ResponseThrottled: "Throttled", ResponseMissingParams: "Missing params", ResponseInvalidParams: "Invalid params", ResponseInvalidCredentials: "Invalid credentials", ResponseInternalError: "Internal error", ResponseInvalidMessage: "Invalid message", ResponseNumberBarred: "Number barred", ResponsePartnerAcctBarred: "Partner account barred", ResponsePartnerQuotaExceeded: "Partner quota exceeded", ResponseRESTNotEnabled: "Account not enabled for REST", ResponseMessageTooLong: "Message too long", ResponseCommunicationFailed: "Communication failed", ResponseInvalidSignature: "Invalid signature", ResponseInvalidSenderAddress: "Invalid sender address", ResponseInvalidTTL: "Invalid TTL", ResponseFacilityNotAllowed: "Facility not allowed", ResponseInvalidMessageClass: "Invalid message class", } func (c ResponseCode) String() string { return nexmoResponseCodeMap[c] } // NexmoMessageReport is the "status report" for a single SMS sent via the Nexmo API type NexmoMessageReport struct { Status ResponseCode `json:"status,string"` MessageID string `json:"message-id"` To string `json:"to"` ClientReference string `json:"client-ref"` RemainingBalance string `json:"remaining-balance"` MessagePrice string `json:"message-price"` Network string `json:"network"` ErrorText string `json:"error-text"` } type NexmoMessageResponse struct { MessageCount int `json:"message-count,string"` Messages []NexmoMessageReport `json:"messages"` } func (nm NexmoMessageResponse) Check() error { if nm.MessageCount == 0 { return ErrNoSMSSent } for i := 0; i < nm.MessageCount; i++ { if nm.Messages[i].Status != ResponseSuccess { logger.WithField("status", nm.Messages[i].Status). WithField("error", nm.Messages[i].ErrorText). Error("Error received from Nexmo") if nm.Messages[i].Status == ResponseInvalidSenderAddress { return nil } return ErrIncompleteSMSSent } } return nil } type NexmoSender struct { logger *log.Entry ApiKey string ApiSecret string httpClient *http.Client } func NewNexmoSender(apiKey, apiSecret string) (*NexmoSender, error) { ns := &NexmoSender{ logger: logger.WithField("name", "nexmoSender"), ApiKey: apiKey, ApiSecret: apiSecret, } ns.createHttpClient() return ns, nil } func (ns *NexmoSender) Send(msg *protocol.Message) error { nexmoSMS := new(NexmoSms) err := json.Unmarshal(msg.Body, nexmoSMS) if err != nil { logger.WithField("error", err.Error()).Error("Could not decode message body to send to nexmo") return err } nexmoSMSResponse, err := ns.sendSms(nexmoSMS) if err != nil { logger.WithField("error", err.Error()).Error("Could not decode nexmo response message body") return err } logger.WithField("response", nexmoSMSResponse).Info("Decoded nexmo response") return nexmoSMSResponse.Check() } func (ns *NexmoSender) sendSms(sms *NexmoSms) (*NexmoMessageResponse, error) { // log before encoding logger.WithField("sms_details", sms).Info("sendSms") smsEncoded, err := sms.EncodeNexmoSms(ns.ApiKey, ns.ApiSecret) if err != nil { logger.WithField("error", err.Error()).Error("Error encoding sms") return nil, err } req, err := http.NewRequest(http.MethodPost, URL, bytes.NewBuffer(smsEncoded)) req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Length", strconv.Itoa(len(smsEncoded))) resp, err := (&http.Client{}).Do(req) if err != nil { logger.WithField("error", err.Error()).Error("Error doing the request to nexmo endpoint") ns.createHttpClient() mTotalSendErrors.Add(1) return nil, ErrNoSMSSent } defer resp.Body.Close() var messageResponse *NexmoMessageResponse respBody, err := ioutil.ReadAll(resp.Body) if err != nil { logger.WithField("error", err.Error()).Error("Error reading the nexmo body response") mTotalResponseInternalErrors.Add(1) return nil, ErrSMSResponseDecodingFailed } err = json.Unmarshal(respBody, &messageResponse) if err != nil { logger.WithField("error", err.Error()).Error("Error decoding the response from nexmo endpoint") mTotalResponseInternalErrors.Add(1) return nil, ErrSMSResponseDecodingFailed } logger.WithField("messageResponse", messageResponse).Info("Actual nexmo response") return messageResponse, nil } func (ns *NexmoSender) createHttpClient() { logger.Info("Recreating HTTP client for nexmo sender") ns.httpClient = &http.Client{ Transport: &http.Transport{ MaxIdleConnsPerHost: MaxIdleConnections, }, Timeout: RequestTimeout, } } ================================================ FILE: server/sms/nexmo_sms_sender_test.go ================================================ package sms import ( "encoding/json" "testing" "time" "github.com/smancke/guble/protocol" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" ) const ( KEY = "ce40b46d" SECRET = "153d2b2c72985370" ) func TestNexmoSender_Send(t *testing.T) { a := assert.New(t) testutil.SkipIfDisabled(t) sender, err := NewNexmoSender(KEY, SECRET) a.NoError(err) sms := new(NexmoSms) sms.To = "+40746278186" sms.From = "REWE Lieferservice" sms.Text = "Lieber Kunde! Ihre Lieferung kommt heute zwischen 12.04 und 12.34 Uhr. Vielen Dank für Ihre Bestellung! Ihr REWE Lieferservice" response, err := sender.sendSms(sms) a.Equal(1, response.MessageCount) a.Equal(ResponseSuccess, response.Messages[0].Status) a.NoError(err) } func TestNexmoSender_SendWithError(t *testing.T) { RequestTimeout = time.Second a := assert.New(t) sender, err := NewNexmoSender(KEY, SECRET) a.NoError(err) sms := NexmoSms{ To: "toNumber", From: "FromNUmber", Text: "body", } d, err := json.Marshal(&sms) a.NoError(err) msg := protocol.Message{ Path: protocol.Path(SMSDefaultTopic), UserID: "samsa", ApplicationID: "sms", ID: uint64(4), Body: d, } err = sender.Send(&msg) a.Error(err) a.Equal(ErrIncompleteSMSSent, err) } ================================================ FILE: server/sms/sms_gateway.go ================================================ package sms import ( "context" "encoding/json" "errors" "github.com/smancke/guble/server/connector" "time" log "github.com/Sirupsen/logrus" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/metrics" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" ) const ( SMSSchema = "sms_notifications" SMSDefaultTopic = "/sms" ) var ( ErrRetryFailed = errors.New("Failed retrying to send message.") ) type Sender interface { Send(*protocol.Message) error } type Config struct { Enabled *bool APIKey *string APISecret *string Workers *int SMSTopic *string IntervalMetrics *bool Name string Schema string } type gateway struct { config *Config sender Sender router router.Router route *router.Route LastIDSent uint64 ctx context.Context cancelFunc context.CancelFunc logger *log.Entry } func New(router router.Router, sender Sender, config Config) (*gateway, error) { if *config.Workers <= 0 { *config.Workers = connector.DefaultWorkers } config.Schema = SMSSchema config.Name = SMSDefaultTopic return &gateway{ config: &config, router: router, sender: sender, logger: logger.WithField("name", config.Name), }, nil } func (g *gateway) Start() error { g.logger.Debug("Starting gateway") err := g.ReadLastID() if err != nil { return err } g.ctx, g.cancelFunc = context.WithCancel(context.Background()) g.initRoute() go g.Run() g.startMetrics() g.logger.Debug("Started gateway") return nil } func (g *gateway) initRoute() { g.route = router.NewRoute(router.RouteConfig{ Path: protocol.Path(*g.config.SMSTopic), ChannelSize: 5000, FetchRequest: g.fetchRequest(), }) } func (g *gateway) fetchRequest() (fr *store.FetchRequest) { if g.LastIDSent > 0 { fr = store.NewFetchRequest( protocol.Path(*g.config.SMSTopic).Partition(), g.LastIDSent+1, 0, store.DirectionForward, -1) } return } func (g *gateway) Run() { g.logger.Debug("Run gateway") var provideErr error go func() { err := g.route.Provide(g.router, true) if err != nil { // cancel subscription loop if there is an error on the provider logger.WithField("error", err.Error()).Error("Provide returned error") provideErr = err g.Cancel() } }() currentMsg, err := g.proxyLoop() if err != nil && provideErr == nil { g.logger.WithFields(log.Fields{ "error": err.Error(), "is_incomplete_sms": err == ErrIncompleteSMSSent, }).Error("Error returned by gateway proxy loop") if err == ErrIncompleteSMSSent { err2 := g.retry(currentMsg) if err2 != nil { g.logger.WithField("error", err2.Error()).Error("Error returned by retry.") if err3 := g.SetLastSentID(currentMsg.ID); err3 != nil { g.logger.WithField("error", err3.Error()).Error("Error setting last ID") } } } // If Route channel closed, try restarting if isRestartableErr(err) { g.Restart() return } } if provideErr != nil { // TODO Bogdan Treat errors where a subscription provide fails g.logger.WithField("error", provideErr.Error()).Error("Route provide error") // Router closed the route, try restart if provideErr == router.ErrInvalidRoute { g.Restart() return } // Router module is stopping, exit the process if _, ok := provideErr.(*router.ModuleStoppingError); ok { return } } } func isRestartableErr(err error) bool { return err == connector.ErrRouteChannelClosed || err == ErrNoSMSSent || err == ErrIncompleteSMSSent || err == ErrSMSResponseDecodingFailed } // proxyLoop returns the current processed message alongside the error that // occured during sending of the message func (g *gateway) proxyLoop() (*protocol.Message, error) { var ( opened bool = true receivedMsg *protocol.Message ) defer func() { g.cancelFunc = nil }() for opened { select { case receivedMsg, opened = <-g.route.MessagesChannel(): if !opened { logger.WithField("receivedMsg", receivedMsg).Info("not open") break } err := g.send(receivedMsg) if err != nil { return receivedMsg, err } case <-g.ctx.Done(): // If the parent context is still running then only this subscriber context // has been cancelled if g.ctx.Err() == nil { return nil, g.ctx.Err() } return nil, nil } } //TODO Cosmin Bogdan returning this error can mean 2 things: overflow of route's channel, or intentional stopping of router / gubled. return nil, connector.ErrRouteChannelClosed } func (g *gateway) retry(msg *protocol.Message) error { l := logger.WithField("message", msg) l.Info("Retrying to send message") for i := 0; i < 3; i++ { l.WithField("retry", i+1).Info("Sending message") err := g.send(msg) if err != nil { l.WithFields(log.Fields{ "retry": i + 1, "err": err.Error(), }).Error("Retry failed") } else { l.WithField("retry", i+1).Info("Retry success") return nil } time.Sleep(100 * time.Millisecond) } return ErrRetryFailed } func (g *gateway) send(receivedMsg *protocol.Message) error { err := g.sender.Send(receivedMsg) if err != nil { log.WithField("error", err.Error()).Error("Sending of message failed") mTotalResponseErrors.Add(1) return err } mTotalSentMessages.Add(1) g.SetLastSentID(receivedMsg.ID) return nil } func (g *gateway) Restart() error { g.logger.WithField("LastIDSent", g.LastIDSent).Debug("Restart in progress") g.Cancel() g.cancelFunc = nil err := g.ReadLastID() if err != nil { return err } g.initRoute() go g.Run() g.logger.WithField("LastIDSent", g.LastIDSent).Debug("Restart finished") return nil } func (g *gateway) Stop() error { g.logger.Debug("Stopping gateway") g.cancelFunc() g.logger.Debug("Stopped gateway") return nil } func (g *gateway) SetLastSentID(ID uint64) error { g.logger.WithField("LastIDSent", ID).WithField("path", *g.config.SMSTopic).Debug("Seting LastIDSent") kvStore, err := g.router.KVStore() if err != nil { g.logger.WithField("error", err.Error()).Error("KVStore could not be accesed from gateway") return err } data, err := json.Marshal(struct{ ID uint64 }{ID: ID}) if err != nil { g.logger.WithField("error", err.Error()).Error("Error encoding last ID") return err } err = kvStore.Put(g.config.Schema, *g.config.SMSTopic, data) if err != nil { g.logger.WithField("error", err.Error()).WithField("path", *g.config.SMSTopic).Error("KVStore could not set value for LastIDSent for topic") return err } g.LastIDSent = ID return nil } func (g *gateway) ReadLastID() error { kvStore, err := g.router.KVStore() if err != nil { g.logger.WithField("error", err.Error()).Error("KVStore could not be accesed from sms gateway") return err } data, exist, err := kvStore.Get(g.config.Schema, *g.config.SMSTopic) if err != nil { g.logger.WithField("error", err.Error()).WithField("path", *g.config.SMSTopic).Error("KvStore could not get value for LastIDSent for topic") return err } if !exist { g.LastIDSent = 0 return nil } v := &struct{ ID uint64 }{} err = json.Unmarshal(data, v) if err != nil { g.logger.WithField("error", err.Error()).Error("Could not parse as uint64 the LastIDSent value stored in db") return err } g.LastIDSent = v.ID g.logger.WithField("LastIDSent", g.LastIDSent).WithField("path", *g.config.SMSTopic).Debug("ReadLastID") return nil } func (g *gateway) Cancel() { if g.cancelFunc != nil { g.cancelFunc() } } func (g *gateway) startMetrics() { mTotalSentMessages.Set(0) mTotalSendErrors.Set(0) mTotalResponseErrors.Set(0) mTotalResponseInternalErrors.Set(0) if *g.config.IntervalMetrics { g.startIntervalMetric(mMinute, time.Minute) g.startIntervalMetric(mHour, time.Hour) g.startIntervalMetric(mDay, time.Hour*24) } } func (g *gateway) startIntervalMetric(m metrics.Map, td time.Duration) { metrics.RegisterInterval(g.ctx, m, td, resetIntervalMetrics, processAndResetIntervalMetrics) } ================================================ FILE: server/sms/sms_gateway_test.go ================================================ package sms import ( "testing" "encoding/json" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" "strings" "time" "expvar" "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store/dummystore" ) func Test_StartStop(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) mockSmsSender := NewMockSender(ctrl) kvStore := kvstore.NewMemoryKVStore() a.NotNil(kvStore) routerMock := NewMockRouter(testutil.MockCtrl) routerMock.EXPECT().KVStore().AnyTimes().Return(kvStore, nil) msgStore := dummystore.New(kvStore) routerMock.EXPECT().MessageStore().AnyTimes().Return(msgStore, nil) topic := "sms" worker := 1 intervalMetrics := true config := Config{ Workers: &worker, SMSTopic: &topic, Name: "test_gateway", Schema: SMSSchema, IntervalMetrics: &intervalMetrics, } routerMock.EXPECT().Subscribe(gomock.Any()).Do(func(r *router.Route) (*router.Route, error) { a.Equal(topic, r.Path.Partition()) return r, nil }) gw, err := New(routerMock, mockSmsSender, config) a.NoError(err) err = gw.Start() a.NoError(err) err = gw.Stop() a.NoError(err) time.Sleep(100 * time.Millisecond) } func Test_SendOneSms(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() defer testutil.EnableDebugForMethod()() a := assert.New(t) mockSmsSender := NewMockSender(ctrl) kvStore := kvstore.NewMemoryKVStore() a.NotNil(kvStore) routerMock := NewMockRouter(testutil.MockCtrl) routerMock.EXPECT().KVStore().AnyTimes().Return(kvStore, nil) msgStore := dummystore.New(kvStore) routerMock.EXPECT().MessageStore().AnyTimes().Return(msgStore, nil) topic := "/sms" worker := 1 intervalMetrics := true config := Config{ Workers: &worker, SMSTopic: &topic, Name: "test_gateway", Schema: SMSSchema, IntervalMetrics: &intervalMetrics, } routerMock.EXPECT().Subscribe(gomock.Any()).Do(func(r *router.Route) (*router.Route, error) { a.Equal(topic, string(r.Path)) return r, nil }) gw, err := New(routerMock, mockSmsSender, config) a.NoError(err) err = gw.Start() a.NoError(err) sms := NexmoSms{ To: "toNumber", From: "FromNUmber", Text: "body", } d, err := json.Marshal(&sms) a.NoError(err) msg := protocol.Message{ Path: protocol.Path(topic), ID: uint64(4), Body: d, } mockSmsSender.EXPECT().Send(gomock.Eq(&msg)).Return(nil) a.NotNil(gw.route) gw.route.Deliver(&msg, true) time.Sleep(100 * time.Millisecond) err = gw.Stop() a.NoError(err) err = gw.ReadLastID() a.NoError(err) time.Sleep(100 * time.Millisecond) totalSentCount := expvar.NewInt("total_sent_messages") totalSentCount.Add(1) a.Equal(totalSentCount, mTotalSentMessages) } func Test_Restart(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() defer testutil.EnableDebugForMethod()() a := assert.New(t) mockSmsSender := NewMockSender(ctrl) kvStore := kvstore.NewMemoryKVStore() a.NotNil(kvStore) routerMock := NewMockRouter(testutil.MockCtrl) routerMock.EXPECT().KVStore().AnyTimes().Return(kvStore, nil) msgStore := NewMockMessageStore(ctrl) routerMock.EXPECT().MessageStore().AnyTimes().Return(msgStore, nil) topic := "/sms" worker := 1 intervalMetrics := true config := Config{ Workers: &worker, SMSTopic: &topic, Name: "test_gateway", Schema: SMSSchema, IntervalMetrics: &intervalMetrics, } routerMock.EXPECT().Subscribe(gomock.Any()).Do(func(r *router.Route) (*router.Route, error) { a.Equal(strings.Split(topic, "/")[1], r.Path.Partition()) return r, nil }).Times(2) gw, err := New(routerMock, mockSmsSender, config) a.NoError(err) err = gw.Start() a.NoError(err) sms := NexmoSms{ To: "toNumber", From: "FromNUmber", Text: "body", } d, err := json.Marshal(&sms) a.NoError(err) msg := protocol.Message{ Path: protocol.Path(topic), UserID: "samsa", ApplicationID: "sms", ID: uint64(4), Body: d, } mockSmsSender.EXPECT().Send(gomock.Eq(&msg)).Times(1).Return(ErrNoSMSSent) doneC := make(chan bool) routerMock.EXPECT().Done().AnyTimes().Return(doneC) // //mockSmsSender.EXPECT().Send(gomock.Eq(&msg)).Return(nil) a.NotNil(gw.route) gw.route.Deliver(&msg, true) time.Sleep(100 * time.Millisecond) } func TestReadLastID(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() defer testutil.EnableDebugForMethod()() a := assert.New(t) mockSmsSender := NewMockSender(ctrl) kvStore := kvstore.NewMemoryKVStore() a.NotNil(kvStore) routerMock := NewMockRouter(testutil.MockCtrl) routerMock.EXPECT().KVStore().AnyTimes().Return(kvStore, nil) msgStore := dummystore.New(kvStore) routerMock.EXPECT().MessageStore().AnyTimes().Return(msgStore, nil) topic := "/sms" worker := 1 config := Config{ Workers: &worker, SMSTopic: &topic, Name: "test_gateway", Schema: SMSSchema, } gw, err := New(routerMock, mockSmsSender, config) a.NoError(err) gw.SetLastSentID(uint64(10)) gw.ReadLastID() a.Equal(uint64(10), gw.LastIDSent) } ================================================ FILE: server/sms/sms_metrics.go ================================================ package sms import ( "github.com/smancke/guble/server/metrics" "time" ) var ( ns = metrics.NS("sms") mTotalSentMessages = ns.NewInt("total_sent_messages") mTotalSendErrors = ns.NewInt("total_sent_message_errors") mTotalResponseErrors = ns.NewInt("total_response_errors") mTotalResponseInternalErrors = ns.NewInt("total_response_internal_errors") mMinute = ns.NewMap("minute") mHour = ns.NewMap("hour") mDay = ns.NewMap("day") ) const ( currentTotalMessagesLatenciesKey = "current_messages_total_latencies_nanos" currentTotalMessagesKey = "current_messages_count" currentTotalErrorsLatenciesKey = "current_errors_total_latencies_nanos" currentTotalErrorsKey = "current_errors_count" ) func processAndResetIntervalMetrics(m metrics.Map, td time.Duration, t time.Time) { msgLatenciesValue := m.Get(currentTotalMessagesLatenciesKey) msgNumberValue := m.Get(currentTotalMessagesKey) errLatenciesValue := m.Get(currentTotalErrorsLatenciesKey) errNumberValue := m.Get(currentTotalErrorsKey) m.Init() resetIntervalMetrics(m, t) metrics.SetRate(m, "last_messages_rate_sec", msgNumberValue, td, time.Second) metrics.SetRate(m, "last_errors_rate_sec", errNumberValue, td, time.Second) metrics.SetAverage(m, "last_messages_average_latency_msec", msgLatenciesValue, msgNumberValue, metrics.MilliPerNano, metrics.DefaultAverageLatencyJSONValue) metrics.SetAverage(m, "last_errors_average_latency_msec", errLatenciesValue, errNumberValue, metrics.MilliPerNano, metrics.DefaultAverageLatencyJSONValue) } func resetIntervalMetrics(m metrics.Map, t time.Time) { m.Set("current_interval_start", metrics.NewTime(t)) metrics.AddToMaps(currentTotalMessagesLatenciesKey, 0, m) metrics.AddToMaps(currentTotalMessagesKey, 0, m) metrics.AddToMaps(currentTotalErrorsLatenciesKey, 0, m) metrics.AddToMaps(currentTotalErrorsKey, 0, m) } ================================================ FILE: server/store/dummystore/dummy_message_store.go ================================================ package dummystore import ( "fmt" "strconv" "sync" "time" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/store" ) const topicSchema = "topic_sequence" // DummyMessageStore is a minimal implementation of the MessageStore interface. // Everything it does is storing the message ids in the key value store to // ensure a monotonic incremented id. // It is intended for testing and demo purpose, as well as dummy for services without persistence. // TODO: implement a simple logic to preserve the last N messages type DummyMessageStore struct { topicSequences map[string]uint64 topicSequencesLock sync.RWMutex kvStore kvstore.KVStore isSyncStarted bool stopC chan bool // used to send the stop request to the syc goroutine stoppedC chan bool // answer from the syc goroutine, when it is stopped idSyncDuration time.Duration } // New returns a new DummyMessageStore. func New(kvStore kvstore.KVStore) *DummyMessageStore { return &DummyMessageStore{ topicSequences: make(map[string]uint64), kvStore: kvStore, idSyncDuration: time.Millisecond * 100, stopC: make(chan bool, 1), stoppedC: make(chan bool, 1), } } // Start the DummyMessageStore. func (dms *DummyMessageStore) Start() error { go dms.startSequenceSync() dms.isSyncStarted = true return nil } // Stop the DummyMessageStore. func (dms *DummyMessageStore) Stop() error { if !dms.isSyncStarted { return nil } dms.stopC <- true <-dms.stoppedC return nil } // StoreMessage is a part of the `store.MessageStore` implementation. func (dms *DummyMessageStore) StoreMessage(message *protocol.Message, nodeID uint8) (int, error) { partitionName := message.Path.Partition() nextID, ts, err := dms.GenerateNextMsgID(partitionName, 0) if err != nil { return 0, err } message.ID = nextID message.Time = ts message.NodeID = nodeID data := message.Bytes() if err := dms.Store(partitionName, nextID, data); err != nil { return 0, err } return len(data), nil } // Store is a part of the `store.MessageStore` implementation. func (dms *DummyMessageStore) Store(partition string, msgID uint64, msg []byte) error { dms.topicSequencesLock.Lock() defer dms.topicSequencesLock.Unlock() return dms.store(partition, msgID, msg) } func (dms *DummyMessageStore) store(partition string, msgId uint64, msg []byte) error { maxID, err := dms.maxMessageID(partition) if err != nil { return err } if msgId > 1+maxID { return fmt.Errorf("DummyMessageStore: Invalid message id for partition %v. Next id should be %v, but was %q", partition, 1+maxID, msgId) } dms.setID(partition, msgId) return nil } // Fetch does nothing in this dummy implementation. // It is a part of the `store.MessageStore` implementation. func (dms *DummyMessageStore) Fetch(req *store.FetchRequest) { } // MaxMessageID is a part of the `store.MessageStore` implementation. func (dms *DummyMessageStore) MaxMessageID(partition string) (uint64, error) { dms.topicSequencesLock.Lock() defer dms.topicSequencesLock.Unlock() return dms.maxMessageID(partition) } // DoInTx is a part of the `store.MessageStore` implementation. func (dms *DummyMessageStore) DoInTx(partition string, fnToExecute func(maxMessageId uint64) error) error { dms.topicSequencesLock.Lock() defer dms.topicSequencesLock.Unlock() maxID, err := dms.maxMessageID(partition) if err != nil { return err } return fnToExecute(maxID) } // GenerateNextMsgID is a part of the `store.MessageStore` implementation. func (dms *DummyMessageStore) GenerateNextMsgID(partitionName string, nodeID uint8) (uint64, int64, error) { dms.topicSequencesLock.Lock() defer dms.topicSequencesLock.Unlock() ts := time.Now().Unix() max, err := dms.maxMessageID(partitionName) if err != nil { return 0, 0, err } next := max + 1 dms.setID(partitionName, next) return next, ts, nil } func (dms *DummyMessageStore) maxMessageID(partition string) (uint64, error) { sequenceValue, exist := dms.topicSequences[partition] if !exist { val, existInKVStore, err := dms.kvStore.Get(topicSchema, partition) if err != nil { return 0, err } if existInKVStore { sequenceValue, err = strconv.ParseUint(string(val), 10, 0) if err != nil { return 0, err } } else { sequenceValue = uint64(0) } } dms.topicSequences[partition] = sequenceValue return sequenceValue, nil } // the id to a new value func (dms *DummyMessageStore) setID(partition string, id uint64) { dms.topicSequences[partition] = id } func (dms *DummyMessageStore) startSequenceSync() { lastSyncValues := make(map[string]uint64) topicsToUpdate := []string{} shouldStop := false for !shouldStop { select { case <-time.After(dms.idSyncDuration): case <-dms.stopC: shouldStop = true } dms.topicSequencesLock.Lock() topicsToUpdate = topicsToUpdate[:0] for topic, seq := range dms.topicSequences { if lastSyncValues[topic] != seq { topicsToUpdate = append(topicsToUpdate, topic) } } dms.topicSequencesLock.Unlock() for _, topic := range topicsToUpdate { dms.topicSequencesLock.Lock() latestValue := dms.topicSequences[topic] dms.topicSequencesLock.Unlock() lastSyncValues[topic] = latestValue dms.kvStore.Put(topicSchema, topic, []byte(strconv.FormatUint(latestValue, 10))) } } dms.stoppedC <- true } func (dms *DummyMessageStore) Check() error { return nil } func (dms *DummyMessageStore) Partition(name string) (store.MessagePartition, error) { return nil, nil } func (dms *DummyMessageStore) Partitions() ([]store.MessagePartition, error) { return nil, nil } ================================================ FILE: server/store/dummystore/dummy_message_store_test.go ================================================ package dummystore import ( "strconv" "testing" "time" "github.com/smancke/guble/server/kvstore" "github.com/stretchr/testify/assert" ) func Test_DummyMessageStore_IncreaseOnStore(t *testing.T) { a := assert.New(t) dms := New(kvstore.NewMemoryKVStore()) a.Equal(uint64(0), fne(dms.MaxMessageID("partition"))) a.NoError(dms.Store("partition", 1, []byte{})) a.NoError(dms.Store("partition", 2, []byte{})) a.Equal(uint64(2), fne(dms.MaxMessageID("partition"))) } func Test_DummyMessageStore_ErrorOnWrongMessageId(t *testing.T) { a := assert.New(t) store := New(kvstore.NewMemoryKVStore()) a.Equal(uint64(0), fne(store.MaxMessageID("partition"))) a.Error(store.Store("partition", 42, []byte{})) } func Test_DummyMessageStore_InitIdsFromKvStore(t *testing.T) { a := assert.New(t) // given: a kv-store with some values, and a dummy-message-store based on it kvStore := kvstore.NewMemoryKVStore() kvStore.Put(topicSchema, "partition1", []byte("42")) kvStore.Put(topicSchema, "partition2", []byte("21")) dms := New(kvStore) // then a.Equal(uint64(42), fne(dms.MaxMessageID("partition1"))) a.Equal(uint64(21), fne(dms.MaxMessageID("partition2"))) } func Test_DummyMessageStore_SyncIds(t *testing.T) { a := assert.New(t) // given: a store which syncs every 1ms kvStore := kvstore.NewMemoryKVStore() dms := New(kvStore) dms.idSyncDuration = time.Millisecond a.Equal(uint64(0), fne(dms.MaxMessageID("partition"))) _, exist, _ := kvStore.Get(topicSchema, "partition") a.False(exist) // and is started dms.Start() defer dms.Stop() // when: we set an id and wait longer than 1ms // lock&unlock mutex here, because normal invocation of setId() in the code is done while already protected by mutex dms.topicSequencesLock.Lock() dms.setID("partition", uint64(42)) dms.topicSequencesLock.Unlock() time.Sleep(time.Millisecond * 4) // the value is synced to the kv store value, exist, _ := kvStore.Get(topicSchema, "partition") a.True(exist) a.Equal([]byte(strconv.FormatUint(uint64(42), 10)), value) } func Test_DummyMessageStore_SyncIdsOnStop(t *testing.T) { a := assert.New(t) // given: a store which syncs nearly never kvStore := kvstore.NewMemoryKVStore() dms := New(kvStore) dms.idSyncDuration = time.Hour // and is started dms.Start() // when: we set an id dms.topicSequencesLock.Lock() dms.setID("partition", uint64(42)) dms.topicSequencesLock.Unlock() // then it is not synced after some wait time.Sleep(time.Millisecond * 2) _, exist, _ := kvStore.Get(topicSchema, "partition") a.False(exist) // but // when: we stop the store dms.Stop() // then: the the value is synced to the kv store value, exist, _ := kvStore.Get(topicSchema, "partition") a.True(exist) a.Equal([]byte(strconv.FormatUint(uint64(42), 10)), value) } func fne(args ...interface{}) interface{} { if args[1] != nil { panic(args[1]) } return args[0] } ================================================ FILE: server/store/fetch_request.go ================================================ package store import ( "errors" "math" "sync" ) var ErrRequestDone = errors.New("Fetch request is done") const ( DirectionOneMessage FetchDirection = 0 DirectionForward FetchDirection = 1 DirectionBackwards FetchDirection = -1 // TODO Bogdan decide the channel size and if should be customizable FetchBufferSize = 10 ) type FetchDirection int // FetchedMessage is a struct containing a pair: guble Message and its ID. type FetchedMessage struct { ID uint64 Message []byte } // FetchRequest is used for fetching messages in a MessageStore. type FetchRequest struct { sync.RWMutex // Partition is the Store name to search for messages Partition string // StartID is the message sequence id to start StartID uint64 // EndID is the message sequence id to finish. If will not be used. EndID uint64 // Direction has 3 possible values: // Direction == 0: Only the Message with StartId // Direction == 1: Fetch also the next Count Messages with a higher MessageId // Direction == -1: Fetch also the next Count Messages with a lower MessageId Direction FetchDirection // Count is the maximum number of messages to return Count int // MessageC is the channel to send the message back to the receiver MessageC chan *FetchedMessage // ErrorC is a channel if an error occurs ErrorC chan error // StartC Through this channel , the total number or result // is returned, before sending the first message. // The Fetch() methods blocks on putting the number to the start channel. StartC chan int done bool } // NewFetchRequest creates a new FetchRequest pointer initialized with provided values // if `count` is negative will be set to MaxInt32 func NewFetchRequest(partition string, start, end uint64, direction FetchDirection, count int) *FetchRequest { if count < 0 { count = math.MaxInt32 } return &FetchRequest{ Partition: partition, StartID: start, EndID: end, Direction: direction, Count: count, } } func (fr *FetchRequest) Init() { fr.Lock() defer fr.Unlock() fr.done = false fr.StartC = make(chan int) fr.MessageC = make(chan *FetchedMessage, FetchBufferSize) fr.ErrorC = make(chan error) } // Ready returns the count of messages that will be returned meaning that // the fetch is starting. It reads the number from the StartC channel. func (fr *FetchRequest) Ready() int { return <-fr.StartC } func (fr *FetchRequest) Messages() <-chan *FetchedMessage { return fr.MessageC } func (fr *FetchRequest) Errors() <-chan error { return fr.ErrorC } func (fr *FetchRequest) Error(err error) { fr.ErrorC <- err } func (fr *FetchRequest) Push(id uint64, message []byte) { fr.PushFetchMessage(&FetchedMessage{id, message}) } func (fr *FetchRequest) PushFetchMessage(fm *FetchedMessage) { fr.MessageC <- fm } func (fr *FetchRequest) PushError(err error) { fr.ErrorC <- err } func (fr *FetchRequest) IsDone() bool { fr.RLock() defer fr.RUnlock() return fr.done } func (fr *FetchRequest) Done() { fr.Lock() defer fr.Unlock() fr.done = true close(fr.MessageC) } ================================================ FILE: server/store/filestore/cache.go ================================================ package filestore import ( "sync" "github.com/smancke/guble/server/store" ) type cache struct { entries []*cacheEntry sync.RWMutex } func newCache() *cache { c := &cache{ entries: make([]*cacheEntry, 0), } return c } func (c *cache) length() int { c.RLock() defer c.RUnlock() return len(c.entries) } func (c *cache) add(entry *cacheEntry) { c.Lock() defer c.Unlock() c.entries = append(c.entries, entry) } type cacheEntry struct { min, max uint64 } // Contains returns true if the req.StartID is between the min and max // There is a chance the request messages to be found in this range func (entry *cacheEntry) Contains(req *store.FetchRequest) bool { if req.StartID == 0 { req.Direction = 1 return true } if req.Direction >= 0 { return req.StartID >= entry.min && req.StartID <= entry.max } return req.StartID >= entry.min } ================================================ FILE: server/store/filestore/index_list.go ================================================ package filestore import ( "fmt" "sync" log "github.com/Sirupsen/logrus" "github.com/smancke/guble/server/store" ) // IndexList a sorted list of fetch entries type indexList struct { items []*index sync.RWMutex } func newIndexList(size int) *indexList { return &indexList{items: make([]*index, 0, size)} } func (l *indexList) len() int { l.RLock() defer l.RUnlock() return len(l.items) } func (l *indexList) insertList(other *indexList) { l.insert(other.toSliceArray()...) } //Insert adds in the sorted list a new element func (l *indexList) insert(items ...*index) { for _, elem := range items { l.insertElem(elem) } } func (l *indexList) insertElem(elem *index) { l.Lock() defer l.Unlock() // first element on list just append at the end if len(l.items) == 0 { l.items = append(l.items, elem) return } // if the first element in list have a bigger id...insert new element on the start of list if l.items[0].id >= elem.id { l.items = append([]*index{elem}, l.items...) return } if l.items[len(l.items)-1].id <= elem.id { l.items = append(l.items, elem) return } //found the correct position to make an insertion sort for i := 1; i <= len(l.items)-1; i++ { if l.items[i].id > elem.id { l.items = append(l.items[:i], append([]*index{elem}, l.items[i:]...)...) return } } } // Clear empties the current list func (l *indexList) clear() { l.items = make([]*index, 0) } // GetIndexEntryFromID performs a binarySearch retrieving the // true, the position and list and the actual entry if found // false , -1 ,nil if position is not found // search performs a binary search returning: // - `true` in case the item was found // - `position` position of the item // - `bestIndex` the closest index to the searched item if not found. // - `index` the index if found func (l *indexList) search(searchID uint64) (bool, int, int, *index) { l.RLock() defer l.RUnlock() if len(l.items) == 0 { return false, -1, -1, nil } h := len(l.items) - 1 f := 0 bestIndex := f for f <= h { mid := (h + f) / 2 if l.items[mid].id == searchID { return true, mid, bestIndex, l.items[mid] } else if l.items[mid].id < searchID { f = mid + 1 } else { h = mid - 1 } if abs(l.items[mid].id, searchID) <= abs(l.items[bestIndex].id, searchID) { bestIndex = mid } } return false, -1, bestIndex, nil } //Back retrieves the element with the biggest id or nil if list is empty func (l *indexList) back() *index { l.RLock() defer l.RUnlock() if len(l.items) == 0 { return nil } return l.items[len(l.items)-1] } //Front retrieves the element with the smallest id or nil if list is empty func (l *indexList) front() *index { l.RLock() defer l.RUnlock() if len(l.items) == 0 { return nil } return l.items[0] } func (l *indexList) toSliceArray() []*index { l.RLock() defer l.RUnlock() return l.items } //Front retrieves the element at the given index or nil if position is incorrect or list is empty func (l *indexList) get(pos int) *index { l.RLock() defer l.RUnlock() if len(l.items) == 0 || pos < 0 || pos >= len(l.items) { logger.WithFields(log.Fields{ "len": len(l.items), "pos": pos, }).Info("Empty list or invalid index") return nil } return l.items[pos] } func (l *indexList) mapWithPredicate(predicate func(elem *index, i int) error) error { l.RLock() defer l.RUnlock() for i, elem := range l.items { if err := predicate(elem, i); err != nil { return err } } return nil } func (l *indexList) String() string { l.RLock() defer l.RUnlock() s := "" for i, elem := range l.items { s += fmt.Sprintf("[%d:%d %d] ", i, elem.id, elem.fileID) } return s } // Contains returns true if given ID is between first and last item in the list func (l *indexList) contains(id uint64) bool { l.RLock() defer l.RUnlock() if len(l.items) == 0 { return false } if id == 0 { return true } return l.items[0].id <= id && id <= l.items[len(l.items)-1].id } // Extract will return a new list containing items requested by the FetchRequest from this list func (l *indexList) extract(req *store.FetchRequest) *indexList { potentialEntries := newIndexList(0) found, pos, lastPos, _ := l.search(req.StartID) currentPos := lastPos if found { currentPos = pos } for potentialEntries.len() < req.Count && currentPos >= 0 && currentPos < l.len() { elem := l.get(currentPos) logger.WithFields(log.Fields{ "elem": *elem, "currentPos": currentPos, "req": *req, }).Debug("Elem in retrieve") if elem == nil { logger.WithFields(log.Fields{ "pos": currentPos, "l.Len": l.len(), "len": potentialEntries.len(), "startID": req.StartID, "count": req.Count, }).Error("Error in retrieving from list.Got nil entry") break } potentialEntries.insert(elem) currentPos += int(req.Direction) // // if we reach req.EndID than we break if req.EndID > 0 && elem.id >= req.EndID { break } } return potentialEntries } func abs(m1, m2 uint64) uint64 { if m1 > m2 { return m1 - m2 } return m2 - m1 } ================================================ FILE: server/store/filestore/index_list_test.go ================================================ package filestore import ( "math/rand" "testing" "github.com/Sirupsen/logrus" "github.com/stretchr/testify/assert" ) func Test_SortedListSanity(t *testing.T) { a := assert.New(t) list := newIndexList(1000) generatedIds := make([]uint64, 0, 11) for i := 0; i < 11; i++ { msgID := uint64(rand.Intn(50)) generatedIds = append(generatedIds, msgID) entry := &index{ size: 3, id: uint64(msgID), offset: 128, } list.insert(entry) } min := uint64(200) max := uint64(0) for _, id := range generatedIds { if max < id { max = id } if min > id { min = id } found, pos, _, foundEntry := list.search(id) a.True(found) a.Equal(foundEntry.id, id) a.True(pos >= 0 && pos <= len(generatedIds)) } logrus.WithField("generatedIds", generatedIds).Info("IdS") a.Equal(min, list.front().id) a.Equal(max, list.back().id) found, pos, bestIndex, foundEntry := list.search(uint64(46)) a.False(found, "Element should not be found since is a number greater than the random generated upper limit") a.Equal(pos, -1) a.Nil(foundEntry) logrus.WithField("bestIndex", bestIndex).Info("Searching for closest position") a.Equal(list.front().id, list.get(0).id, "First element should contain the smallest element") a.Nil(list.get(-1), "Trying to get an invalid index will return nil") list.clear() a.Nil(list.front()) a.Nil(list.back()) } ================================================ FILE: server/store/filestore/logger.go ================================================ package filestore import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithField("module", "filestore") ================================================ FILE: server/store/filestore/message_partition.go ================================================ package filestore import ( "encoding/binary" "fmt" "io/ioutil" "os" "path/filepath" "strings" "sync" "time" "github.com/smancke/guble/server/store" "io" log "github.com/Sirupsen/logrus" ) var ( magicNumber = []byte{42, 249, 180, 108, 82, 75, 222, 182} fileFormatVersion = []byte{1} messagesPerFile = uint64(10000) indexEntrySize = 20 ) const ( gubleNodeIdBits = 3 sequenceBits = 12 gubleNodeIdShift = sequenceBits timestampLeftShift = sequenceBits + gubleNodeIdBits gubleEpoch = 1467714505012 ) type index struct { id uint64 offset uint64 size uint32 fileID int } type messagePartition struct { basedir string name string appendFile *os.File indexFile *os.File appendFilePosition uint64 maxMessageID uint64 sequenceNumber uint64 totalNumberOfMessages uint64 entriesCount uint64 list *indexList fileCache *cache sync.RWMutex } func newMessagePartition(basedir string, storeName string) (*messagePartition, error) { p := &messagePartition{ basedir: basedir, name: storeName, list: newIndexList(int(messagesPerFile)), fileCache: newCache(), } return p, p.initialize() } func (p *messagePartition) Name() string { return p.name } func (p *messagePartition) MaxMessageID() uint64 { p.RLock() defer p.RUnlock() return p.maxMessageID } func (p *messagePartition) Count() uint64 { p.RLock() defer p.RUnlock() return p.totalNumberOfMessages } func (p *messagePartition) initialize() error { p.Lock() defer p.Unlock() // reset the cache entries p.fileCache = newCache() err := p.readIdxFiles() if err != nil { logger.WithField("err", err).Error("MessagePartition error on scanFiles") return err } return nil } // Returns the start messages ids for all available message files // in a sorted list func (p *messagePartition) readIdxFiles() error { allFiles, err := ioutil.ReadDir(p.basedir) if err != nil { return err } var indexFilenames []string for _, fileInfo := range allFiles { if strings.HasPrefix(fileInfo.Name(), p.name+"-") && strings.HasSuffix(fileInfo.Name(), ".idx") { fileIDString := filepath.Join(p.basedir, fileInfo.Name()) logger.WithField("name", fileIDString).Info("Index name") indexFilenames = append(indexFilenames, fileIDString) } } // if no .idx file are found.. there is nothing to load if len(indexFilenames) == 0 { logger.Info("No .idx files found") return nil } //load the filecache from all the files logger.WithFields(log.Fields{ "filenames": indexFilenames, "totalFiles": len(indexFilenames), }).Info("Found files") for i := 0; i < len(indexFilenames)-1; i++ { cEntry, err := readCacheEntryFromIdxFile(indexFilenames[i]) if err != nil { logger.WithFields(log.Fields{ "idxFilename": indexFilenames[i], "err": err, }).Error("Error loading existing .idxFile") return err } //add to total number of messages per partition p.totalNumberOfMessages += messagesPerFile // put entry in file cache p.fileCache.add(cEntry) logger. WithField("entries", p.fileCache.entries). WithField("filename", indexFilenames[i]). Error("Entries") // check the message id's for max value if cEntry.max >= p.maxMessageID { p.maxMessageID = cEntry.max } } // read the idx file with biggest id and load in the sorted cache if err := p.loadLastIndexList(indexFilenames[len(indexFilenames)-1]); err != nil { logger.WithFields(log.Fields{ "idxFilename": indexFilenames[(len(indexFilenames) - 1)], "err": err, }).Error("Error loading last .idx file") return err } //add the last part p.totalNumberOfMessages += uint64(p.list.len()) back := p.list.back() if back != nil && back.id >= p.maxMessageID { p.maxMessageID = back.id } return nil } func (p *messagePartition) closeAppendFiles() error { if p.appendFile != nil { if err := p.appendFile.Close(); err != nil { if p.indexFile != nil { defer p.indexFile.Close() } return err } p.appendFile = nil } if p.indexFile != nil { err := p.indexFile.Close() p.indexFile = nil return err } return nil } // readCacheEntryFromIdxFile reads the first and last entry from a idx file which should be sorted func readCacheEntryFromIdxFile(filename string) (entry *cacheEntry, err error) { entriesInIndex, err := calculateNoEntries(filename) if err != nil { return } file, err := os.Open(filename) if err != nil { return } defer file.Close() min, _, _, err := readIndexEntry(file, 0) if err != nil { return } max, _, _, err := readIndexEntry(file, int64((entriesInIndex-1)*uint64(indexEntrySize))) if err != nil { return } entry = &cacheEntry{min, max} return } func (p *messagePartition) createNextAppendFiles() error { filename := p.composeMsgFilenameForPosition(uint64(p.fileCache.length())) logger.WithField("filename", filename).Info("Creating next append files") appendfile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { return err } // write file header on new files if stat, _ := appendfile.Stat(); stat.Size() == 0 { p.appendFilePosition = uint64(stat.Size()) _, err = appendfile.Write(magicNumber) if err != nil { return err } _, err = appendfile.Write(fileFormatVersion) if err != nil { return err } } indexfile, errIndex := os.OpenFile(p.composeIdxFilenameForPosition(uint64(p.fileCache.length())), os.O_RDWR|os.O_CREATE, 0666) if errIndex != nil { defer appendfile.Close() defer os.Remove(appendfile.Name()) return err } p.appendFile = appendfile p.indexFile = indexfile stat, err := appendfile.Stat() if err != nil { return err } p.appendFilePosition = uint64(stat.Size()) return nil } func (p *messagePartition) generateNextMsgID(nodeID uint8) (uint64, int64, error) { p.Lock() defer p.Unlock() //Get the local Timestamp currTime := time.Now() // timestamp in Seconds will be return to client timestamp := currTime.Unix() //Use the unixNanoTimestamp for generating id nanoTimestamp := currTime.UnixNano() if nanoTimestamp < gubleEpoch { err := fmt.Errorf("Clock is moving backwards. Rejecting requests until %d.", timestamp) return 0, 0, err } id := (uint64(nanoTimestamp-gubleEpoch) << timestampLeftShift) | (uint64(nodeID) << gubleNodeIdShift) | p.sequenceNumber p.sequenceNumber++ logger.WithFields(log.Fields{ "id": id, "messagePartition": p.basedir, "localSequenceNumber": p.sequenceNumber, "currentNode": nodeID, }).Debug("Generated id") return id, timestamp, nil } func (p *messagePartition) Close() error { p.Lock() defer p.Unlock() return p.closeAppendFiles() } func (p *messagePartition) DoInTx(fnToExecute func(maxMessageId uint64) error) error { p.Lock() defer p.Unlock() return fnToExecute(p.maxMessageID) } func (p *messagePartition) Store(msgID uint64, msg []byte) error { p.Lock() defer p.Unlock() return p.store(msgID, msg) } func (p *messagePartition) store(messageID uint64, data []byte) error { if p.entriesCount == messagesPerFile || p.appendFile == nil || p.indexFile == nil { logger.WithFields(log.Fields{ "msgId": messageID, "entriesCount": p.entriesCount, "fileCache": p.fileCache, }).Debug("store") if err := p.closeAppendFiles(); err != nil { return err } if p.entriesCount == messagesPerFile { logger.WithFields(log.Fields{ "msgId": messageID, "entriesCount": p.entriesCount, }).Info("Dumping current file") //sort the indexFile err := p.rewriteSortedIdxFile(p.composeIdxFilenameForPosition(uint64(p.fileCache.length()))) if err != nil { logger.WithError(err).Error("Error dumping file") return err } //Add items in the filecache p.fileCache.add(&cacheEntry{ min: p.list.front().id, max: p.list.back().id, }) //clear the current sorted cache p.list.clear() p.entriesCount = 0 } if err := p.createNextAppendFiles(); err != nil { return err } } // write the message size and the message id: 32 bit and 64 bit, so 12 bytes sizeAndID := make([]byte, 12) binary.LittleEndian.PutUint32(sizeAndID, uint32(len(data))) binary.LittleEndian.PutUint64(sizeAndID[4:], messageID) if _, err := p.appendFile.Write(sizeAndID); err != nil { return err } // write the message if _, err := p.appendFile.Write(data); err != nil { return err } // write the index entry to the index file messageOffset := p.appendFilePosition + uint64(len(sizeAndID)) err := writeIndexEntry(p.indexFile, messageID, messageOffset, uint32(len(data)), p.entriesCount) if err != nil { return err } p.entriesCount++ p.totalNumberOfMessages++ logger.WithFields(log.Fields{ "p.noOfEntriesInIndexFile": p.entriesCount, "msgID": messageID, "msgSize": uint32(len(data)), "msgOffset": messageOffset, "filename": p.indexFile.Name(), }).Debug("Wrote in indexFile") //create entry for l e := &index{ id: messageID, offset: messageOffset, size: uint32(len(data)), fileID: p.fileCache.length(), } p.list.insert(e) p.appendFilePosition += uint64(len(sizeAndID) + len(data)) if messageID > p.maxMessageID { p.maxMessageID = messageID } return nil } // Fetch fetches a set of messages func (p *messagePartition) Fetch(req *store.FetchRequest) { le := logger.WithFields(log.Fields{ "partition": req.Partition, "startID": req.StartID, "endID": req.EndID, "Count": req.Count, }) le.Debug("Fetching") go func() { fetchList, err := p.calculateFetchList(req) if err != nil { log.WithField("err", err).Error("Error calculating list") req.ErrorC <- err return } req.StartC <- fetchList.len() err = p.fetchByFetchlist(fetchList, req) if err != nil { le.WithField("err", err).Error("Error calculating list") req.Error(err) return } req.Done() }() } // fetchByFetchlist fetches the messages in the supplied fetchlist and sends them to the message-channel func (p *messagePartition) fetchByFetchlist(fetchList *indexList, req *store.FetchRequest) error { return fetchList.mapWithPredicate(func(index *index, _ int) error { if req.IsDone() { return store.ErrRequestDone } filename := p.composeMsgFilenameForPosition(uint64(index.fileID)) file, err := os.Open(filename) if err != nil { return err } defer file.Close() msg := make([]byte, index.size, index.size) _, err = file.ReadAt(msg, int64(index.offset)) if err != nil { logger.WithFields(log.Fields{ "err": err, "offset": index.offset, }).Error("Error ReadAt") return err } req.Push(index.id, msg) return nil }) } // calculateFetchList returns a list of fetchEntry records for all messages in the fetch request. func (p *messagePartition) calculateFetchList(req *store.FetchRequest) (*indexList, error) { if req.Direction == 0 { req.Direction = 1 } potentialEntries := newIndexList(0) // reading from IndexFiles // TODO: fix prev when EndID logic will be done // prev specifies if we found anything in the previous list, in which case // it is possible the items to continue in the next list prev := false p.fileCache.RLock() for i, fce := range p.fileCache.entries { if fce.Contains(req) || (prev && potentialEntries.len() < req.Count) { prev = true l, err := p.loadIndexList(i) if err != nil { logger.WithError(err).Info("Error loading idx file in memory") return nil, err } potentialEntries.insert(l.extract(req).toSliceArray()...) } else { prev = false } } // Read from current cached value (the idx file which size is smaller than MESSAGE_PER_FILE if p.list.contains(req.StartID) || (prev && potentialEntries.len() < req.Count) { potentialEntries.insert(p.list.extract(req).toSliceArray()...) } // Currently potentialEntries contains a potentials IDs from any files and // from in memory. From this will select only Count. fetchList := potentialEntries.extract(req) p.fileCache.RUnlock() return fetchList, nil } func (p *messagePartition) rewriteSortedIdxFile(filename string) error { logger.WithFields(log.Fields{ "filename": filename, }).Info("Dumping Sorted list") file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666) if err != nil { return err } defer file.Close() lastID := uint64(0) for i := 0; i < p.list.len(); i++ { item := p.list.get(i) if lastID >= item.id { logger.WithFields(log.Fields{ "err": err, "filename": filename, }).Error("Sorted list is not sorted") return err } lastID = item.id err := writeIndexEntry(file, item.id, item.offset, item.size, uint64(i)) logger.WithFields(log.Fields{ "curMsgId": item.id, "err": err, "pos": i, "filename": file.Name(), }).Debug("Wrote while dumpSortedIndexFile") if err != nil { logger.WithField("err", err).Error("Error writing indexfile in sorted way.") return err } } return nil } // readIndexEntry reads from a .idx file from the given `position` the msgID msgOffset and msgSize func readIndexEntry(file *os.File, position int64) (uint64, uint64, uint32, error) { offsetBuffer := make([]byte, indexEntrySize) if _, err := file.ReadAt(offsetBuffer, position); err != nil { logger.WithFields(log.Fields{ "err": err, "file": file.Name(), "indexPos": position, }).Error("Error reading index entry") return 0, 0, 0, err } id := binary.LittleEndian.Uint64(offsetBuffer) offset := binary.LittleEndian.Uint64(offsetBuffer[8:]) size := binary.LittleEndian.Uint32(offsetBuffer[16:]) return id, offset, size, nil } // writeIndexEntry write in a .idx file to the given `pos` the msgIDm msgOffset and msgSize func writeIndexEntry(w io.WriterAt, id uint64, offset uint64, size uint32, pos uint64) error { position := int64(uint64(indexEntrySize) * pos) offsetBuffer := make([]byte, indexEntrySize) binary.LittleEndian.PutUint64(offsetBuffer, id) binary.LittleEndian.PutUint64(offsetBuffer[8:], offset) binary.LittleEndian.PutUint32(offsetBuffer[16:], size) if _, err := w.WriteAt(offsetBuffer, position); err != nil { logger.WithFields(log.Fields{ "err": err, "position": position, "id": id, }).Error("Error writing index entry") return err } return nil } // calculateNoEntries reads the idx file with name `filename` and will calculate how many entries are func calculateNoEntries(filename string) (uint64, error) { stat, err := os.Stat(filename) if err != nil { logger.WithField("err", err).Error("Stat failed") return 0, err } entriesInIndex := uint64(stat.Size() / int64(indexEntrySize)) return entriesInIndex, nil } // loadLastIndexFile will construct the current Sorted List for fetch entries which corresponds to the idx file with the biggest name func (p *messagePartition) loadLastIndexList(filename string) error { logger.WithField("filename", filename).Info("Loading last index file") l, err := p.loadIndexList(p.fileCache.length()) if err != nil { logger.WithError(err).Error("Error loading last index filename") return err } p.list = l p.entriesCount = uint64(l.len()) return nil } // loadIndexFile will read a file and will return a sorted list for fetchEntries func (p *messagePartition) loadIndexList(fileID int) (*indexList, error) { filename := p.composeIdxFilenameForPosition(uint64(fileID)) l := newIndexList(int(messagesPerFile)) logger.WithField("filename", filename).Debug("loadIndexFile") entriesInIndex, err := calculateNoEntries(filename) if err != nil { return nil, err } file, err := os.Open(filename) if err != nil { logger.WithField("err", err).Error("os.Open failed") return nil, err } defer file.Close() for i := uint64(0); i < entriesInIndex; i++ { id, offset, size, err := readIndexEntry(file, int64(i*uint64(indexEntrySize))) logger.WithFields(log.Fields{ "offset": offset, "size": size, "id": id, "err": err, }).Debug("readIndexEntry") if err != nil { logger.WithField("err", err).Error("Read error") return nil, err } e := &index{ id: id, size: size, offset: offset, fileID: fileID, } l.insert(e) logger.WithField("len", l.len()).Debug("loadIndexFile") } return l, nil } func (p *messagePartition) composeMsgFilenameForPosition(value uint64) string { return filepath.Join(p.basedir, fmt.Sprintf("%s-%020d.msg", p.name, value)) } func (p *messagePartition) composeIdxFilenameForPosition(value uint64) string { return filepath.Join(p.basedir, fmt.Sprintf("%s-%020d.idx", p.name, value)) } ================================================ FILE: server/store/filestore/message_partition_robustness_test.go ================================================ package filestore import ( log "github.com/Sirupsen/logrus" "github.com/smancke/guble/server/store" "github.com/smancke/guble/testutil" "github.com/stretchr/testify/assert" "io/ioutil" "math" "os" "strconv" "testing" "time" ) func Test_MessagePartition_forConcurrentWriteAndReads(t *testing.T) { testutil.SkipIfShort(t) // testutil.PprofDebug() a := assert.New(t) dir, _ := ioutil.TempDir("", "guble_partition_store_test") defer os.RemoveAll(dir) store, _ := newMessagePartition(dir, "myMessages") n := 2000 * 100 nReaders := 7 writerDone := make(chan bool) go messagePartitionWriter(a, store, n, writerDone) readerDone := make(chan bool) for i := 1; i <= nReaders; i++ { go messagePartitionReader("reader"+strconv.Itoa(i), a, store, n, readerDone) } select { case <-writerDone: case <-time.After(time.Second * 30): a.Fail("writer timed out") } timeout := time.After(time.Second * 30) for i := 0; i < nReaders; i++ { select { case <-readerDone: case <-timeout: a.Fail("reader timed out") } } } func messagePartitionWriter(a *assert.Assertions, store *messagePartition, n int, done chan bool) { for i := 1; i <= n; i++ { msg := []byte("Hello " + strconv.Itoa(i)) a.NoError(store.Store(uint64(i), msg)) } done <- true } func messagePartitionReader(name string, a *assert.Assertions, mStore *messagePartition, n int, done chan bool) { lastReadMessage := 0 for lastReadMessage < n { msgC := make(chan *store.FetchedMessage, 10) errorC := make(chan error) log.WithFields(log.Fields{ "module": "testing", "name": name, "lastReadMsg": lastReadMessage + 1, }).Debug("Start fetching") mStore.Fetch(&store.FetchRequest{ Partition: "myMessages", StartID: uint64(lastReadMessage + 1), Direction: 1, Count: math.MaxInt32, MessageC: msgC, ErrorC: errorC, StartC: make(chan int, 1), }) FETCH: for { select { case msgAndID, open := <-msgC: if !open { log.WithFields(log.Fields{ "module": "testing", "name": name, "lastReadMsg": lastReadMessage, }).Debug("Stop fetching") break FETCH } a.Equal(lastReadMessage+1, int(msgAndID.ID), "Reader: "+name) lastReadMessage = int(msgAndID.ID) case err := <-errorC: a.Fail("received error", err.Error()) <-done return } } } log.WithFields(log.Fields{ "module": "testing", "name": name, "lastReadMsg": lastReadMessage, }).Debug("Ready got id") done <- true } ================================================ FILE: server/store/filestore/message_partition_test.go ================================================ package filestore import ( "fmt" "io/ioutil" "os" "path" "testing" "time" "github.com/smancke/guble/server/store" "errors" "github.com/stretchr/testify/assert" ) func TestFileMessageStore_GenerateNextMsgId(t *testing.T) { a := assert.New(t) dir, _ := ioutil.TempDir("", "guble_message_partition_test") defer os.RemoveAll(dir) mStore, err := newMessagePartition(dir, "node1") a.Nil(err) var generatedIDs []uint64 lastID := uint64(0) for i := 0; i < 1000; i++ { id, _, err := mStore.generateNextMsgID(1) generatedIDs = append(generatedIDs, id) a.True(id > lastID, "Ids should be monotonic") lastID = id a.Nil(err) } } func TestFileMessageStore_GenerateNextMsgIdMultipleNodes(t *testing.T) { a := assert.New(t) dir, _ := ioutil.TempDir("", "guble_message_partition_test") defer os.RemoveAll(dir) mStore, err := newMessagePartition(dir, "node1") a.Nil(err) dir2, _ := ioutil.TempDir("", "guble_message_partition_test2") defer os.RemoveAll(dir2) mStore2, err := newMessagePartition(dir2, "node1") a.Nil(err) var generatedIDs []uint64 lastID := uint64(0) for i := 0; i < 1000; i++ { id, _, err := mStore.generateNextMsgID(1) id2, _, err := mStore2.generateNextMsgID(2) a.True(id2 > id, "Ids should be monotonic") generatedIDs = append(generatedIDs, id) generatedIDs = append(generatedIDs, id2) time.Sleep(1 * time.Millisecond) a.True(id > lastID, "Ids should be monotonic") a.True(id2 > lastID, "Ids should be monotonic") lastID = id2 a.Nil(err) } for i := 0; i < len(generatedIDs)-1; i++ { if generatedIDs[i] >= generatedIDs[i+1] { a.FailNow("Not Sorted") } } } func Test_MessagePartition_loadFiles(t *testing.T) { a := assert.New(t) // allow five messages per file messagesPerFile = uint64(5) dir, _ := ioutil.TempDir("", "guble_message_partition_test") defer os.RemoveAll(dir) mStore, _ := newMessagePartition(dir, "myMessages") msgData := []byte("aaaaaaaaaa") // 10 bytes message a.NoError(mStore.Store(uint64(3), msgData)) // stored offset 21, size: 10 a.NoError(mStore.Store(uint64(4), msgData)) // stored offset 21+10+12=43 a.NoError(mStore.Store(uint64(10), msgData)) // stored offset 43+22=65 a.NoError(mStore.Store(uint64(9), msgData)) // stored offset 65+22=87 a.NoError(mStore.Store(uint64(5), msgData)) // stored offset 87+22=109 // here second file will start a.NoError(mStore.Store(uint64(8), msgData)) // stored offset 21 a.NoError(mStore.Store(uint64(15), msgData)) // stored offset 43 a.NoError(mStore.Store(uint64(13), msgData)) // stored offset 65 a.NoError(mStore.Store(uint64(22), msgData)) // stored offset 87 a.NoError(mStore.Store(uint64(23), msgData)) // stored offset 109 // third file a.NoError(mStore.Store(uint64(24), msgData)) // stored offset 21 a.NoError(mStore.Store(uint64(26), msgData)) // stored offset 43 a.NoError(mStore.Store(uint64(30), msgData)) // stored offset 65 a.Equal(uint64(13), mStore.Count()) a.NoError(mStore.Close()) err := mStore.initialize() a.NoError(err) cEntry, err := readCacheEntryFromIdxFile(path.Join(dir, "myMessages-00000000000000000000.idx")) a.Equal(uint64(3), cEntry.min) a.Equal(uint64(10), cEntry.max) a.NoError(err) a.Equal(uint64(26), mStore.Count()) } func Test_MessagePartition_correctIdAfterRestart(t *testing.T) { a := assert.New(t) dir, _ := ioutil.TempDir("", "guble_message_partition_test") defer os.RemoveAll(dir) mStore, _ := newMessagePartition(dir, "myMessages") a.NoError(mStore.Store(uint64(1), []byte("aaaaaaaaaa"))) a.NoError(mStore.Store(uint64(2), []byte("aaaaaaaaaa"))) a.Equal(uint64(2), mStore.MaxMessageID()) a.NoError(mStore.Close()) a.Equal(uint64(2), mStore.Count()) newMStore, err := newMessagePartition(dir, "myMessages") a.NoError(err) a.Equal(uint64(2), newMStore.MaxMessageID()) a.Equal(uint64(2), newMStore.Count()) } func Benchmark_Storing_HelloWorld_Messages(b *testing.B) { a := assert.New(b) dir, _ := ioutil.TempDir("", "guble_message_partition_test") defer os.RemoveAll(dir) mStore, _ := newMessagePartition(dir, "myMessages") b.ResetTimer() for i := 1; i <= b.N; i++ { a.NoError(mStore.Store(uint64(i), []byte("Hello World"))) } a.NoError(mStore.Close()) b.StopTimer() } func Benchmark_Storing_1Kb_Messages(b *testing.B) { a := assert.New(b) dir, _ := ioutil.TempDir("", "guble_message_partition_test") defer os.RemoveAll(dir) mStore, _ := newMessagePartition(dir, "myMessages") message := make([]byte, 1024) for i := range message { message[i] = 'a' } b.ResetTimer() for i := 1; i <= b.N; i++ { a.NoError(mStore.Store(uint64(i), message)) } a.NoError(mStore.Close()) b.StopTimer() } func Benchmark_Storing_1MB_Messages(b *testing.B) { a := assert.New(b) dir, _ := ioutil.TempDir("", "guble_message_partition_test") defer os.RemoveAll(dir) mStore, _ := newMessagePartition(dir, "myMessages") message := make([]byte, 1024*1024) for i := range message { message[i] = 'a' } b.ResetTimer() for i := 1; i <= b.N; i++ { a.NoError(mStore.Store(uint64(i), message)) } a.NoError(mStore.Close()) b.StopTimer() } func Test_calculateFetchList(t *testing.T) { // allow five messages per file messagesPerFile = uint64(5) msgData := []byte("aaaaaaaaaa") // 10 bytes message a := assert.New(t) dir, _ := ioutil.TempDir("", "guble_message_partition_test") defer os.RemoveAll(dir) mStore, _ := newMessagePartition(dir, "myMessages") // File header: MAGIC_NUMBER + FILE_NUMBER_VERSION = 9 bytes in the file // For each stored message there is a 12 bytes write that contains the msgID and size a.NoError(mStore.Store(uint64(3), msgData)) // stored offset 21, size: 10 a.NoError(mStore.Store(uint64(4), msgData)) // stored offset 21+10+12=43 a.NoError(mStore.Store(uint64(10), msgData)) // stored offset 43+22=65 a.NoError(mStore.Store(uint64(9), msgData)) // stored offset 65+22=87 a.NoError(mStore.Store(uint64(5), msgData)) // stored offset 87+22=109 // here second file will start a.NoError(mStore.Store(uint64(8), msgData)) // stored offset 21 a.NoError(mStore.Store(uint64(15), msgData)) // stored offset 43 a.NoError(mStore.Store(uint64(13), msgData)) // stored offset 65 a.NoError(mStore.Store(uint64(22), msgData)) // stored offset 87 a.NoError(mStore.Store(uint64(23), msgData)) // stored offset 109 // third file a.NoError(mStore.Store(uint64(24), msgData)) // stored offset 21 a.NoError(mStore.Store(uint64(26), msgData)) // stored offset 43 a.NoError(mStore.Store(uint64(30), msgData)) // stored offset 65 defer a.NoError(mStore.Close()) testCases := []struct { description string req store.FetchRequest expectedResults indexList }{ {`direct match`, store.FetchRequest{StartID: 3, Direction: 0, Count: 1}, indexList{ items: []*index{{3, uint64(21), 10, 0}}, // messageId, offset, size, fileId }, }, {`direct match in second file`, store.FetchRequest{StartID: 8, Direction: 0, Count: 1}, indexList{ items: []*index{{8, uint64(21), 10, 1}}, // messageId, offset, size, fileId, }, }, {`direct match in second file, not first position`, store.FetchRequest{StartID: 13, Direction: 0, Count: 1}, indexList{ items: []*index{{13, uint64(65), 10, 1}}, // messageId, offset, size, fileId, }, }, // TODO this is caused by hasStartID() functions.This will be done when implementing the EndID logic // {`next entry matches`, // store.FetchRequest{StartID: 1, Direction: 0, Count: 1}, // SortedIndexList{ // {3, uint64(21), 10, 0}, // messageId, offset, size, fileId // }, // }, {`entry before matches`, store.FetchRequest{StartID: 5, Direction: -1, Count: 2}, indexList{ items: []*index{ {4, uint64(43), 10, 0}, // messageId, offset, size, fileId {5, uint64(109), 10, 0}, // messageId, offset, size, fileId }, }, }, {`backward, no match`, store.FetchRequest{StartID: 1, Direction: -1, Count: 1}, indexList{}, }, {`forward, no match (out of files)`, store.FetchRequest{StartID: 99999999999, Direction: 1, Count: 1}, indexList{}, }, {`forward, no match (after last id in last file)`, store.FetchRequest{StartID: 31, Direction: 1, Count: 1}, indexList{}, }, {`forward, overlapping files`, store.FetchRequest{StartID: 9, Direction: 1, Count: 3}, indexList{ items: []*index{ {9, uint64(87), 10, 0}, // messageId, offset, size, fileId {10, uint64(65), 10, 0}, // messageId, offset, size, fileId {13, uint64(65), 10, 1}, // messageId, offset, size, fileId }, }, }, {`backward, overlapping files`, store.FetchRequest{StartID: 26, Direction: -1, Count: 4}, indexList{ items: []*index{ // {15, uint64(43), 10, 1}, // messageId, offset, size, fileId {22, uint64(87), 10, 1}, // messageId, offset, size, fileId {23, uint64(109), 10, 1}, // messageId, offset, size, fileId {24, uint64(21), 10, 2}, // messageId, offset, size, fileId {26, uint64(43), 10, 2}, // messageId, offset, size, fileId }, }, }, {`forward, over more then 2 files`, store.FetchRequest{StartID: 5, Direction: 1, Count: 10}, indexList{ items: []*index{ {5, uint64(109), 10, 0}, // messageId, offset, size, fileId {8, uint64(21), 10, 1}, // messageId, offset, size, fileId {9, uint64(87), 10, 0}, // messageId, offset, size, fileId {10, uint64(65), 10, 0}, // messageId, offset, size, fileId {13, uint64(65), 10, 1}, // messageId, offset, size, fileId {15, uint64(43), 10, 1}, // messageId, offset, size, fileId {22, uint64(87), 10, 1}, // messageId, offset, size, fileId {23, uint64(109), 10, 1}, // messageId, offset, size, fileId {24, uint64(21), 10, 2}, // messageId, offset, size, fileId {26, uint64(43), 10, 2}, // messageId, offset, size, fileId }, }, }, } for _, testcase := range testCases { testcase.req.Partition = "myMessages" fetchEntries, err := mStore.calculateFetchList(&testcase.req) a.NoError(err, "Tescase: "+testcase.description) a.True(matchSortedList(t, testcase.expectedResults, *fetchEntries), "Tescase: "+testcase.description) } } func matchSortedList(t *testing.T, expected, actual indexList) bool { if !assert.Equal(t, expected.len(), actual.len(), "Invalid length") { return false } err := expected.mapWithPredicate(func(elem *index, i int) error { a := actual.get(i) assert.Equal(t, *elem, *a) if elem.id != a.id || elem.offset != a.offset || elem.size != a.size || elem.fileID != a.fileID { return errors.New("Element not equal!") } return nil }) return assert.NoError(t, err) } func Test_Partition_Fetch(t *testing.T) { a := assert.New(t) // allow five messages per file messagesPerFile = uint64(5) msgData := []byte("1111111111") // 10 bytes message msgData2 := []byte("2222222222") // 10 bytes message msgData3 := []byte("3333333333") // 10 bytes message dir, _ := ioutil.TempDir("", "guble_message_partition_test") defer os.RemoveAll(dir) mStore, _ := newMessagePartition(dir, "myMessages") // File header: MAGIC_NUMBER + FILE_NUMBER_VERSION = 9 bytes in the file // For each stored message there is a 12 bytes write that contains the msgID and size a.NoError(mStore.Store(uint64(3), msgData)) // stored offset 21, size: 10 a.NoError(mStore.Store(uint64(4), msgData)) // stored offset 21+10+12=43 a.NoError(mStore.Store(uint64(10), msgData)) // stored offset 43+22=65 a.NoError(mStore.Store(uint64(9), msgData2)) // stored offset 65+22=87 a.NoError(mStore.Store(uint64(5), msgData3)) // stored offset 87+22=109 // here second file will start a.NoError(mStore.Store(uint64(8), msgData2)) // stored offset 21 a.NoError(mStore.Store(uint64(15), msgData)) // stored offset 43 a.NoError(mStore.Store(uint64(13), msgData3)) // stored offset 65 a.NoError(mStore.Store(uint64(22), msgData)) // stored offset 87 a.NoError(mStore.Store(uint64(23), msgData)) // stored offset 109 // third file a.NoError(mStore.Store(uint64(24), msgData)) // stored offset 21 a.NoError(mStore.Store(uint64(26), msgData)) // stored offset 43 a.NoError(mStore.Store(uint64(30), msgData)) // stored offset 65 defer a.NoError(mStore.Close()) testCases := []struct { description string req store.FetchRequest expectedResults []string }{ {`direct match`, store.FetchRequest{StartID: 3, Direction: 0, Count: 1}, []string{"1111111111"}, }, {`direct match in second file`, store.FetchRequest{StartID: 8, Direction: 0, Count: 1}, []string{"2222222222"}, }, {`next entry matches`, store.FetchRequest{StartID: 13, Direction: 0, Count: 1}, []string{"3333333333"}, }, {`entry before matches`, store.FetchRequest{StartID: 5, Direction: -1, Count: 2}, []string{"1111111111", "3333333333"}, }, {`backward, no match`, store.FetchRequest{StartID: 1, Direction: -1, Count: 1}, []string{}, }, {`forward, no match (out of files)`, store.FetchRequest{StartID: 99999999999, Direction: 1, Count: 1}, []string{}, }, {`forward, no match (after last id in last file)`, store.FetchRequest{StartID: mStore.maxMessageID + uint64(8), Direction: 1, Count: 1}, []string{}, }, {`forward, overlapping files`, store.FetchRequest{StartID: 9, Direction: 1, Count: 3}, []string{"2222222222", "1111111111", "3333333333"}, }, {`forward, over more then 2 files`, store.FetchRequest{StartID: 5, Direction: 1, Count: 10}, []string{"3333333333", "2222222222", "2222222222", "1111111111", "3333333333", "1111111111", "1111111111", "1111111111", "1111111111", "1111111111"}, }, {`backward, overlapping files`, store.FetchRequest{StartID: 26, Direction: -1, Count: 4}, []string{"1111111111", "1111111111", "1111111111", "1111111111"}, }, {`backward, all messages`, store.FetchRequest{StartID: uint64(100), Direction: -1, Count: 100}, []string{"1111111111", "1111111111", "3333333333", "2222222222", "2222222222", "1111111111", "3333333333", "1111111111", "1111111111", "1111111111", "1111111111", "1111111111", "1111111111"}, }, } for _, testcase := range testCases { testcase.req.Partition = "myMessages" testcase.req.MessageC = make(chan *store.FetchedMessage) testcase.req.ErrorC = make(chan error) testcase.req.StartC = make(chan int) messages := []string{} mStore.Fetch(&testcase.req) select { case numberOfResults := <-testcase.req.StartC: a.Equal(len(testcase.expectedResults), numberOfResults) case <-time.After(time.Second): a.Fail("timeout") return } loop: for { select { case msg, open := <-testcase.req.MessageC: if !open { break loop } messages = append(messages, string(msg.Message)) case err := <-testcase.req.ErrorC: a.Fail(err.Error()) break loop case <-time.After(time.Second): a.Fail("timeout") return } } a.Equal(testcase.expectedResults, messages, "Tescase: "+testcase.description) } } func TestFilenameGeneration(t *testing.T) { a := assert.New(t) mStore := &messagePartition{ basedir: "/foo/bar/", name: "myMessages", fileCache: newCache(), } a.Equal("/foo/bar/myMessages-00000000000000000000.msg", mStore.composeMsgFilenameForPosition(uint64(mStore.fileCache.length()))) a.Equal("/foo/bar/myMessages-00000000000000000042.idx", mStore.composeIdxFilenameForPosition(42)) a.Equal("/foo/bar/myMessages-00000000000000000000.idx", mStore.composeIdxFilenameForPosition(0)) a.Equal(fmt.Sprintf("/foo/bar/myMessages-%020d.idx", messagesPerFile), mStore.composeIdxFilenameForPosition(messagesPerFile)) } ================================================ FILE: server/store/filestore/message_store.go ================================================ // Package filestore is a filesystem-based implementation of the MessageStore interface. package filestore import ( "errors" "io/ioutil" "os" "path" "strings" "sync" "syscall" log "github.com/Sirupsen/logrus" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/store" ) // FileMessageStore is a struct used by the filesystem-based implementation of the MessageStore interface. // It holds the base directory, a map of messagePartitions etc. type FileMessageStore struct { partitions map[string]*messagePartition basedir string mutex sync.RWMutex } // New returns a new FileMessageStore. func New(basedir string) *FileMessageStore { return &FileMessageStore{ partitions: make(map[string]*messagePartition), basedir: basedir, } } // MaxMessageID is a part of the `store.MessageStore` implementation. func (fms *FileMessageStore) MaxMessageID(partition string) (uint64, error) { p, err := fms.Partition(partition) if err != nil { return 0, err } return p.MaxMessageID(), nil } // Stop the FileMessageStore. // Implements the service.stopable interface. func (fms *FileMessageStore) Stop() error { fms.mutex.Lock() defer fms.mutex.Unlock() logger.Info("Stopping") var returnError error for key, partition := range fms.partitions { if err := partition.Close(); err != nil { returnError = err logger.WithFields(log.Fields{ "key": key, "err": err, }).Error("Error on closing message store partition") } delete(fms.partitions, key) } return returnError } // GenerateNextMsgID is a part of the `store.MessageStore` implementation. func (fms *FileMessageStore) GenerateNextMsgID(partitionName string, nodeID uint8) (uint64, int64, error) { p, err := fms.Partition(partitionName) if err != nil { return 0, 0, err } return p.(*messagePartition).generateNextMsgID(nodeID) } // StoreMessage is a part of the `store.MessageStore` implementation. func (fms *FileMessageStore) StoreMessage(message *protocol.Message, nodeID uint8) (int, error) { partitionName := message.Path.Partition() // If nodeID is zero means we are running in standalone more, otherwise // if the message has no nodeID it means it was received by this node if nodeID == 0 || message.NodeID == 0 { id, ts, err := fms.GenerateNextMsgID(partitionName, nodeID) if err != nil { logger.WithError(err).Error("Generation of id failed") return 0, err } message.ID = id message.Time = ts message.NodeID = nodeID log.WithFields(log.Fields{ "generatedID": id, "generatedTime": message.Time, }).Debug("Locally generated ID for message") } data := message.Bytes() if err := fms.Store(partitionName, message.ID, message.Bytes()); err != nil { logger. WithError(err).WithField("partition", partitionName). Error("Error storing locally generated messagein partition") return 0, err } logger.WithFields(log.Fields{ "id": message.ID, "ts": message.Time, "partition": partitionName, "messageUserID": message.UserID, "nodeID": nodeID, }).Debug("Stored message") return len(data), nil } // Store stores a message within a partition. // It is a part of the `store.MessageStore` implementation. func (fms *FileMessageStore) Store(partition string, msgID uint64, msg []byte) error { p, err := fms.Partition(partition) if err != nil { return err } return p.Store(msgID, msg) } // Fetch asynchronously fetches a set of messages defined by the fetch request. // It is a part of the `store.MessageStore` implementation. func (fms *FileMessageStore) Fetch(req *store.FetchRequest) { p, err := fms.Partition(req.Partition) if err != nil { req.ErrorC <- err return } p.Fetch(req) } // DoInTx is a part of the `store.MessageStore` implementation. func (fms *FileMessageStore) DoInTx(partition string, fnToExecute func(maxMessageId uint64) error) error { p, err := fms.Partition(partition) if err != nil { return err } return p.DoInTx(fnToExecute) } // Partitions will walk the filesystem and return all message partitions // TODO Bogdan This is not required anymore as the store already read the partitions // and saved them in the cacheEntry for the store. Retrieve from there if possible func (fms *FileMessageStore) Partitions() (partitions []store.MessagePartition, err error) { entries, err := ioutil.ReadDir(fms.basedir) if err != nil { logger.WithError(err).Error("Error reading partitions") return nil, err } for _, entry := range entries { if entry.IsDir() { partition, err := fms.Partition(entry.Name()) if err != nil { continue } partitions = append(partitions, partition) } } return } func (fms *FileMessageStore) Partition(partition string) (store.MessagePartition, error) { fms.mutex.Lock() defer fms.mutex.Unlock() partitionStore, exist := fms.partitions[partition] if !exist { dir := path.Join(fms.basedir, partition) if _, errStat := os.Stat(dir); errStat != nil { if os.IsNotExist(errStat) { if errMkdir := os.MkdirAll(dir, 0700); errMkdir != nil { logger.WithError(errMkdir).Error("partitionStore") return nil, errMkdir } } else { logger.WithError(errStat).Error("partitionStore") return nil, errStat } } var err error partitionStore, err = newMessagePartition(dir, partition) if err != nil { logger.WithField("err", err).Error("partitionStore") return nil, err } fms.partitions[partition] = partitionStore } return partitionStore, nil } // Check returns if available storage space is still above a certain threshold. func (fms *FileMessageStore) Check() error { var stat syscall.Statfs_t syscall.Statfs(fms.basedir, &stat) // available space in bytes = available blocks * size per block freeSpace := stat.Bavail * uint64(stat.Bsize) // total space in bytes = total system blocks * size per block totalSpace := stat.Blocks * uint64(stat.Bsize) usedSpacePercentage := 1 - (float64(freeSpace) / float64(totalSpace)) if usedSpacePercentage > 0.95 { errorMessage := "Storage is almost full" logger.WithFields(log.Fields{ "percentage": usedSpacePercentage, }).Warn(errorMessage) return errors.New(errorMessage) } return nil } // extractPartitionName returns the partition name from a filepath // The files would have this format /basepath/partition-number.extenstion // if filepath is not in the right format empty string is returned func extractPartitionName(p string) string { s := strings.SplitN(path.Base(p), "-", 2) if len(s) <= 2 { return "" } return s[0] } ================================================ FILE: server/store/filestore/message_store_test.go ================================================ package filestore import ( "fmt" "io/ioutil" "testing" "time" "github.com/smancke/guble/server/store" "github.com/stretchr/testify/assert" ) func Test_Fetch(t *testing.T) { a := assert.New(t) dir, _ := ioutil.TempDir("", "guble_message_store_test") //defer os.RemoveAll(dir) // when i store a message mStore := New(dir) a.NoError(mStore.Store("p1", uint64(1), []byte("aaaaaaaaaa"))) a.NoError(mStore.Store("p1", uint64(2), []byte("bbbbbbbbbb"))) a.NoError(mStore.Store("p2", uint64(1), []byte("1111111111"))) a.NoError(mStore.Store("p2", uint64(2), []byte("2222222222"))) testCases := []struct { description string req store.FetchRequest expectedResults []string }{ {`match in partition 1`, store.FetchRequest{Partition: "p1", StartID: 2, Count: 1}, []string{"bbbbbbbbbb"}, }, {`match in partition 2`, store.FetchRequest{Partition: "p2", StartID: 2, Count: 1}, []string{"2222222222"}, }, } for _, testcase := range testCases { testcase.req.MessageC = make(chan *store.FetchedMessage) testcase.req.ErrorC = make(chan error) testcase.req.StartC = make(chan int) messages := []string{} mStore.Fetch(&testcase.req) select { case numberOfResults := <-testcase.req.StartC: a.Equal(len(testcase.expectedResults), numberOfResults) case <-time.After(time.Second): a.Fail("timeout") return } loop: for { select { case msg, open := <-testcase.req.MessageC: if !open { break loop } messages = append(messages, string(msg.Message)) case err := <-testcase.req.ErrorC: a.Fail(err.Error()) break loop case <-time.After(time.Second): a.Fail("timeout") return } } a.Equal(testcase.expectedResults, messages, "Tescase: "+testcase.description) } } func Test_MessageStore_Close(t *testing.T) { a := assert.New(t) dir, _ := ioutil.TempDir("", "guble_message_store_test") //defer os.RemoveAll(dir) // when i store a message store := New(dir) a.NoError(store.Store("p1", uint64(1), []byte("aaaaaaaaaa"))) a.NoError(store.Store("p2", uint64(1), []byte("1111111111"))) a.Equal(2, len(store.partitions)) a.NoError(store.Stop()) a.Equal(0, len(store.partitions)) } func Test_MaxMessageId(t *testing.T) { a := assert.New(t) dir, _ := ioutil.TempDir("", "guble_message_store_test") //defer os.RemoveAll(dir) expectedMaxID := 2 // when i store a message store := New(dir) a.NoError(store.Store("p1", uint64(1), []byte("aaaaaaaaaa"))) a.NoError(store.Store("p1", uint64(expectedMaxID), []byte("bbbbbbbbbb"))) maxID, err := store.MaxMessageID("p1") a.Nil(err, "No error should be received for partition p1") a.Equal(maxID, uint64(expectedMaxID), fmt.Sprintf("MaxId should be [%d]", expectedMaxID)) } func Test_MaxMessageIdError(t *testing.T) { a := assert.New(t) store := New("/TestDir") _, err := store.MaxMessageID("p2") a.NotNil(err) } func Test_MessagePartitionReturningError(t *testing.T) { a := assert.New(t) store := New("/TestDir") _, err := store.Partition("p1") a.NotNil(err) fmt.Println(err) store2 := New("/") _, err2 := store2.Partition("p1") fmt.Println(err2) } func Test_FetchWithError(t *testing.T) { a := assert.New(t) mStore := New("/TestDir") chanCallBack := make(chan error, 1) aFetchRequest := store.FetchRequest{Partition: "p1", StartID: 2, Count: 1, ErrorC: chanCallBack} mStore.Fetch(&aFetchRequest) err := <-aFetchRequest.ErrorC a.NotNil(err) } func Test_StoreWithError(t *testing.T) { a := assert.New(t) mStore := New("/TestDir") err := mStore.Store("p1", uint64(1), []byte("124151qfas")) a.NotNil(err) } func Test_DoInTx(t *testing.T) { a := assert.New(t) dir, _ := ioutil.TempDir("", "guble_message_store_test") mStore := New(dir) a.NoError(mStore.Store("p1", uint64(1), []byte("aaaaaaaaaa"))) err := mStore.DoInTx("p1", func(maxId uint64) error { return nil }) a.Nil(err) } func Test_DoInTxError(t *testing.T) { a := assert.New(t) mStore := New("/TestDir") err := mStore.DoInTx("p2", nil) a.NotNil(err) } func Test_Check(t *testing.T) { a := assert.New(t) dir, _ := ioutil.TempDir("", "guble_message_store_test") mStore := New(dir) a.NoError(mStore.Store("p1", uint64(1), []byte("aaaaaaaaaa"))) err := mStore.Check() a.Nil(err) } // func Test_Partitions(t *testing.T) { // // Store multiple partitions then recreate the store and see if they are picked up // a := assert.New(t) // msg := []byte("test message data") // dir, err := ioutil.TempDir("", "guble_message_store_test") // a.NoError(err) // store := New(dir) // a.NoError(store.Store("p1", uint64(2), msg)) // a.NoError(store.Store("p2", uint64(2), msg)) // a.NoError(store.Store("p3", uint64(2), msg)) // store2 := New(dir) // partitions, err := store2.Partitions() // a.NoError(err) // a.Equal(3, len(partitions)) // a.Equal("p1", partitions[0].Name) // a.Equal("p2", partitions[1].Name) // a.Equal("p3", partitions[2].Name) // } ================================================ FILE: server/store/store.go ================================================ package store import "github.com/smancke/guble/protocol" // MessageStore is an interface for a persistence backend storing topics. type MessageStore interface { // Store a message within a partition. // The message id must be equal to MaxMessageId +1. // So the caller has to maintain the consistence between // fetching an id and storing the message. Store(partition string, messageID uint64, data []byte) error // Generates a new ID for the message if it's new and stores it // Returns the size of the new message or error // Takes the message and cluster node ID as parameters. StoreMessage(*protocol.Message, uint8) (int, error) // Fetch fetches a set of messages. // The results, as well as errors are communicated asynchronously using // the channels, supplied by the FetchRequest. Fetch(*FetchRequest) // MaxMessageId returns the highest message id for a particular partition MaxMessageID(partition string) (uint64, error) // DoInTx executes the supplied function within the locking context of the message partition. // This ensures, that wile the code is executed, no change to the supplied maxMessageId can occur. // The error result if the fnToExecute or an error while locking will be returned by DoInTx. DoInTx(partition string, fnToExecute func(uint64) error) error // GenerateNextMsgId generates a new message ID based on a timestamp in a strictly monotonically order GenerateNextMsgID(partition string, nodeID uint8) (uint64, int64, error) Partition(string) (MessagePartition, error) // Partitions returns a slice of `MessagePartition` available in the store Partitions() ([]MessagePartition, error) } type MessagePartition interface { // Name returns the name of the partition Name() string // MaxMessageID return the last message ID stored in this partition MaxMessageID() uint64 Count() uint64 Store(uint64, []byte) error Fetch(req *FetchRequest) DoInTx(func(uint64) error) error } ================================================ FILE: server/utils_test.go ================================================ package server import ( "bytes" "fmt" "io/ioutil" "net/http" "os" "strconv" "strings" "sync" "testing" "time" "github.com/smancke/guble/server/connector" "github.com/smancke/guble/server/fcm" "errors" "github.com/smancke/guble/client" "github.com/smancke/guble/server/service" "github.com/stretchr/testify/assert" "gopkg.in/alecthomas/kingpin.v2" ) type testClusterNodeConfig struct { HttpListen string // "host:port" format or just ":port" NodeID int NodePort int StoragePath string // if empty it will create a temporary directory MemoryStore string KVStore string Remotes string } func (tnc *testClusterNodeConfig) parseConfig() error { var err error dir := tnc.StoragePath if dir == "" { dir, err = ioutil.TempDir("", "guble_test") if err != nil { return err } } tnc.StoragePath = dir args := []string{ "--log", "debug", "--http", tnc.HttpListen, "--storage-path", tnc.StoragePath, "--health-endpoint", "", "--fcm", "--fcm-api-key", "WILL BE OVERWRITTEN", "--fcm-workers", "4", } if tnc.MemoryStore != "" { args = append(args, "--ms", tnc.MemoryStore) } if tnc.KVStore != "" { args = append(args, "--kvs", tnc.KVStore) } if tnc.NodeID > 0 { if tnc.Remotes == "" { return fmt.Errorf("Missing Remotes value when running in cluster mode.") } args = append( args, "--node-id", strconv.Itoa(tnc.NodeID), "--node-port", strconv.Itoa(tnc.NodePort), "--remotes", tnc.Remotes, ) } _, err = kingpin.CommandLine.Parse(args) return err } type testClusterNode struct { testClusterNodeConfig t *testing.T FCM *TestFCM Service *service.Service } func newTestClusterNode(t *testing.T, nodeConfig testClusterNodeConfig) *testClusterNode { a := assert.New(t) err := nodeConfig.parseConfig() if !a.NoError(err) { return nil } s := StartService() var ( fcmConnector connector.ResponsiveConnector ok bool ) for _, iface := range s.ModulesSortedByStartOrder() { if fcmConnector, ok = iface.(connector.ResponsiveConnector); ok { break } } if !a.True(ok, "There should be a module of type GCMConnector") { return nil } return &testClusterNode{ testClusterNodeConfig: nodeConfig, t: t, FCM: &TestFCM{ t: t, Connector: fcmConnector, }, Service: s, } } func (tcn *testClusterNode) client(userID string, bufferSize int, autoReconnect bool) (client.Client, error) { serverAddr := tcn.Service.WebServer().GetAddr() wsURL := "ws://" + serverAddr + "/stream/user/" + userID httpURL := "http://" + serverAddr return client.Open(wsURL, httpURL, bufferSize, autoReconnect) } func (tcn *testClusterNode) Subscribe(topic, id string) { tcn.FCM.subscribe(tcn.Service.WebServer().GetAddr(), topic, id) } func (tcn *testClusterNode) Unsubscribe(topic, id string) { tcn.FCM.unsubscribe(tcn.Service.WebServer().GetAddr(), topic, id) } func (tcn *testClusterNode) cleanup(removeDir bool) { tcn.FCM.cleanup() err := tcn.Service.Stop() assert.NoError(tcn.t, err) if removeDir { err = os.RemoveAll(tcn.StoragePath) assert.NoError(tcn.t, err) } } type TestFCM struct { sync.RWMutex t *testing.T Connector connector.ResponsiveConnector Received int // received messages receiveC chan bool timeout time.Duration } func (tfcm *TestFCM) setupRoundTripper(timeout time.Duration, bufferSize int, response string) { tfcm.receiveC = make(chan bool, bufferSize) tfcm.timeout = timeout sender, err := fcm.CreateFcmSender(response, tfcm.receiveC, timeout) assert.NoError(tfcm.t, err) tfcm.Connector.SetSender(sender) // start counting the received messages to FCM tfcm.receive() } func (tfcm *TestFCM) subscribe(addr, topic, id string) { urlFormat := fmt.Sprintf("http://%s/fcm/user_%%s/gcm_%%s/%%s", addr) a := assert.New(tfcm.t) response, err := http.Post( fmt.Sprintf(urlFormat, id, id, strings.TrimPrefix(topic, "/")), "text/plain", bytes.NewBufferString(""), ) if a.NoError(err) { a.Equal(response.StatusCode, 200) } body, err := ioutil.ReadAll(response.Body) a.NoError(err) a.Equal(fmt.Sprintf("{\"subscribed\":\"%s\"}", topic), string(body)) } func (tfcm *TestFCM) unsubscribe(addr, topic, id string) { urlFormat := fmt.Sprintf("http://%s/fcm/user_%%s/gcm_%%s/%%s", addr) a := assert.New(tfcm.t) req, err := http.NewRequest( http.MethodDelete, fmt.Sprintf(urlFormat, id, id, strings.TrimPrefix(topic, "/")), bytes.NewBufferString("")) a.NoError(err) hc := &http.Client{} response, err := hc.Do(req) if a.NoError(err) { a.Equal(response.StatusCode, 200) } body, err := ioutil.ReadAll(response.Body) a.NoError(err) a.Equal(fmt.Sprintf(`{"unsubscribed":"%s"}`, topic), string(body)) } // Wait waits count * tgcm.timeout, wait ensure count number of messages have been waited to pass // through GCM round tripper func (tfcm *TestFCM) wait(count int) { time.Sleep(time.Duration(count) * tfcm.timeout) } // Receive starts a goroutine that will receive on the receiveC and increment the Received counter // Returns an error if channel is not create func (tfcm *TestFCM) receive() error { if tfcm.receiveC == nil { return errors.New("Round tripper not created") } go func() { for { if _, opened := <-tfcm.receiveC; opened { tfcm.Lock() tfcm.Received++ tfcm.Unlock() } } }() return nil } func (tfcm *TestFCM) checkReceived(expected int) { time.Sleep((50 * time.Millisecond) + tfcm.timeout) tfcm.RLock() defer tfcm.RUnlock() assert.Equal(tfcm.t, expected, tfcm.Received) } func (tfcm *TestFCM) reset() { tfcm.Lock() defer tfcm.Unlock() tfcm.Received = 0 } func (tfcm *TestFCM) cleanup() { if tfcm.receiveC != nil { close(tfcm.receiveC) } } ================================================ FILE: server/webserver/logger.go ================================================ package webserver import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithFields(log.Fields{ "module": "webserver", }) ================================================ FILE: server/webserver/web_server.go ================================================ package webserver import ( "net" "net/http" "strings" "time" ) // WebServer is a struct representing a HTTP Server (using a net.Listener and a ServeMux multiplexer). type WebServer struct { server *http.Server ln net.Listener mux *http.ServeMux addr string } // New returns a new WebServer. func New(addr string) *WebServer { return &WebServer{ mux: http.NewServeMux(), addr: addr, } } // Start the WebServer (implementing service.startable interface). func (ws *WebServer) Start() (err error) { logger.WithField("address", ws.addr).Info("Http server is starting up on address") ws.server = &http.Server{Addr: ws.addr, Handler: ws.mux} ws.ln, err = net.Listen("tcp", ws.addr) if err != nil { return } go func() { err = ws.server.Serve(tcpKeepAliveListener{TCPListener: ws.ln.(*net.TCPListener)}) if err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") { logger.WithError(err).Error("ListenAndServe") } logger.WithField("address", ws.addr).Info("Http server stopped") }() return } // Stop the WebServer (implementing service.stopable interface). func (ws *WebServer) Stop() (err error) { if ws.ln != nil { err = ws.ln.Close() } // reset the mux ws.mux = http.NewServeMux() return } // Handle the given prefix using the given handler. // It is a part of the service.endpoint interface. func (ws *WebServer) Handle(prefix string, handler http.Handler) { ws.mux.Handle(prefix, handler) } // GetAddr returns the address on which the WebServer is listening. // It is a part of the service.endpoint interface. func (ws *WebServer) GetAddr() string { if ws.ln == nil { return "::unknown::" } return ws.ln.Addr().String() } // copied from golang: net/http/server.go // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted // connections. It's used by ListenAndServe and ListenAndServeTLS so // dead TCP connections (e.g. closing laptop mid-download) eventually // go away. type tcpKeepAliveListener struct { *net.TCPListener } func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { tc, err := ln.AcceptTCP() if err != nil { return } tc.SetKeepAlive(true) tc.SetKeepAlivePeriod(10 * time.Second) return tc, nil } ================================================ FILE: server/webserver/web_server_test.go ================================================ package webserver import ( "bytes" "github.com/stretchr/testify/assert" "io/ioutil" "net/http" "testing" "time" ) func TestStartAndStopWebServer(t *testing.T) { // given: a configured echo webserver server := New("localhost:3333") server.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { bytes, _ := ioutil.ReadAll(r.Body) w.Write(bytes) }) // when: I start the server server.Start() time.Sleep(time.Millisecond * 10) addr := server.GetAddr() // and: send a testmessage resp, err := http.Post("http://"+addr, "text/plain", bytes.NewBufferString("hello")) // then: the message is returned assert.NoError(t, err) responseBody, _ := ioutil.ReadAll(resp.Body) assert.Equal(t, "hello", string(responseBody)) // and when: we stop the service server.Stop() time.Sleep(time.Millisecond * 100) // then: the next call returns an error // because the server is closed c2 := &http.Client{} c2.Transport = &http.Transport{DisableKeepAlives: true} _, err = c2.Post("http://"+addr, "text/plain", bytes.NewBufferString("hello")) assert.Error(t, err) } ================================================ FILE: server/websocket/logger.go ================================================ package websocket import ( log "github.com/Sirupsen/logrus" ) var logger = log.WithFields(log.Fields{ "module": "websocket", }) ================================================ FILE: server/websocket/mocks_auth_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/auth (interfaces: AccessManager) package websocket import ( gomock "github.com/golang/mock/gomock" protocol "github.com/smancke/guble/protocol" auth "github.com/smancke/guble/server/auth" ) // Mock of AccessManager interface type MockAccessManager struct { ctrl *gomock.Controller recorder *_MockAccessManagerRecorder } // Recorder for MockAccessManager (not exported) type _MockAccessManagerRecorder struct { mock *MockAccessManager } func NewMockAccessManager(ctrl *gomock.Controller) *MockAccessManager { mock := &MockAccessManager{ctrl: ctrl} mock.recorder = &_MockAccessManagerRecorder{mock} return mock } func (_m *MockAccessManager) EXPECT() *_MockAccessManagerRecorder { return _m.recorder } func (_m *MockAccessManager) IsAllowed(_param0 auth.AccessType, _param1 string, _param2 protocol.Path) bool { ret := _m.ctrl.Call(_m, "IsAllowed", _param0, _param1, _param2) ret0, _ := ret[0].(bool) return ret0 } func (_mr *_MockAccessManagerRecorder) IsAllowed(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "IsAllowed", arg0, arg1, arg2) } ================================================ FILE: server/websocket/mocks_router_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/router (interfaces: Router) package websocket import ( "github.com/golang/mock/gomock" "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/cluster" "github.com/smancke/guble/server/kvstore" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" ) // Mock of Router interface type MockRouter struct { ctrl *gomock.Controller recorder *_MockRouterRecorder } // Recorder for MockRouter (not exported) type _MockRouterRecorder struct { mock *MockRouter } func NewMockRouter(ctrl *gomock.Controller) *MockRouter { mock := &MockRouter{ctrl: ctrl} mock.recorder = &_MockRouterRecorder{mock} return mock } func (_m *MockRouter) EXPECT() *_MockRouterRecorder { return _m.recorder } func (_m *MockRouter) AccessManager() (auth.AccessManager, error) { ret := _m.ctrl.Call(_m, "AccessManager") ret0, _ := ret[0].(auth.AccessManager) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) AccessManager() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "AccessManager") } func (_m *MockRouter) Cluster() *cluster.Cluster { ret := _m.ctrl.Call(_m, "Cluster") ret0, _ := ret[0].(*cluster.Cluster) return ret0 } func (_mr *_MockRouterRecorder) Cluster() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Cluster") } func (_m *MockRouter) Done() <-chan bool { ret := _m.ctrl.Call(_m, "Done") ret0, _ := ret[0].(<-chan bool) return ret0 } func (_mr *_MockRouterRecorder) Done() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Done") } func (_m *MockRouter) Fetch(_param0 *store.FetchRequest) error { ret := _m.ctrl.Call(_m, "Fetch", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockRouter) GetSubscribers(_param0 string) ([]byte, error) { ret := _m.ctrl.Call(_m, "GetSubscribers", _param0) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) GetSubscribers(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GetSubscribers", arg0) } func (_m *MockRouter) HandleMessage(_param0 *protocol.Message) error { ret := _m.ctrl.Call(_m, "HandleMessage", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockRouterRecorder) HandleMessage(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "HandleMessage", arg0) } func (_m *MockRouter) KVStore() (kvstore.KVStore, error) { ret := _m.ctrl.Call(_m, "KVStore") ret0, _ := ret[0].(kvstore.KVStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) KVStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "KVStore") } func (_m *MockRouter) MessageStore() (store.MessageStore, error) { ret := _m.ctrl.Call(_m, "MessageStore") ret0, _ := ret[0].(store.MessageStore) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) MessageStore() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MessageStore") } func (_m *MockRouter) Subscribe(_param0 *router.Route) (*router.Route, error) { ret := _m.ctrl.Call(_m, "Subscribe", _param0) ret0, _ := ret[0].(*router.Route) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockRouterRecorder) Subscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Subscribe", arg0) } func (_m *MockRouter) Unsubscribe(_param0 *router.Route) { _m.ctrl.Call(_m, "Unsubscribe", _param0) } func (_mr *_MockRouterRecorder) Unsubscribe(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Unsubscribe", arg0) } ================================================ FILE: server/websocket/mocks_store_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/store (interfaces: MessageStore) package websocket import ( gomock "github.com/golang/mock/gomock" protocol "github.com/smancke/guble/protocol" store "github.com/smancke/guble/server/store" ) // Mock of MessageStore interface type MockMessageStore struct { ctrl *gomock.Controller recorder *_MockMessageStoreRecorder } // Recorder for MockMessageStore (not exported) type _MockMessageStoreRecorder struct { mock *MockMessageStore } func NewMockMessageStore(ctrl *gomock.Controller) *MockMessageStore { mock := &MockMessageStore{ctrl: ctrl} mock.recorder = &_MockMessageStoreRecorder{mock} return mock } func (_m *MockMessageStore) EXPECT() *_MockMessageStoreRecorder { return _m.recorder } func (_m *MockMessageStore) DoInTx(_param0 string, _param1 func(uint64) error) error { ret := _m.ctrl.Call(_m, "DoInTx", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockMessageStoreRecorder) DoInTx(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "DoInTx", arg0, arg1) } func (_m *MockMessageStore) Fetch(_param0 *store.FetchRequest) { _m.ctrl.Call(_m, "Fetch", _param0) } func (_mr *_MockMessageStoreRecorder) Fetch(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Fetch", arg0) } func (_m *MockMessageStore) GenerateNextMsgID(_param0 string, _param1 byte) (uint64, int64, error) { ret := _m.ctrl.Call(_m, "GenerateNextMsgID", _param0, _param1) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(int64) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } func (_mr *_MockMessageStoreRecorder) GenerateNextMsgID(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "GenerateNextMsgID", arg0, arg1) } func (_m *MockMessageStore) MaxMessageID(_param0 string) (uint64, error) { ret := _m.ctrl.Call(_m, "MaxMessageID", _param0) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) MaxMessageID(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "MaxMessageID", arg0) } func (_m *MockMessageStore) Partition(_param0 string) (store.MessagePartition, error) { ret := _m.ctrl.Call(_m, "Partition", _param0) ret0, _ := ret[0].(store.MessagePartition) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) Partition(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Partition", arg0) } func (_m *MockMessageStore) Partitions() ([]store.MessagePartition, error) { ret := _m.ctrl.Call(_m, "Partitions") ret0, _ := ret[0].([]store.MessagePartition) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) Partitions() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Partitions") } func (_m *MockMessageStore) Store(_param0 string, _param1 uint64, _param2 []byte) error { ret := _m.ctrl.Call(_m, "Store", _param0, _param1, _param2) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockMessageStoreRecorder) Store(arg0, arg1, arg2 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Store", arg0, arg1, arg2) } func (_m *MockMessageStore) StoreMessage(_param0 *protocol.Message, _param1 byte) (int, error) { ret := _m.ctrl.Call(_m, "StoreMessage", _param0, _param1) ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockMessageStoreRecorder) StoreMessage(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "StoreMessage", arg0, arg1) } ================================================ FILE: server/websocket/mocks_websocket_gen_test.go ================================================ // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/smancke/guble/server/websocket (interfaces: WSConnection) package websocket import ( gomock "github.com/golang/mock/gomock" ) // Mock of WSConnection interface type MockWSConnection struct { ctrl *gomock.Controller recorder *_MockWSConnectionRecorder } // Recorder for MockWSConnection (not exported) type _MockWSConnectionRecorder struct { mock *MockWSConnection } func NewMockWSConnection(ctrl *gomock.Controller) *MockWSConnection { mock := &MockWSConnection{ctrl: ctrl} mock.recorder = &_MockWSConnectionRecorder{mock} return mock } func (_m *MockWSConnection) EXPECT() *_MockWSConnectionRecorder { return _m.recorder } func (_m *MockWSConnection) Close() { _m.ctrl.Call(_m, "Close") } func (_mr *_MockWSConnectionRecorder) Close() *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Close") } func (_m *MockWSConnection) Receive(_param0 *[]byte) error { ret := _m.ctrl.Call(_m, "Receive", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockWSConnectionRecorder) Receive(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Receive", arg0) } func (_m *MockWSConnection) Send(_param0 []byte) error { ret := _m.ctrl.Call(_m, "Send", _param0) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockWSConnectionRecorder) Send(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Send", arg0) } ================================================ FILE: server/websocket/receiver.go ================================================ package websocket import ( "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" "errors" "fmt" "math" "strconv" "strings" log "github.com/Sirupsen/logrus" ) var errUnreadMsgsAvailable = errors.New("unread messages available") // Receiver is a helper class, for managing a combined pull push on a topic. // It is used for implementation of the + (receive) command in the guble protocol. type Receiver struct { cancelC chan bool sendC chan []byte applicationID string router router.Router messageStore store.MessageStore path protocol.Path doFetch bool doSubscription bool startID int64 maxCount int lastSentID uint64 shouldStop bool route *router.Route enableNotifications bool userID string } // NewReceiverFromCmd parses the info in the command func NewReceiverFromCmd( applicationID string, cmd *protocol.Cmd, sendChannel chan []byte, router router.Router, userID string) (rec *Receiver, err error) { messageStore, err := router.MessageStore() if err != nil { return nil, err } rec = &Receiver{ applicationID: applicationID, sendC: sendChannel, router: router, messageStore: messageStore, cancelC: make(chan bool, 1), enableNotifications: true, userID: userID, } if len(cmd.Arg) == 0 || cmd.Arg[0] != '/' { return nil, fmt.Errorf("command requires at least a path argument, but non given") } args := strings.SplitN(cmd.Arg, " ", 3) rec.path = protocol.Path(args[0]) if len(args) > 1 { rec.doFetch = true rec.startID, err = strconv.ParseInt(args[1], 10, 64) if err != nil { return nil, fmt.Errorf("startid has to be empty or int, but was %q: %v", args[1], err) } } rec.doSubscription = true if len(args) > 2 { rec.doSubscription = false rec.maxCount, err = strconv.Atoi(args[2]) if err != nil { return nil, fmt.Errorf("maxCount has to be empty or int, but was %q: %v", args[1], err) } } return rec, nil } // Start starts the receiver loop func (rec *Receiver) Start() error { rec.shouldStop = false if rec.doFetch && !rec.doSubscription { go rec.fetchOnlyLoop() } else { go rec.subscriptionLoop() } return nil } func (rec *Receiver) subscriptionLoop() { for !rec.shouldStop { if rec.doFetch { if err := rec.fetch(); err != nil { logger.WithError(err).WithField("rec", rec).Error("Error while fetching subscription") rec.sendError(protocol.ERROR_INTERNAL_SERVER, err.Error()) return } if err := rec.messageStore.DoInTx(rec.path.Partition(), rec.subscribeIfNoUnreadMessagesAvailable); err != nil { if err == errUnreadMsgsAvailable { logger.WithFields(log.Fields{ "lastSentId": rec.lastSentID, "receiver": rec, }).Error("errUnreadMsgsAvailable") rec.startID = int64(rec.lastSentID) + 1 continue // fetch again } else { logger.WithError(err).WithField("recStartId", rec.startID). Error("Error while subscribeIfNoUnreadMessagesAvailable") rec.sendError(protocol.ERROR_INTERNAL_SERVER, err.Error()) return } } } else { rec.subscribe() } rec.receiveFromSubscription() if !rec.shouldStop { //fmt.Printf(" router closed .. on msg: %v\n", rec.lastSendId) // the router kicked us out, because we are too slow for realtime listening, // so we setup parameters for fetching and closing the gap. Than we can subscribe again. rec.startID = int64(rec.lastSentID) + 1 rec.doFetch = true } } } func (rec *Receiver) subscribeIfNoUnreadMessagesAvailable(maxMessageID uint64) error { if maxMessageID > rec.lastSentID { return errUnreadMsgsAvailable } rec.subscribe() return nil } func (rec *Receiver) subscribe() { rec.route = router.NewRoute( router.RouteConfig{ RouteParams: router.RouteParams{"application_id": rec.applicationID, "user_id": rec.userID}, Path: rec.path, ChannelSize: 10, }, ) _, err := rec.router.Subscribe(rec.route) if err != nil { rec.sendError(protocol.ERROR_SUBSCRIBED_TO, string(rec.path), err.Error()) } else { rec.sendOK(protocol.SUCCESS_SUBSCRIBED_TO, string(rec.path)) } } func (rec *Receiver) receiveFromSubscription() { for { select { case m, ok := <-rec.route.MessagesChannel(): if !ok { logger.WithFields(log.Fields{ "applicationId": rec.applicationID, }).Debug("Router closed the channel returning from subscription for") return } logger.WithFields(log.Fields{ "applicationId": rec.applicationID, "messageMetadata": m.Metadata(), }).Debug("Delivering message") if m.ID > rec.lastSentID { rec.lastSentID = m.ID rec.sendC <- m.Bytes() } else { logger.WithFields(log.Fields{ "msgId": m.ID, }).Debug("Message already sent to client. Dropping message.") } case <-rec.cancelC: rec.shouldStop = true rec.router.Unsubscribe(rec.route) rec.route = nil rec.sendOK(protocol.SUCCESS_CANCELED, string(rec.path)) return } } } func (rec *Receiver) fetchOnlyLoop() { err := rec.fetch() if err != nil { logger.WithError(err).WithField("rec", rec).Error("Error while fetching") rec.sendError(protocol.ERROR_INTERNAL_SERVER, err.Error()) } } func (rec *Receiver) fetch() error { fetch := &store.FetchRequest{ Partition: rec.path.Partition(), MessageC: make(chan *store.FetchedMessage, 10), //TODO MAKE more tests when the receiver will be refactored after the route params is integrated.Initial capacity was 3 ErrorC: make(chan error), StartC: make(chan int), Count: rec.maxCount, } if rec.startID >= 0 { fetch.Direction = 1 fetch.StartID = uint64(rec.startID) if rec.maxCount == 0 { fetch.Count = math.MaxInt32 } } else { fetch.Direction = -1 maxID, err := rec.messageStore.MaxMessageID(rec.path.Partition()) if err != nil { return err } fetch.StartID = maxID if rec.maxCount == 0 { fetch.Count = -1 * int(rec.startID) } } rec.messageStore.Fetch(fetch) for { select { case numberOfResults := <-fetch.StartC: rec.sendOK(protocol.SUCCESS_FETCH_START, fmt.Sprintf("%v %v", rec.path, numberOfResults)) case msgAndID, open := <-fetch.MessageC: if !open { rec.sendOK(protocol.SUCCESS_FETCH_END, string(rec.path)) return nil } logger.WithFields(log.Fields{ "msgId": msgAndID.ID, "msg": string(msgAndID.Message), "lastSendId": rec.lastSentID, }).Info("Reply sent") rec.lastSentID = msgAndID.ID rec.sendC <- msgAndID.Message case err := <-fetch.ErrorC: return err case <-rec.cancelC: rec.shouldStop = true rec.sendOK(protocol.SUCCESS_CANCELED, string(rec.path)) // TODO implement cancellation in message store return nil } } } // Stop stops/cancels the receiver func (rec *Receiver) Stop() error { rec.cancelC <- true return nil } func (rec *Receiver) sendError(name string, argPattern string, params ...interface{}) { notificationMessage := &protocol.NotificationMessage{ Name: name, Arg: fmt.Sprintf(argPattern, params...), IsError: true, } rec.sendC <- notificationMessage.Bytes() } func (rec *Receiver) sendOK(name string, argPattern string, params ...interface{}) { if rec.enableNotifications { notificationMessage := &protocol.NotificationMessage{ Name: name, Arg: fmt.Sprintf(argPattern, params...), IsError: false, } rec.sendC <- notificationMessage.Bytes() } } ================================================ FILE: server/websocket/receiver_test.go ================================================ package websocket import ( "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" "github.com/smancke/guble/testutil" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "errors" "math" "testing" "time" ) func Test_Receiver_error_handling_on_create(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) badArgs := []string{"", "20", "foo 20 20", "/foo 20 20 20", "/foo a", "/foo 20 b"} for _, arg := range badArgs { rec, _, _, _, err := aMockedReceiver(arg) a.Nil(rec, "Testing with: "+arg) a.Error(err, "Testing with: "+arg) } } func Test_Receiver_Fetch_Subscribe_Fetch_Subscribe(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) rec, msgChannel, routerMock, messageStore, err := aMockedReceiver("/foo 0") a.NoError(err) // fetch first, starting at 0 fetchFirst1 := messageStore.EXPECT().Fetch(gomock.Any()).Do(func(r *store.FetchRequest) { go func() { a.Equal("foo", r.Partition) a.Equal(store.DirectionForward, r.Direction) a.Equal(uint64(0), r.StartID) a.Equal(int(math.MaxInt32), r.Count) r.StartC <- 2 r.MessageC <- &store.FetchedMessage{ID: uint64(1), Message: []byte("fetch_first1-a")} r.MessageC <- &store.FetchedMessage{ID: uint64(2), Message: []byte("fetch_first1-b")} close(r.MessageC) }() }) // there is a gap between fetched and max id messageID1 := messageStore.EXPECT().DoInTx(gomock.Any(), gomock.Any()). Do(func(partition string, callback func(maxMessageId uint64) error) { callback(uint64(3)) }).Return(errUnreadMsgsAvailable) messageID1.After(fetchFirst1) // fetch again, starting at 3, because, there is still a gap fetchFirst2 := messageStore.EXPECT().Fetch(gomock.Any()).Do(func(r *store.FetchRequest) { go func() { a.Equal("foo", r.Partition) a.Equal(store.DirectionForward, r.Direction) a.Equal(uint64(3), r.StartID) a.Equal(int(math.MaxInt32), r.Count) r.StartC <- 1 r.MessageC <- &store.FetchedMessage{ID: uint64(3), Message: []byte("fetch_first2-a")} close(r.MessageC) }() }) fetchFirst2.After(messageID1) // the gap is closed messageID2 := messageStore.EXPECT().DoInTx(gomock.Any(), gomock.Any()). Do(func(partition string, callback func(maxMessageId uint64) error) { callback(uint64(3)) }) messageID2.After(fetchFirst2) // subscribe subscribe := routerMock.EXPECT().Subscribe(gomock.Any()).Do(func(r *router.Route) { a.Equal(r.Path, protocol.Path("/foo")) r.Deliver(&protocol.Message{ID: uint64(4), Body: []byte("router-a"), Time: 1405544146}, true) r.Deliver(&protocol.Message{ID: uint64(5), Body: []byte("router-b"), Time: 1405544146}, true) r.Close() // emulate router close }) subscribe.After(messageID2) // router closed, so we fetch again, starting at 6 (after meesages from subscribe) fetchAfter := messageStore.EXPECT().Fetch(gomock.Any()).Do(func(r *store.FetchRequest) { go func() { a.Equal(uint64(6), r.StartID) a.Equal(int(math.MaxInt32), r.Count) r.StartC <- 1 r.MessageC <- &store.FetchedMessage{ID: uint64(6), Message: []byte("fetch_after-a")} close(r.MessageC) }() }) fetchAfter.After(subscribe) // no gap messageID3 := messageStore.EXPECT().DoInTx(gomock.Any(), gomock.Any()). Do(func(partition string, callback func(maxMessageId uint64) error) { callback(uint64(6)) }) messageID3.After(fetchAfter) // subscribe and don't send messages, // so the client has to wait until we stop subscribe2 := routerMock.EXPECT().Subscribe(gomock.Any()) subscribe2.After(messageID3) subscriptionLoopDone := make(chan bool) go func() { rec.subscriptionLoop() subscriptionLoopDone <- true }() expectMessages(a, msgChannel, "#"+protocol.SUCCESS_FETCH_START+" /foo 2", "fetch_first1-a", "fetch_first1-b", "#"+protocol.SUCCESS_FETCH_END+" /foo", "#"+protocol.SUCCESS_FETCH_START+" /foo 1", "fetch_first2-a", "#"+protocol.SUCCESS_FETCH_END+" /foo", "#"+protocol.SUCCESS_SUBSCRIBED_TO+" /foo", ",4,,,,1405544146,0\n\nrouter-a", ",5,,,,1405544146,0\n\nrouter-b", "#"+protocol.SUCCESS_FETCH_START+" /foo 1", "fetch_after-a", "#"+protocol.SUCCESS_FETCH_END+" /foo", "#"+protocol.SUCCESS_SUBSCRIBED_TO+" /foo", ) time.Sleep(time.Millisecond) routerMock.EXPECT().Unsubscribe(gomock.Any()) rec.Stop() expectMessages(a, msgChannel, "#"+protocol.SUCCESS_CANCELED+" /foo", ) testutil.ExpectDone(a, subscriptionLoopDone) } func Test_Receiver_Fetch_Returns_Correct_Messages(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) rec, msgChannel, _, messageStore, err := aMockedReceiver("/foo 0 2") a.NoError(err) messages := []string{"The answer ", "is 42"} done := make(chan bool) messageStore.EXPECT().Fetch(gomock.Any()).Do(func(r *store.FetchRequest) { go func() { r.StartC <- len(messages) for i, m := range messages { r.MessageC <- &store.FetchedMessage{ID: uint64(i + 1), Message: []byte(m)} } close(r.MessageC) done <- true }() }) fetchHasTerminated := make(chan bool) go func() { rec.fetchOnlyLoop() fetchHasTerminated <- true }() testutil.ExpectDone(a, done) expectMessages(a, msgChannel, "#"+protocol.SUCCESS_FETCH_START+" /foo 2") expectMessages(a, msgChannel, messages...) expectMessages(a, msgChannel, "#"+protocol.SUCCESS_FETCH_END+" /foo") testutil.ExpectDone(a, fetchHasTerminated) ctrl.Finish() } func Test_Receiver_Fetch_Produces_Correct_Fetch_Requests(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) testcases := []struct { desc string arg string maxID int expect store.FetchRequest }{ {desc: "simple forward fetch", arg: "/foo 0 20", maxID: -1, expect: store.FetchRequest{Partition: "foo", Direction: 1, StartID: uint64(0), Count: 20}, }, {desc: "forward fetch without bounds", arg: "/foo 0", maxID: -1, expect: store.FetchRequest{Partition: "foo", Direction: 1, StartID: uint64(0), Count: math.MaxInt32}, }, {desc: "backward fetch to top", arg: "/foo -20", maxID: 42, expect: store.FetchRequest{Partition: "foo", Direction: -1, StartID: uint64(42), Count: 20}, }, {desc: "backward fetch with count", arg: "/foo -1 10", maxID: 42, expect: store.FetchRequest{Partition: "foo", Direction: -1, StartID: uint64(42), Count: 10}, }, } for _, test := range testcases { rec, _, _, messageStore, err := aMockedReceiver(test.arg) a.NotNil(rec) a.NoError(err, test.desc) if test.maxID != -1 { messageStore.EXPECT().MaxMessageID(test.expect.Partition). Return(uint64(test.maxID), nil) } done := make(chan bool) messageStore.EXPECT().Fetch(gomock.Any()).Do(func(r *store.FetchRequest) { a.Equal(test.expect.Partition, r.Partition, test.desc) a.Equal(test.expect.Direction, r.Direction, test.desc) a.Equal(test.expect.StartID, r.StartID, test.desc) a.Equal(test.expect.Count, r.Count, test.desc) done <- true }) go rec.fetchOnlyLoop() testutil.ExpectDone(a, done) rec.Stop() } } func Test_Receiver_Fetch_Sends_error_on_failure(t *testing.T) { a := assert.New(t) for _, arg := range []string{ "/foo 0 2", // fetch only "/foo 0", // fetch and subscribe } { ctrl := gomock.NewController(t) rec, msgChannel, _, messageStore, err := aMockedReceiver(arg) a.NoError(err) messageStore.EXPECT().Fetch(gomock.Any()).Do(func(r *store.FetchRequest) { go func() { r.ErrorC <- errors.New("expected test error") }() }) rec.Start() expectMessages(a, msgChannel, "!error-server-internal expected test error") ctrl.Finish() } } func Test_Receiver_Fetch_Sends_error_on_failure_in_MaxMessageId(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) rec, msgChannel, _, messageStore, err := aMockedReceiver("/foo -2 2") a.NoError(err) messageStore.EXPECT().MaxMessageID("foo"). Return(uint64(0), errors.New("expected test error")) rec.Start() expectMessages(a, msgChannel, "!error-server-internal expected test error") } //rec, sendChannel, router, messageStore, err := aMockedReceiver("+") func aMockedReceiver(arg string) (*Receiver, chan []byte, *MockRouter, *MockMessageStore, error) { routerMock := NewMockRouter(testutil.MockCtrl) messageStore := NewMockMessageStore(testutil.MockCtrl) routerMock.EXPECT().MessageStore().Return(messageStore, nil).AnyTimes() sendChannel := make(chan []byte) cmd := &protocol.Cmd{ Name: protocol.CmdReceive, Arg: arg, } rec, err := NewReceiverFromCmd("any-appId", cmd, sendChannel, routerMock, "userId") return rec, sendChannel, routerMock, messageStore, err } func expectMessages(a *assert.Assertions, msgChannel chan []byte, message ...string) { for _, m := range message { select { case msg := <-msgChannel: a.Equal(m, string(msg)) case <-time.After(time.Millisecond * 100): a.Fail("timeout: " + m) return } } } ================================================ FILE: server/websocket/websocket_connector.go ================================================ package websocket import ( "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/router" log "github.com/Sirupsen/logrus" "github.com/gorilla/websocket" "github.com/rs/xid" "fmt" "net/http" "strings" "time" ) var webSocketUpgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } // WSHandler is a struct used for handling websocket connections on a certain prefix. type WSHandler struct { router router.Router prefix string accessManager auth.AccessManager } // NewWSHandler returns a new WSHandler. func NewWSHandler(router router.Router, prefix string) (*WSHandler, error) { accessManager, err := router.AccessManager() if err != nil { return nil, err } return &WSHandler{ router: router, prefix: prefix, accessManager: accessManager, }, nil } // GetPrefix returns the prefix. // It is a part of the service.endpoint implementation. func (handler *WSHandler) GetPrefix() string { return handler.prefix } // ServeHTTP is an http.Handler. // It is a part of the service.endpoint implementation. func (handler *WSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c, err := webSocketUpgrader.Upgrade(w, r, nil) if err != nil { logger.WithError(err).Error("Error on upgrading to websocket") return } defer c.Close() NewWebSocket(handler, &wsconn{c}, extractUserID(r.RequestURI)).Start() } // WSConnection is a wrapper interface for the needed functions of the websocket.Conn // It is introduced for testability of the WSHandler type WSConnection interface { Close() Send(bytes []byte) (err error) Receive(bytes *[]byte) (err error) } // wsconnImpl is a Wrapper of the websocket.Conn // implementing the interface WSConn for better testability type wsconn struct { *websocket.Conn } // Close the connection. func (conn *wsconn) Close() { conn.Conn.Close() } // Send bytes through the connection and possibly return an error. func (conn *wsconn) Send(bytes []byte) error { return conn.WriteMessage(websocket.BinaryMessage, bytes) } // Receive bytes through the connection and possibly return an error. func (conn *wsconn) Receive(bytes *[]byte) (err error) { _, *bytes, err = conn.ReadMessage() return err } // WebSocket struct represents a websocket. type WebSocket struct { *WSHandler WSConnection applicationID string userID string sendChannel chan []byte receivers map[protocol.Path]*Receiver } // NewWebSocket returns a new WebSocket. func NewWebSocket(handler *WSHandler, wsConn WSConnection, userID string) *WebSocket { return &WebSocket{ WSHandler: handler, WSConnection: wsConn, applicationID: xid.New().String(), userID: userID, sendChannel: make(chan []byte, 10), receivers: make(map[protocol.Path]*Receiver), } } // Start the WebSocket (the send and receive loops). // It is implementing the service.startable interface. func (ws *WebSocket) Start() error { ws.sendConnectionMessage() go ws.sendLoop() ws.receiveLoop() return nil } func (ws *WebSocket) sendLoop() { for raw := range ws.sendChannel { if !ws.checkAccess(raw) { continue } if err := ws.Send(raw); err != nil { logger.WithFields(log.Fields{ "userId": ws.userID, "applicationID": ws.applicationID, "totalSize": len(raw), "actualContent": string(raw), }).Error("Could not send") ws.cleanAndClose() break } } } func (ws *WebSocket) checkAccess(raw []byte) bool { if len(raw) > 0 && raw[0] == byte('/') { path := getPathFromRawMessage(raw) logger.WithFields(log.Fields{ "userID": ws.userID, "path": path, }).Debug("Received msg") return len(path) == 0 || ws.accessManager.IsAllowed(auth.READ, ws.userID, path) } return true } func getPathFromRawMessage(raw []byte) protocol.Path { i := strings.Index(string(raw), ",") return protocol.Path(raw[:i]) } func (ws *WebSocket) receiveLoop() { var message []byte for { err := ws.Receive(&message) if err != nil { logger.WithFields(log.Fields{ "applicationID": ws.applicationID, }).Debug("Closed connnection by application") ws.cleanAndClose() break } //protocol.Debug("websocket_connector, raw message received: %v", string(message)) cmd, err := protocol.ParseCmd(message) if err != nil { ws.sendError(protocol.ERROR_BAD_REQUEST, "error parsing command. %v", err.Error()) continue } switch cmd.Name { case protocol.CmdSend: ws.handleSendCmd(cmd) case protocol.CmdReceive: ws.handleReceiveCmd(cmd) case protocol.CmdCancel: ws.handleCancelCmd(cmd) default: ws.sendError(protocol.ERROR_BAD_REQUEST, "unknown command %v", cmd.Name) } } } func (ws *WebSocket) sendConnectionMessage() { n := &protocol.NotificationMessage{ Name: protocol.SUCCESS_CONNECTED, Arg: "You are connected to the server.", Json: fmt.Sprintf(`{"ApplicationId": "%s", "UserId": "%s", "Time": "%s"}`, ws.applicationID, ws.userID, time.Now().Format(time.RFC3339)), } ws.sendChannel <- n.Bytes() } func (ws *WebSocket) handleReceiveCmd(cmd *protocol.Cmd) { rec, err := NewReceiverFromCmd( ws.applicationID, cmd, ws.sendChannel, ws.router, ws.userID, ) if err != nil { logger.WithError(err).Error("Client error in handleReceiveCmd") ws.sendError(protocol.ERROR_BAD_REQUEST, err.Error()) return } ws.receivers[rec.path] = rec rec.Start() } func (ws *WebSocket) handleCancelCmd(cmd *protocol.Cmd) { if len(cmd.Arg) == 0 { ws.sendError(protocol.ERROR_BAD_REQUEST, "- command requires a path argument, but none given") return } path := protocol.Path(cmd.Arg) rec, exist := ws.receivers[path] if exist { rec.Stop() delete(ws.receivers, path) } } func (ws *WebSocket) handleSendCmd(cmd *protocol.Cmd) { logger.WithFields(log.Fields{ "cmd": string(cmd.Bytes()), }).Debug("Sending ") if len(cmd.Arg) == 0 { ws.sendError(protocol.ERROR_BAD_REQUEST, "send command requires a path argument, but none given") return } args := strings.SplitN(cmd.Arg, " ", 2) msg := &protocol.Message{ Path: protocol.Path(args[0]), ApplicationID: ws.applicationID, UserID: ws.userID, HeaderJSON: cmd.HeaderJSON, Body: cmd.Body, } ws.router.HandleMessage(msg) ws.sendOK(protocol.SUCCESS_SEND, "") } func (ws *WebSocket) cleanAndClose() { logger.WithFields(log.Fields{ "applicationID": ws.applicationID, }).Debug("Closing applicationId") for path, rec := range ws.receivers { rec.Stop() delete(ws.receivers, path) } ws.Close() } func (ws *WebSocket) sendError(name string, argPattern string, params ...interface{}) { n := &protocol.NotificationMessage{ Name: name, Arg: fmt.Sprintf(argPattern, params...), IsError: true, } ws.sendChannel <- n.Bytes() } func (ws *WebSocket) sendOK(name string, argPattern string, params ...interface{}) { n := &protocol.NotificationMessage{ Name: name, Arg: fmt.Sprintf(argPattern, params...), IsError: false, } ws.sendChannel <- n.Bytes() } // Extracts the userID out of an URI or empty string if format not met // Example: // http://example.com/user/user01/ -> user01 // http://example.com/user/ -> "" func extractUserID(uri string) string { uriParts := strings.SplitN(uri, "/user/", 2) if len(uriParts) != 2 { return "" } return uriParts[1] } ================================================ FILE: server/websocket/websocket_connector_test.go ================================================ package websocket import ( "github.com/smancke/guble/protocol" "github.com/smancke/guble/server/auth" "github.com/smancke/guble/server/router" "github.com/smancke/guble/server/store" "github.com/smancke/guble/testutil" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "fmt" "strings" "sync" "testing" "time" ) var aTestMessage = &protocol.Message{ ID: uint64(42), Path: "/foo", Body: []byte("Test"), } func Test_WebSocket_SubscribeAndUnsubscribe(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() a := assert.New(t) messages := []string{"+ /foo", "+ /bar", "- /foo"} wsconn, routerMock, messageStore := createDefaultMocks(messages) var wg sync.WaitGroup wg.Add(3) doneGroup := func(bytes []byte) error { wg.Done() return nil } routerMock.EXPECT().Subscribe(routeMatcher{"/foo"}).Return(nil, nil) wsconn.EXPECT(). Send([]byte("#" + protocol.SUCCESS_SUBSCRIBED_TO + " /foo")). Do(doneGroup) routerMock.EXPECT().Subscribe(routeMatcher{"/bar"}).Return(nil, nil) wsconn.EXPECT(). Send([]byte("#" + protocol.SUCCESS_SUBSCRIBED_TO + " /bar")). Do(doneGroup) routerMock.EXPECT().Unsubscribe(routeMatcher{"/foo"}) wsconn.EXPECT(). Send([]byte("#" + protocol.SUCCESS_CANCELED + " /foo")). Do(doneGroup) websocket := runNewWebSocket(wsconn, routerMock, messageStore, nil) wg.Wait() a.Equal(1, len(websocket.receivers)) a.Equal(protocol.Path("/bar"), websocket.receivers[protocol.Path("/bar")].path) } func Test_SendMessage(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() commands := []string{"> /path\n{\"key\": \"value\"}\nHello, this is a test"} wsconn, routerMock, messageStore := createDefaultMocks(commands) routerMock.EXPECT().HandleMessage(messageMatcher{path: "/path", message: "Hello, this is a test", header: `{"key": "value"}`}) wsconn.EXPECT().Send([]byte("#send")) runNewWebSocket(wsconn, routerMock, messageStore, nil) } func Test_AnIncomingMessageIsDelivered(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() wsconn, routerMock, messageStore := createDefaultMocks([]string{}) wsconn.EXPECT().Send(aTestMessage.Bytes()) handler := runNewWebSocket(wsconn, routerMock, messageStore, nil) handler.sendChannel <- aTestMessage.Bytes() time.Sleep(time.Millisecond * 2) } func Test_AnIncomingMessageIsNotAllowed(t *testing.T) { ctrl, finish := testutil.NewMockCtrl(t) defer finish() wsconn, routerMock, _ := createDefaultMocks([]string{}) tam := NewMockAccessManager(ctrl) tam.EXPECT().IsAllowed(auth.READ, "testuser", protocol.Path("/foo")).Return(false) handler := NewWebSocket( testWSHandler(routerMock, tam), wsconn, "testuser", ) go func() { handler.Start() }() time.Sleep(time.Millisecond * 2) handler.sendChannel <- aTestMessage.Bytes() time.Sleep(time.Millisecond * 2) //nothing shall have been sent //now allow tam.EXPECT().IsAllowed(auth.READ, "testuser", protocol.Path("/foo")).Return(true) wsconn.EXPECT().Send(aTestMessage.Bytes()) time.Sleep(time.Millisecond * 2) handler.sendChannel <- aTestMessage.Bytes() time.Sleep(time.Millisecond * 2) } func Test_BadCommands(t *testing.T) { _, finish := testutil.NewMockCtrl(t) defer finish() badRequests := []string{"XXXX", "", ">", ">/foo", "+", "-", "send /foo"} wsconn, routerMock, messageStore := createDefaultMocks(badRequests) counter := 0 var wg sync.WaitGroup wg.Add(len(badRequests)) wsconn.EXPECT().Send(gomock.Any()).Do(func(data []byte) error { if strings.HasPrefix(string(data), "#connected") { return nil } if strings.HasPrefix(string(data), "!error-bad-request") { counter++ } else { t.Logf("expected bad-request, but got: %v", string(data)) } wg.Done() return nil }).AnyTimes() runNewWebSocket(wsconn, routerMock, messageStore, nil) wg.Wait() assert.Equal(t, len(badRequests), counter, "expected number of bad requests does not match") } func TestExtractUserId(t *testing.T) { assert.Equal(t, "marvin", extractUserID("/foo/user/marvin")) assert.Equal(t, "marvin", extractUserID("/user/marvin")) assert.Equal(t, "", extractUserID("/")) } func testWSHandler( routerMock *MockRouter, accessManager auth.AccessManager) *WSHandler { return &WSHandler{ router: routerMock, prefix: "/prefix", accessManager: accessManager, } } func runNewWebSocket( wsconn *MockWSConnection, routerMock *MockRouter, messageStore store.MessageStore, accessManager auth.AccessManager) *WebSocket { if accessManager == nil { accessManager = auth.NewAllowAllAccessManager(true) } websocket := NewWebSocket( testWSHandler(routerMock, accessManager), wsconn, "testuser", ) go func() { websocket.Start() }() time.Sleep(time.Millisecond * 2) return websocket } func createDefaultMocks(inputMessages []string) ( *MockWSConnection, *MockRouter, *MockMessageStore) { inputMessagesC := make(chan []byte, len(inputMessages)) for _, msg := range inputMessages { inputMessagesC <- []byte(msg) } routerMock := NewMockRouter(testutil.MockCtrl) messageStore := NewMockMessageStore(testutil.MockCtrl) routerMock.EXPECT().MessageStore().Return(messageStore, nil).AnyTimes() wsconn := NewMockWSConnection(testutil.MockCtrl) wsconn.EXPECT().Receive(gomock.Any()).Do(func(message *[]byte) error { *message = <-inputMessagesC return nil }).Times(len(inputMessages) + 1) wsconn.EXPECT().Send(connectedNotificationMatcher{}) return wsconn, routerMock, messageStore } // --- routeMatcher --------- type routeMatcher struct { path string } func (n routeMatcher) Matches(x interface{}) bool { return n.path == string(x.(*router.Route).Path) } func (n routeMatcher) String() string { return "route path equals " + n.path } // --- messageMatcher --------- type messageMatcher struct { id uint64 path string message string header string } func (n messageMatcher) Matches(x interface{}) bool { return n.path == string(x.(*protocol.Message).Path) && n.message == string(x.(*protocol.Message).Body) && (n.id == 0 || n.id == x.(*protocol.Message).ID) && (n.header == "" || (n.header == x.(*protocol.Message).HeaderJSON)) } func (n messageMatcher) String() string { return fmt.Sprintf("message equals %q, %q, %q", n.id, n.path, n.message) } // --- Connected Notification Matcher --------- type connectedNotificationMatcher struct { } func (notify connectedNotificationMatcher) Matches(x interface{}) bool { return strings.HasPrefix(string(x.([]byte)), "#connected") } func (notify connectedNotificationMatcher) String() string { return fmt.Sprintf("is connected message") } ================================================ FILE: test.sh ================================================ #!/usr/bin/env bash GO_TEST_DISABLED=true go test -short ./... TESTRESULT=$? RED='\033[0;31m' GREEN='\033[0;32m' NOCOLOR='\033[0m' case ${TESTRESULT} in 0) MESSAGE="${GREEN}OK" ;; 1) MESSAGE="${RED}Test(s) failing" ;; 2) MESSAGE="${RED}Compilation error" ;; *) MESSAGE="${RED}Error(s)" ;; esac echo -e "${MESSAGE}${NOCOLOR}\n" exit ${TESTRESULT} ================================================ FILE: testutil/testutil.go ================================================ package testutil import ( //used for pprof server _ "net/http/pprof" log "github.com/Sirupsen/logrus" "github.com/docker/distribution/health" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "net/http" "os" "testing" "time" ) // MockCtrl is a gomock.Controller to use globally var MockCtrl *gomock.Controller func init() { // disable error output while testing // because also negative tests are tested log.SetLevel(log.ErrorLevel) } // NewMockCtrl initializes the `MockCtrl` package var and returns a method to // finish the controller when test is complete // **Important**: Don't forget to call the returned method at the end of the test // Usage: // ctrl, finish := test_util.NewMockCtrl(t) // defer finish() func NewMockCtrl(t *testing.T) (*gomock.Controller, func()) { MockCtrl = gomock.NewController(t) return MockCtrl, func() { MockCtrl.Finish() } } func NewMockBenchmarkCtrl(b *testing.B) (*gomock.Controller, func()) { MockCtrl = gomock.NewController(b) return MockCtrl, func() { MockCtrl.Finish() } } // EnableDebugForMethod enables debug-level output through the current test // Usage: // testutil.EnableDebugForMethod()() func EnableDebugForMethod() func() { reset := log.GetLevel() log.SetLevel(log.DebugLevel) return func() { log.SetLevel(reset) } } // EnableInfoForMethod enables info-level output through the current test // Usage: // testutil.EnableInfoForMethod()() func EnableInfoForMethod() func() { reset := log.GetLevel() log.SetLevel(log.InfoLevel) return func() { log.SetLevel(reset) } } // ExpectDone waits to receive a value in the doneChannel for at least a second // or fails the test. func ExpectDone(a *assert.Assertions, doneChannel chan bool) { select { case <-doneChannel: return case <-time.After(time.Second): a.Fail("timeout in expectDone") } } // ExpectPanic expects a panic (and fails if this does not happen). func ExpectPanic(t *testing.T) { if r := recover(); r == nil { assert.Fail(t, "Expecting a panic but unfortunately it did not happen") } } // ResetDefaultRegistryHealthCheck resets the existing registry containing health-checks func ResetDefaultRegistryHealthCheck() { health.DefaultRegistry = health.NewRegistry() } //SkipIfShort skips a test if the `-short` flag is given to `go test` func SkipIfShort(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } } //SkipIfDisabled skips a test if the GO_TEST_DISABLED environment variable is set to any value (when `go test` runs) func SkipIfDisabled(t *testing.T) { if os.Getenv("GO_TEST_DISABLED") != "" { t.Skip("skipping disabled test.") } } func PprofDebug() { go func() { http.ListenAndServe("localhost:6060", nil) }() }