├── .dockerignore ├── .github └── workflows │ └── build.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── admin-frontend ├── .gitignore ├── build │ ├── asset-manifest.json │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ ├── precache-manifest.3afed1ea7c9ac78d7d981ba00d010ddc.js │ ├── service-worker.js │ └── static │ │ ├── css │ │ └── main.74be1e85.chunk.css │ │ └── js │ │ ├── 2.0baf6681.chunk.js │ │ ├── main.849f1b4e.chunk.js │ │ └── runtime~main.a8a9905a.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── AMQPApi.js │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── components │ ├── channels │ │ └── index.js │ ├── connections │ │ └── index.js │ ├── exchanges │ │ └── index.js │ ├── overview │ │ └── index.js │ └── queues │ │ └── index.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ └── registerServiceWorker.js ├── admin ├── handler_bindings.go ├── handler_channels.go ├── handler_connections.go ├── handler_exchanges.go ├── handler_overview.go ├── handler_queues.go ├── server.go └── utils.go ├── amqp ├── constants_generated.go ├── extended_constants.go ├── methods_generated.go ├── readers_writers.go ├── readers_writers_test.go ├── types.go └── types_test.go ├── auth ├── auth.go └── auth_test.go ├── bin └── .gitignore ├── binding ├── binding.go └── binding_test.go ├── config ├── config.go └── default.go ├── consumer └── consumer.go ├── etc └── config.yaml ├── exchange ├── exchange.go └── exchange_test.go ├── go.mod ├── go.sum ├── interfaces └── interfaces.go ├── main.go ├── metrics ├── counter.go ├── registry.go └── trackBuffer.go ├── msgstorage └── msgstorage.go ├── pool └── buffer.go ├── protocol ├── amqp0-9-1.extended.xml ├── amqp0-9-1.xml ├── protogen.go └── templates.go ├── qos ├── qos.go └── qos_test.go ├── queue ├── queue.go ├── queue_consumer_test.go ├── queue_storage_test.go └── queue_test.go ├── readme ├── channels.jpg ├── connections.jpg ├── exchanges.jpg ├── overview.jpg └── queues.jpg ├── safequeue ├── safequeue.go └── safequeue_test.go ├── server ├── basicMethods.go ├── channel.go ├── channelMethods.go ├── confirmMethods.go ├── connection.go ├── connectionMethods.go ├── daemon.go ├── daemon_linux.go ├── exchangeMethods.go ├── queueMethods.go ├── server.go ├── server_basic_test.go ├── server_channel_test.go ├── server_confirm_test.go ├── server_connection_test.go ├── server_exchange_test.go ├── server_persist_test.go ├── server_queue_test.go ├── server_test.go └── vhost.go ├── srvstorage └── srvstorage.go └── storage ├── .DS_Store ├── storage_badger.go └── storage_bunt.go /.dockerignore: -------------------------------------------------------------------------------- 1 | admin-frontend/node_modules 2 | db -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build-go: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: 1.19 22 | 23 | - name: Install goveralls 24 | run: go install github.com/mattn/goveralls@latest 25 | 26 | - name: Test 27 | run: | 28 | ulimit -n 2048 ; go test -v ./... -covermode=count -coverprofile=covprofile.tmp 29 | cat covprofile.tmp | grep -v "generated" | grep -v "admin" > covprofile 30 | 31 | - name: Send coverage 32 | env: 33 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: goveralls -coverprofile=covprofile -service=github 35 | 36 | build-js: 37 | runs-on: ubuntu-latest 38 | defaults: 39 | run: 40 | working-directory: 'admin-frontend' 41 | strategy: 42 | matrix: 43 | node-version: [ 14.x, 16.x ] 44 | 45 | steps: 46 | - uses: actions/checkout@v3 47 | - name: Use Node.js ${{ matrix.node-version }} 48 | uses: actions/setup-node@v3 49 | with: 50 | node-version: ${{ matrix.node-version }} 51 | cache: 'npm' 52 | cache-dependency-path: '**/package-lock.json' 53 | 54 | - name: Install deps 55 | working-directory: admin-frontend 56 | run: npm ci 57 | 58 | - name: Build admin 59 | working-directory: admin-frontend 60 | run: npm run build --if-present -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/garagemq 2 | db/ 3 | node_modules 4 | .idea 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM golang as builder 3 | 4 | ENV GO111MODULE=on 5 | 6 | WORKDIR /app 7 | 8 | COPY go.mod . 9 | COPY go.sum . 10 | 11 | RUN go mod download 12 | 13 | COPY . . 14 | 15 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/garagemq 16 | 17 | # final stage 18 | FROM alpine 19 | COPY --from=builder /app/bin/garagemq /app/ 20 | COPY --from=builder /app/admin-frontend/build /app/admin-frontend/build 21 | WORKDIR /app 22 | 23 | EXPOSE 5672 15672 24 | 25 | ENTRYPOINT ["/app/garagemq"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alexander Valinurov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | amqp.gen: 2 | go run protocol/*.go && go fmt amqp/*_generated.go 3 | 4 | deps: 5 | dep ensure && cd admin-frontend && yarn install 6 | 7 | build.all: deps 8 | go build -o bin/garagemq main.go && cd admin-frontend && yarn build 9 | 10 | build: 11 | env GO111MODULE=on go build -o bin/garagemq main.go 12 | 13 | run: build 14 | bin/garagemq 15 | 16 | profile: build 17 | bin/garagemq --hprof=true 18 | 19 | vet: 20 | env GO111MODULE=on go vet github.com/valinurovam/garagemq... 21 | 22 | test: 23 | ulimit -n 2048 && env GO111MODULE=on go test -cover github.com/valinurovam/garagemq... -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GarageMQ [![Build Status](https://github.com/valinurovam/garagemq/actions/workflows/build.yml/badge.svg)](https://github.com/valinurovam/garagemq/actions) [![Coverage Status](https://coveralls.io/repos/github/valinurovam/garagemq/badge.svg)](https://coveralls.io/github/valinurovam/garagemq) [![Go Report Card](https://goreportcard.com/badge/github.com/valinurovam/garagemq)](https://goreportcard.com/report/github.com/valinurovam/garagemq) 2 | 3 | GarageMQ is a message broker that implement the Advanced Message Queuing Protocol (AMQP). Compatible with any AMQP or RabbitMQ clients (tested streadway/amqp and php-amqp lib) 4 | 5 | #### Table of Contents 6 | - [Goals of this project](#goals-of-this-project) 7 | - [Demo](#demo) 8 | - [Installation and Building](#installation-and-building) 9 | - [Docker](#docker) 10 | - [Go get](#go-get) 11 | - [Execution flags](#execution-flags) 12 | - [Default config params](#default-config-params) 13 | - [Performance tests](#performance-tests) 14 | - [Persistent messages](#persistent-messages) 15 | - [Transient messages](#transient-messages) 16 | - [Internals](#internals) 17 | - [Backend for durable entities](#backend-for-durable-entities) 18 | - [QOS](#qos) 19 | - [Admin server](#admin-server) 20 | - [TODO](#todo) 21 | - [Contribution](#contribution) 22 | 23 | ## Goals of this project 24 | 25 | - Have fun and learn a lot 26 | - Implement AMQP message broker in Go 27 | - Make protocol compatible with RabbitMQ and standard AMQP 0-9-1. 28 | 29 | 30 | ## Demo 31 | Simple demo server on Digital Ocean, ```2 GB Memory / 25 GB Disk / FRA1 - Ubuntu Docker 17.12.0~ce on 16.04``` 32 | 33 | | Server | Port | Admin port | Login | Password | Virtual Host | 34 | |:-------------:|:----:|:----------:|:-----:|:--------:|:------------:| 35 | | 46.101.117.78 | 5672 | 15672 | guest | guest | / | 36 | 37 | - [AdminServer - http://46.101.117.78:15672/](http://46.101.117.78:15672/) 38 | - Connect uri - ```amqp://guest:guest@46.101.117.78:5672``` 39 | 40 | ## Installation and Building 41 | ### Docker 42 | 43 | The quick way to start with GarageMQ is by using `docker`. You can build it by your own or pull from docker-hub 44 | ```shell 45 | docker pull amplitudo/garagemq 46 | docker run --name garagemq -p 5672:5672 -p 15672:15672 amplitudo/garagemq 47 | ``` 48 | or 49 | 50 | ```shell 51 | go get -u github.com/valinurovam/garagemq/... 52 | cd $GOPATH/src/github.com/valinurovam/garagemq 53 | docker build -t garagemq . 54 | docker run --name garagemq -p 5672:5672 -p 15672:15672 garagemq 55 | 56 | ``` 57 | ### Go get 58 | You can also use [go get](https://golang.org/cmd/go/#hdr-Download_and_install_packages_and_dependencies): ```go get -u github.com/valinurovam/garagemq/...``` 59 | ```shell 60 | go get -u github.com/valinurovam/garagemq/... 61 | cd $GOPATH/src/github.com/valinurovam/garagemq 62 | make build.all && make run 63 | ``` 64 | ### Execution flags 65 | | Flag | Default | Description | ENV | 66 | | :--- | :--- | :--- | :--- | 67 | | --config | [default config](#default-config-params) | Config path | GMQ_CONFIG | 68 | | --log-file | stdout | Log file path or `stdout`, `stderr`| GMQ_LOG_FILE | 69 | | --log-level | info | Logger level | GMQ_LOG_LEVEL | 70 | | --hprof | false | Enable or disable [hprof profiler](https://golang.org/pkg/net/http/pprof/#pkg-overview) | GMQ_HPROF | 71 | | --hprof-host | 0.0.0.0 | Profiler host | GMQ_HPROF_HOST | 72 | | --hprof-port | 8080 | Profiler port | GMQ_HPROF_PORT | 73 | 74 | ### Default config params 75 | ```yaml 76 | # Proto name to implement (amqp-rabbit or amqp-0-9-1) 77 | proto: amqp-rabbit 78 | # User list 79 | users: 80 | - username: guest 81 | password: 084e0343a0486ff05530df6c705c8bb4 # guest md5 82 | # Server TCP settings 83 | tcp: 84 | ip: 0.0.0.0 85 | port: 5672 86 | nodelay: false 87 | readBufSize: 196608 88 | writeBufSize: 196608 89 | # Admin-server settings 90 | admin: 91 | ip: 0.0.0.0 92 | port: 15672 93 | queue: 94 | shardSize: 8192 95 | maxMessagesInRam: 131072 96 | # DB settings 97 | db: 98 | # default path 99 | defaultPath: db 100 | # backend engine (badger or buntdb) 101 | engine: badger 102 | # Default virtual host path 103 | vhost: 104 | defaultPath: / 105 | # Security check rule (md5 or bcrypt) 106 | security: 107 | passwordCheck: md5 108 | connection: 109 | channelsMax: 4096 110 | frameMaxSize: 65536 111 | ``` 112 | 113 | ## Performance tests 114 | 115 | Performance tests with load testing tool https://github.com/rabbitmq/rabbitmq-perf-test on test-machine: 116 | ``` 117 | MacBook Pro (15-inch, 2016) 118 | Processor 2,6 GHz Intel Core i7 119 | Memory 16 GB 2133 MHz LPDDR3 120 | ``` 121 | ### Persistent messages 122 | ```shell 123 | ./bin/runjava com.rabbitmq.perf.PerfTest --exchange test -uri amqp://guest:guest@localhost:5672 --queue test --consumers 10 --producers 5 --qos 100 -flag persistent 124 | ... 125 | ... 126 | id: test-235131-686, sending rate avg: 53577 msg/s 127 | id: test-235131-686, receiving rate avg: 51941 msg/s 128 | ``` 129 | ### Transient messages 130 | ```shell 131 | ./bin/runjava com.rabbitmq.perf.PerfTest --exchange test -uri amqp://guest:guest@localhost:5672 --queue test --consumers 10 --producers 5 --qos 100 132 | ... 133 | ... 134 | id: test-235231-085, sending rate avg: 71247 msg/s 135 | id: test-235231-085, receiving rate avg: 69009 msg/s 136 | ``` 137 | 138 | ## Internals 139 | 140 | ### Backend for durable entities 141 | Database backend is changeable through config `db.engine` 142 | ``` 143 | db: 144 | defaultPath: db 145 | engine: badger 146 | ``` 147 | ``` 148 | db: 149 | defaultPath: db 150 | engine: buntdb 151 | ``` 152 | - Badger https://github.com/dgraph-io/badger 153 | - BuntDB https://github.com/tidwall/buntdb 154 | 155 | ### QOS 156 | 157 | `basic.qos` method implemented for standard AMQP and RabbitMQ mode. It means that by default qos applies for connection(global=true) or channel(global=false). 158 | RabbitMQ Qos means for channel(global=true) or each new consumer(global=false). 159 | 160 | ### Admin server 161 | 162 | The administration server is available at standard `:15672` port and is `read only mode` at the moment. Main page above, and [more screenshots](/readme) at /readme folder 163 | 164 | ![Overview](readme/overview.jpg) 165 | 166 | ## TODO 167 | - [ ] Optimize binds 168 | - [ ] Replication and clusterization 169 | - [ ] Own backend for durable entities and persistent messages 170 | - [ ] Migrate to message reference counting 171 | 172 | ## Contribution 173 | Contribution of any kind is always welcome and appreciated. Contribution Guidelines in WIP 174 | -------------------------------------------------------------------------------- /admin-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # misc 8 | .DS_Store 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | -------------------------------------------------------------------------------- /admin-frontend/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.74be1e85.chunk.css", 4 | "main.js": "/static/js/main.849f1b4e.chunk.js", 5 | "runtime~main.js": "/static/js/runtime~main.a8a9905a.js", 6 | "static/js/2.0baf6681.chunk.js": "/static/js/2.0baf6681.chunk.js", 7 | "index.html": "/index.html", 8 | "precache-manifest.3afed1ea7c9ac78d7d981ba00d010ddc.js": "/precache-manifest.3afed1ea7c9ac78d7d981ba00d010ddc.js", 9 | "service-worker.js": "/service-worker.js" 10 | } 11 | } -------------------------------------------------------------------------------- /admin-frontend/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valinurovam/garagemq/becd122222a0028eb64343f4d7391ee2ec7a321e/admin-frontend/build/favicon.ico -------------------------------------------------------------------------------- /admin-frontend/build/index.html: -------------------------------------------------------------------------------- 1 | GarageMQ Admin
-------------------------------------------------------------------------------- /admin-frontend/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /admin-frontend/build/precache-manifest.3afed1ea7c9ac78d7d981ba00d010ddc.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = (self.__precacheManifest || []).concat([ 2 | { 3 | "revision": "17e248444df3626a9e0a604edc67dc73", 4 | "url": "/index.html" 5 | }, 6 | { 7 | "revision": "c6188c42d3dd03530bfd", 8 | "url": "/static/css/main.74be1e85.chunk.css" 9 | }, 10 | { 11 | "revision": "56a022d4a2f9fa8032f0", 12 | "url": "/static/js/2.0baf6681.chunk.js" 13 | }, 14 | { 15 | "revision": "c6188c42d3dd03530bfd", 16 | "url": "/static/js/main.849f1b4e.chunk.js" 17 | }, 18 | { 19 | "revision": "42ac5946195a7306e2a5", 20 | "url": "/static/js/runtime~main.a8a9905a.js" 21 | } 22 | ]); -------------------------------------------------------------------------------- /admin-frontend/build/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); 15 | 16 | importScripts( 17 | "/precache-manifest.3afed1ea7c9ac78d7d981ba00d010ddc.js" 18 | ); 19 | 20 | self.addEventListener('message', (event) => { 21 | if (event.data && event.data.type === 'SKIP_WAITING') { 22 | self.skipWaiting(); 23 | } 24 | }); 25 | 26 | workbox.core.clientsClaim(); 27 | 28 | /** 29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 30 | * requests for URLs in the manifest. 31 | * See https://goo.gl/S9QRab 32 | */ 33 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 35 | 36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), { 37 | 38 | blacklist: [/^\/_/,/\/[^/]+\.[^/]+$/], 39 | }); 40 | -------------------------------------------------------------------------------- /admin-frontend/build/static/css/main.74be1e85.chunk.css: -------------------------------------------------------------------------------- 1 | body{margin:0;padding:0;font-family:sans-serif} -------------------------------------------------------------------------------- /admin-frontend/build/static/js/runtime~main.a8a9905a.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /admin-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valinurovam/garagemq/becd122222a0028eb64343f4d7391ee2ec7a321e/admin-frontend/public/favicon.ico -------------------------------------------------------------------------------- /admin-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | GarageMQ Admin 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /admin-frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /admin-frontend/src/AMQPApi.js: -------------------------------------------------------------------------------- 1 | import axios from "axios/index"; 2 | 3 | const AMQPAPI = axios.create({ 4 | baseURL: (process.env.NODE_ENV === 'development') ? 'http://localhost:15672' : '', 5 | headers: { 6 | 'Accept': 'application/json', 7 | 'Content-Type': 'application/json', 8 | }, 9 | }); 10 | 11 | export default AMQPAPI; -------------------------------------------------------------------------------- /admin-frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | -------------------------------------------------------------------------------- /admin-frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Link, Route, Switch, withRouter} from 'react-router-dom' 4 | import AppBar from '@material-ui/core/AppBar'; 5 | import Toolbar from '@material-ui/core/Toolbar'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import {withStyles} from "@material-ui/core/styles/index"; 8 | import classNames from 'classnames'; 9 | import CssBaseline from '@material-ui/core/CssBaseline'; 10 | import Drawer from '@material-ui/core/Drawer'; 11 | import List from '@material-ui/core/List'; 12 | import Divider from '@material-ui/core/Divider'; 13 | 14 | import ListItem from '@material-ui/core/ListItem'; 15 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 16 | import ListItemText from '@material-ui/core/ListItemText'; 17 | import DashboardIcon from '@material-ui/icons/Dashboard'; 18 | import Cast from '@material-ui/icons/Cast'; 19 | import DeviceHub from '@material-ui/icons/DeviceHub'; 20 | import PowerInput from '@material-ui/icons/PowerInput'; 21 | import CloudQueue from '@material-ui/icons/CloudQueue'; 22 | 23 | 24 | import Exchanges from './components/exchanges'; 25 | import Connections from './components/connections'; 26 | import Queues from './components/queues'; 27 | import Overview from './components/overview'; 28 | import Channels from './components/channels'; 29 | 30 | const drawerWidth = 200; 31 | 32 | let menuItems = [ 33 | { 34 | route: '/', text: 'Overview', icon: () => { 35 | return () 36 | } 37 | }, 38 | { 39 | route: '/connections', text: 'Connections', icon: () => { 40 | return () 41 | } 42 | }, 43 | { 44 | route: '/channels', text: 'Channels', icon: () => { 45 | return () 46 | } 47 | }, 48 | { 49 | route: '/exchanges', text: 'Exchanges', icon: () => { 50 | return () 51 | } 52 | }, 53 | { 54 | route: '/queues', text: 'Queues', icon: () => { 55 | return () 56 | } 57 | }, 58 | ]; 59 | 60 | const styles = theme => ({ 61 | root: { 62 | display: 'flex', 63 | }, 64 | toolbar: { 65 | paddingRight: 24, // keep right padding when drawer closed 66 | }, 67 | toolbarIcon: { 68 | display: 'flex', 69 | alignItems: 'center', 70 | justifyContent: 'flex-end', 71 | padding: '0 8px', 72 | ...theme.mixins.toolbar, 73 | }, 74 | appBar: { 75 | zIndex: theme.zIndex.drawer + 1, 76 | transition: theme.transitions.create(['width', 'margin'], { 77 | easing: theme.transitions.easing.sharp, 78 | duration: theme.transitions.duration.leavingScreen, 79 | }), 80 | }, 81 | appBarShift: { 82 | marginLeft: drawerWidth, 83 | width: `calc(100% - ${drawerWidth}px)`, 84 | transition: theme.transitions.create(['width', 'margin'], { 85 | easing: theme.transitions.easing.sharp, 86 | duration: theme.transitions.duration.enteringScreen, 87 | }), 88 | }, 89 | title: { 90 | flexGrow: 1, 91 | }, 92 | drawerPaper: { 93 | position: 'relative', 94 | whiteSpace: 'nowrap', 95 | width: drawerWidth, 96 | transition: theme.transitions.create('width', { 97 | easing: theme.transitions.easing.sharp, 98 | duration: theme.transitions.duration.enteringScreen, 99 | }), 100 | }, 101 | appBarSpacer: theme.mixins.toolbar, 102 | content: { 103 | flexGrow: 1, 104 | padding: theme.spacing.unit * 3, 105 | height: '100vh', 106 | overflow: 'auto', 107 | }, 108 | chartContainer: { 109 | marginLeft: -22, 110 | }, 111 | tableContainer: { 112 | height: 320, 113 | }, 114 | }); 115 | 116 | class App extends React.Component { 117 | menuItems = () => { 118 | let result = []; 119 | for (let i = 0; i < menuItems.length; i++) { 120 | let item = menuItems[i]; 121 | result.push( 122 | 123 | 124 | {item.icon()} 125 | 126 | 127 | ); 128 | } 129 | 130 | return result; 131 | }; 132 | 133 | currentPage = () => { 134 | const {location} = this.props; 135 | let item = menuItems.find(e => e.route === location.pathname); 136 | if (!item) { 137 | return 'Not Found' 138 | } 139 | 140 | return item.text; 141 | }; 142 | 143 | render() { 144 | const {classes} = this.props; 145 | 146 | return ( 147 | 148 | 149 |
150 | 154 | 155 | 156 | {this.currentPage()} 157 | 158 | 159 | GarageMQ 160 | 161 | 162 | 163 | 170 |
171 | 172 |
173 | 174 | {this.menuItems()} 175 | 176 |
177 |
178 |
179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 |
188 |
189 |
190 | ) 191 | } 192 | } 193 | 194 | App.propTypes = { 195 | classes: PropTypes.object.isRequired, 196 | selected: PropTypes.string, 197 | }; 198 | 199 | 200 | export default withStyles(styles)(withRouter(App)); -------------------------------------------------------------------------------- /admin-frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /admin-frontend/src/components/channels/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Table from '@material-ui/core/Table'; 3 | import TableBody from '@material-ui/core/TableBody'; 4 | import TableCell from '@material-ui/core/TableCell'; 5 | import TableHead from '@material-ui/core/TableHead'; 6 | import TableRow from '@material-ui/core/TableRow'; 7 | import {withStyles} from '@material-ui/core/styles'; 8 | import Grid from '@material-ui/core/Grid'; 9 | import AMQPAPI from '../../AMQPApi' 10 | import Paper from '@material-ui/core/Paper'; 11 | 12 | const styles = theme => ({ 13 | tableContainer: { 14 | width: '100%', 15 | }, 16 | table: { 17 | width: '100%', 18 | }, 19 | formContainer: { 20 | width: 300, 21 | }, 22 | form: { 23 | marginLeft: theme.spacing.unit * 1, 24 | }, 25 | submit: { 26 | marginTop: theme.spacing.unit * 3, 27 | }, 28 | tableHead: { 29 | fontWeight: 'bold', 30 | } 31 | }); 32 | 33 | class Channels extends React.Component { 34 | constructor() { 35 | super(); 36 | 37 | this.state = { 38 | channels: [], 39 | }; 40 | 41 | this.loadChannels() 42 | } 43 | 44 | loadChannels = () => { 45 | AMQPAPI.get('/channels') 46 | .then(response => this.setState({channels: response.data.items ? response.data.items : []})) 47 | }; 48 | 49 | 50 | tableItems = () => { 51 | const {classes} = this.props; 52 | let rowId = 0; 53 | 54 | return ( 55 | 56 | 57 | 58 | Channel 59 | Virtual host 60 | User name 61 | QOS 62 | Unacked 63 | Publish 64 | Confirm 65 | Deliver 66 | Get 67 | Ack 68 | 69 | 70 | 71 | {this.state.channels.map(row => { 72 | return ( 73 | 74 | {row.channel} 75 | {row.vhost} 76 | {row.user} 77 | {row.qos} 78 | {row.counters.unacked.value} 79 | {this.transformRate(row.counters.publish)} 80 | {this.transformRate(row.counters.confirm)} 81 | {this.transformRate(row.counters.deliver)} 82 | {this.transformRate(row.counters.get)} 83 | {this.transformRate(row.counters.ack)} 84 | 85 | ); 86 | })} 87 | 88 |
89 | ) 90 | }; 91 | 92 | transformRate = (trackValue) => { 93 | if (!trackValue || !trackValue.value) { 94 | return '0/s' 95 | } 96 | 97 | return trackValue.value + '/s' 98 | }; 99 | 100 | render() { 101 | setTimeout(this.loadChannels, 5000); 102 | const {classes} = this.props; 103 | 104 | return ( 105 | 111 | 112 | {this.tableItems()} 113 | 114 | 115 | ) 116 | } 117 | } 118 | 119 | export default withStyles(styles)(Channels); -------------------------------------------------------------------------------- /admin-frontend/src/components/connections/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Table from '@material-ui/core/Table'; 3 | import TableBody from '@material-ui/core/TableBody'; 4 | import TableCell from '@material-ui/core/TableCell'; 5 | import TableHead from '@material-ui/core/TableHead'; 6 | import TableRow from '@material-ui/core/TableRow'; 7 | import {withStyles} from '@material-ui/core/styles'; 8 | import Grid from '@material-ui/core/Grid'; 9 | import AMQPAPI from '../../AMQPApi' 10 | import Paper from '@material-ui/core/Paper'; 11 | 12 | const styles = theme => ({ 13 | tableContainer: { 14 | width: '100%', 15 | }, 16 | table: { 17 | width: '100%', 18 | }, 19 | formContainer: { 20 | width: 300, 21 | }, 22 | form: { 23 | marginLeft: theme.spacing.unit * 1, 24 | }, 25 | submit: { 26 | marginTop: theme.spacing.unit * 3, 27 | }, 28 | tableHead: { 29 | fontWeight: 'bold', 30 | } 31 | }); 32 | 33 | class Connections extends React.Component { 34 | constructor() { 35 | super(); 36 | 37 | this.state = { 38 | connections: [], 39 | }; 40 | 41 | this.loadConnections() 42 | } 43 | 44 | loadConnections = () => { 45 | AMQPAPI.get('/connections') 46 | .then(response => this.setState({connections: response.data.items ? response.data.items : []})) 47 | }; 48 | 49 | 50 | tableItems = () => { 51 | const {classes} = this.props; 52 | let rowId = 0; 53 | 54 | return ( 55 | 56 | 57 | 58 | ID 59 | Virtual Host 60 | Name 61 | User name 62 | Protocol 63 | Channels 64 | From client 65 | To client 66 | 67 | 68 | 69 | {this.state.connections.map(row => { 70 | return ( 71 | 72 | {row.id} 73 | {row.vhost} 74 | {row.addr} 75 | {row.user} 76 | {row.protocol} 77 | {row.channels_count} 78 | {this.transformTraffic(row.from_client)} 79 | {this.transformTraffic(row.to_client)} 80 | 81 | ); 82 | })} 83 | 84 |
85 | ) 86 | }; 87 | 88 | transformTraffic = (trackValue) => { 89 | let value = 0; 90 | if (trackValue) { 91 | value = trackValue.value; 92 | } 93 | if (value > 1024 * 1024) { 94 | value = Math.round(value * 100 / 1024 / 1024) / 100; 95 | return value + ' MB/s'; 96 | } else if (value > 1024) { 97 | value = Math.round(value * 100 / 1024) / 100; 98 | return value + ' KB/s'; 99 | } else { 100 | return value + ' B/s'; 101 | } 102 | }; 103 | 104 | render() { 105 | setTimeout(this.loadConnections, 5000); 106 | const {classes} = this.props; 107 | 108 | return ( 109 | 115 | 116 | {this.tableItems()} 117 | 118 | 119 | ) 120 | } 121 | } 122 | 123 | export default withStyles(styles)(Connections); -------------------------------------------------------------------------------- /admin-frontend/src/components/exchanges/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Table from '@material-ui/core/Table'; 3 | import TableBody from '@material-ui/core/TableBody'; 4 | import TableCell from '@material-ui/core/TableCell'; 5 | import TableHead from '@material-ui/core/TableHead'; 6 | import TableRow from '@material-ui/core/TableRow'; 7 | import Checkbox from '@material-ui/core/Checkbox'; 8 | import {withStyles} from '@material-ui/core/styles'; 9 | import Grid from '@material-ui/core/Grid'; 10 | import AMQPAPI from '../../AMQPApi' 11 | import Paper from '@material-ui/core/Paper'; 12 | 13 | const styles = theme => ({ 14 | tableContainer: { 15 | width: '100%', 16 | }, 17 | table: { 18 | width: '100%', 19 | }, 20 | formContainer: { 21 | width: 300, 22 | }, 23 | form: { 24 | marginLeft: theme.spacing.unit * 1, 25 | }, 26 | submit: { 27 | marginTop: theme.spacing.unit * 3, 28 | }, 29 | }); 30 | 31 | class Exchanges extends React.Component { 32 | constructor() { 33 | super(); 34 | 35 | this.state = { 36 | exchanges: [], 37 | }; 38 | 39 | this.loadExchanges() 40 | } 41 | 42 | loadExchanges = () => { 43 | AMQPAPI.get('/exchanges') 44 | .then(response => this.setState({exchanges: response.data.items ? response.data.items : []})) 45 | }; 46 | 47 | tableItems = () => { 48 | const {classes} = this.props; 49 | let rowId = 0; 50 | 51 | return ( 52 | 53 | 54 | 55 | Virtual Host 56 | Name 57 | Type 58 | Durable 59 | Internal 60 | Auto Delete 61 | Msg rate in 62 | Msg rate out 63 | 64 | 65 | 66 | {this.state.exchanges.map(row => { 67 | return ( 68 | 69 | {row.vhost} 70 | {row.name} 71 | {row.type} 72 | 73 | 74 | 75 | {this.transformRate(row.msg_rate_in)} 76 | {this.transformRate(row.msg_rate_out)} 77 | 78 | ); 79 | })} 80 | 81 |
82 | ) 83 | }; 84 | 85 | transformRate = (trackValue) => { 86 | if (!trackValue || !trackValue.value) { 87 | return '' 88 | } 89 | 90 | return trackValue.value + '/s' 91 | }; 92 | 93 | render() { 94 | setTimeout(this.loadExchanges, 5000); 95 | const {classes} = this.props; 96 | 97 | return ( 98 | 104 | 105 | {this.tableItems()} 106 | 107 | 108 | ) 109 | } 110 | } 111 | 112 | export default withStyles(styles)(Exchanges); -------------------------------------------------------------------------------- /admin-frontend/src/components/queues/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Table from '@material-ui/core/Table'; 3 | import TableBody from '@material-ui/core/TableBody'; 4 | import TableCell from '@material-ui/core/TableCell'; 5 | import TableHead from '@material-ui/core/TableHead'; 6 | import TableRow from '@material-ui/core/TableRow'; 7 | import Checkbox from '@material-ui/core/Checkbox'; 8 | import {withStyles} from '@material-ui/core/styles'; 9 | import Grid from '@material-ui/core/Grid'; 10 | import AMQPAPI from '../../AMQPApi' 11 | import Paper from '@material-ui/core/Paper'; 12 | 13 | const styles = theme => ({ 14 | tableContainer: { 15 | width: '100%', 16 | }, 17 | table: { 18 | width: '100%', 19 | }, 20 | formContainer: { 21 | width: 300, 22 | }, 23 | form: { 24 | marginLeft: theme.spacing.unit * 1, 25 | }, 26 | submit: { 27 | marginTop: theme.spacing.unit * 3, 28 | }, 29 | }); 30 | 31 | class Queues extends React.Component { 32 | constructor() { 33 | super(); 34 | 35 | this.state = { 36 | queues: [], 37 | }; 38 | 39 | this.loadQueues() 40 | } 41 | 42 | loadQueues = () => { 43 | AMQPAPI.get('/queues') 44 | .then(response => this.setState({queues: response.data.items ? response.data.items : []})) 45 | }; 46 | 47 | tableItems = () => { 48 | const {classes} = this.props; 49 | let rowId = 0; 50 | 51 | return ( 52 | 53 | 54 | 55 | Virtual Host 56 | Name 57 | D 58 | AD 59 | Excl 60 | Ready 61 | Unacked 62 | Total 63 | Incoming 64 | Deliver 65 | Get 66 | Ack 67 | 68 | 69 | 70 | {this.state.queues.map(row => { 71 | return ( 72 | 73 | {row.vhost} 74 | {row.name} 75 | 76 | 77 | 78 | {row.counters.ready ? row.counters.ready.value : 0} 79 | {row.counters.unacked ? row.counters.unacked.value : 0} 80 | {row.counters.total ? row.counters.total.value : 0} 81 | {this.transformRate(row.counters.incoming)} 82 | {this.transformRate(row.counters.deliver)} 83 | {this.transformRate(row.counters.get)} 84 | {this.transformRate(row.counters.ack)} 85 | 86 | ); 87 | })} 88 | 89 |
90 | ) 91 | }; 92 | 93 | transformRate = (trackValue) => { 94 | if (!trackValue || !trackValue.value) { 95 | return '0/s' 96 | } 97 | 98 | return trackValue.value + '/s' 99 | }; 100 | 101 | render() { 102 | setTimeout(this.loadQueues, 5000); 103 | const {classes} = this.props; 104 | 105 | return ( 106 | 112 | 113 | {this.tableItems()} 114 | 115 | 116 | ) 117 | } 118 | } 119 | 120 | export default withStyles(styles)(Queues); -------------------------------------------------------------------------------- /admin-frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /admin-frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { HashRouter } from 'react-router-dom'; 4 | import './index.css'; 5 | import App from './App'; 6 | import registerServiceWorker from './registerServiceWorker'; 7 | 8 | 9 | ReactDOM.render(( 10 | 11 | 12 | 13 | ), document.getElementById('root')); 14 | registerServiceWorker(); 15 | -------------------------------------------------------------------------------- /admin-frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /admin-frontend/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /admin/handler_bindings.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/valinurovam/garagemq/server" 7 | ) 8 | 9 | type BindingsHandler struct { 10 | amqpServer *server.Server 11 | } 12 | 13 | type BindingsResponse struct { 14 | Items []*Binding `json:"items"` 15 | } 16 | 17 | type Binding struct { 18 | Queue string `json:"queue"` 19 | Exchange string `json:"exchange"` 20 | RoutingKey string `json:"routing_key"` 21 | } 22 | 23 | func NewBindingsHandler(amqpServer *server.Server) http.Handler { 24 | return &BindingsHandler{amqpServer: amqpServer} 25 | } 26 | 27 | func (h *BindingsHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 28 | response := &BindingsResponse{} 29 | req.ParseForm() 30 | vhName := req.Form.Get("vhost") 31 | exName := req.Form.Get("exchange") 32 | 33 | vhost := h.amqpServer.GetVhost(vhName) 34 | if vhost == nil { 35 | JSONResponse(resp, response, 200) 36 | return 37 | } 38 | 39 | exchange := vhost.GetExchange(exName) 40 | if exchange == nil { 41 | JSONResponse(resp, response, 200) 42 | return 43 | } 44 | 45 | for _, bind := range exchange.GetBindings() { 46 | response.Items = append( 47 | response.Items, 48 | &Binding{ 49 | Queue: bind.GetQueue(), 50 | Exchange: bind.GetExchange(), 51 | RoutingKey: bind.GetExchange(), 52 | }, 53 | ) 54 | } 55 | 56 | JSONResponse(resp, response, 200) 57 | } 58 | -------------------------------------------------------------------------------- /admin/handler_channels.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "sort" 7 | 8 | "github.com/valinurovam/garagemq/metrics" 9 | "github.com/valinurovam/garagemq/server" 10 | ) 11 | 12 | type ChannelsHandler struct { 13 | amqpServer *server.Server 14 | } 15 | 16 | type ChannelsResponse struct { 17 | Items []*Channel `json:"items"` 18 | } 19 | 20 | type Channel struct { 21 | ConnID uint64 22 | ChannelID uint16 23 | Channel string `json:"channel"` 24 | Vhost string `json:"vhost"` 25 | User string `json:"user"` 26 | Qos string `json:"qos"` 27 | Confirm bool `json:"confirm"` 28 | 29 | Counters map[string]*metrics.TrackItem `json:"counters"` 30 | } 31 | 32 | func NewChannelsHandler(amqpServer *server.Server) http.Handler { 33 | return &ChannelsHandler{amqpServer: amqpServer} 34 | } 35 | 36 | func (h *ChannelsHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 37 | response := &ChannelsResponse{} 38 | for _, conn := range h.amqpServer.GetConnections() { 39 | for chID, ch := range conn.GetChannels() { 40 | publish := ch.GetMetrics().Publish.Track.GetLastDiffTrackItem() 41 | confirm := ch.GetMetrics().Confirm.Track.GetLastDiffTrackItem() 42 | deliver := ch.GetMetrics().Deliver.Track.GetLastDiffTrackItem() 43 | get := ch.GetMetrics().Get.Track.GetLastDiffTrackItem() 44 | ack := ch.GetMetrics().Acknowledge.Track.GetLastDiffTrackItem() 45 | unacked := ch.GetMetrics().Unacked.Track.GetLastTrackItem() 46 | 47 | response.Items = append( 48 | response.Items, 49 | &Channel{ 50 | ConnID: conn.GetID(), 51 | ChannelID: chID, 52 | Channel: fmt.Sprintf("%s (%d)", conn.GetRemoteAddr().String(), chID), 53 | Vhost: conn.GetVirtualHost().GetName(), 54 | User: conn.GetUsername(), 55 | Qos: fmt.Sprintf("%d / %d", ch.GetQos().PrefetchCount(), ch.GetQos().PrefetchSize()), 56 | Counters: map[string]*metrics.TrackItem{ 57 | "publish": publish, 58 | "confirm": confirm, 59 | "deliver": deliver, 60 | "get": get, 61 | "ack": ack, 62 | "unacked": unacked, 63 | }, 64 | }, 65 | ) 66 | } 67 | } 68 | 69 | sort.Slice( 70 | response.Items, 71 | func(i, j int) bool { 72 | if response.Items[i].ConnID != response.Items[j].ConnID { 73 | return response.Items[i].ConnID > response.Items[j].ConnID 74 | } else { 75 | return response.Items[i].ChannelID > response.Items[j].ChannelID 76 | } 77 | }, 78 | ) 79 | 80 | JSONResponse(resp, response, 200) 81 | } 82 | -------------------------------------------------------------------------------- /admin/handler_connections.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | "sort" 6 | 7 | "github.com/valinurovam/garagemq/metrics" 8 | "github.com/valinurovam/garagemq/server" 9 | ) 10 | 11 | type ConnectionsHandler struct { 12 | amqpServer *server.Server 13 | } 14 | 15 | type ConnectionsResponse struct { 16 | Items []*Connection `json:"items"` 17 | } 18 | 19 | type Connection struct { 20 | ID int `json:"id"` 21 | Vhost string `json:"vhost"` 22 | Addr string `json:"addr"` 23 | ChannelsCount int `json:"channels_count"` 24 | User string `json:"user"` 25 | Protocol string `json:"protocol"` 26 | FromClient *metrics.TrackItem `json:"from_client"` 27 | ToClient *metrics.TrackItem `json:"to_client"` 28 | } 29 | 30 | func NewConnectionsHandler(amqpServer *server.Server) http.Handler { 31 | return &ConnectionsHandler{amqpServer: amqpServer} 32 | } 33 | 34 | func (h *ConnectionsHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 35 | response := &ConnectionsResponse{} 36 | for _, conn := range h.amqpServer.GetConnections() { 37 | response.Items = append( 38 | response.Items, 39 | &Connection{ 40 | ID: int(conn.GetID()), 41 | Vhost: conn.GetVirtualHost().GetName(), 42 | Addr: conn.GetRemoteAddr().String(), 43 | ChannelsCount: len(conn.GetChannels()), 44 | User: conn.GetUsername(), 45 | Protocol: h.amqpServer.GetProtoVersion(), 46 | FromClient: conn.GetMetrics().TrafficIn.Track.GetLastDiffTrackItem(), 47 | ToClient: conn.GetMetrics().TrafficOut.Track.GetLastDiffTrackItem(), 48 | }, 49 | ) 50 | } 51 | 52 | sort.Slice( 53 | response.Items, 54 | func(i, j int) bool { 55 | return response.Items[i].ID > response.Items[j].ID 56 | }, 57 | ) 58 | 59 | JSONResponse(resp, response, 200) 60 | } 61 | -------------------------------------------------------------------------------- /admin/handler_exchanges.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | "sort" 6 | 7 | "github.com/valinurovam/garagemq/metrics" 8 | "github.com/valinurovam/garagemq/server" 9 | ) 10 | 11 | type ExchangesHandler struct { 12 | amqpServer *server.Server 13 | } 14 | 15 | type ExchangesResponse struct { 16 | Items []*Exchange `json:"items"` 17 | } 18 | 19 | type Exchange struct { 20 | Name string `json:"name"` 21 | Vhost string `json:"vhost"` 22 | Type string `json:"type"` 23 | Durable bool `json:"durable"` 24 | Internal bool `json:"internal"` 25 | AutoDelete bool `json:"auto_delete"` 26 | MsgRateIn *metrics.TrackItem `json:"msg_rate_in"` 27 | MsgRateOut *metrics.TrackItem `json:"msg_rate_out"` 28 | } 29 | 30 | func NewExchangesHandler(amqpServer *server.Server) http.Handler { 31 | return &ExchangesHandler{amqpServer: amqpServer} 32 | } 33 | 34 | func (h *ExchangesHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 35 | response := &ExchangesResponse{} 36 | 37 | for vhostName, vhost := range h.amqpServer.GetVhosts() { 38 | for _, exchange := range vhost.GetExchanges() { 39 | name := exchange.GetName() 40 | if name == "" { 41 | name = "(AMQP default)" 42 | } 43 | response.Items = append( 44 | response.Items, 45 | &Exchange{ 46 | Name: name, 47 | Vhost: vhostName, 48 | Durable: exchange.IsDurable(), 49 | Internal: exchange.IsInternal(), 50 | AutoDelete: exchange.IsAutoDelete(), 51 | Type: exchange.GetTypeAlias(), 52 | MsgRateIn: exchange.GetMetrics().MsgIn.Track.GetLastDiffTrackItem(), 53 | MsgRateOut: exchange.GetMetrics().MsgOut.Track.GetLastDiffTrackItem(), 54 | }, 55 | ) 56 | } 57 | } 58 | 59 | sort.Slice(response.Items, func(i, j int) bool { return response.Items[i].Name < response.Items[j].Name }) 60 | JSONResponse(resp, response, 200) 61 | } 62 | -------------------------------------------------------------------------------- /admin/handler_overview.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/valinurovam/garagemq/metrics" 7 | "github.com/valinurovam/garagemq/server" 8 | ) 9 | 10 | type OverviewHandler struct { 11 | amqpServer *server.Server 12 | } 13 | 14 | type OverviewResponse struct { 15 | Metrics []*Metric `json:"metrics"` 16 | Counters map[string]int `json:"counters"` 17 | } 18 | 19 | type Metric struct { 20 | Name string `json:"name"` 21 | Sample []*metrics.TrackItem `json:"sample"` 22 | } 23 | 24 | func NewOverviewHandler(amqpServer *server.Server) http.Handler { 25 | return &OverviewHandler{amqpServer: amqpServer} 26 | } 27 | 28 | func (h *OverviewHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 29 | response := &OverviewResponse{ 30 | Counters: make(map[string]int), 31 | } 32 | h.populateMetrics(response) 33 | h.populateCounters(response) 34 | 35 | JSONResponse(resp, response, 200) 36 | } 37 | 38 | func (h *OverviewHandler) populateMetrics(response *OverviewResponse) { 39 | serverMetrics := h.amqpServer.GetMetrics() 40 | response.Metrics = append(response.Metrics, &Metric{ 41 | Name: "server.publish", 42 | Sample: serverMetrics.Publish.Track.GetDiffTrack(), 43 | }) 44 | response.Metrics = append(response.Metrics, &Metric{ 45 | Name: "server.deliver", 46 | Sample: serverMetrics.Deliver.Track.GetDiffTrack(), 47 | }) 48 | response.Metrics = append(response.Metrics, &Metric{ 49 | Name: "server.confirm", 50 | Sample: serverMetrics.Confirm.Track.GetDiffTrack(), 51 | }) 52 | response.Metrics = append(response.Metrics, &Metric{ 53 | Name: "server.acknowledge", 54 | Sample: serverMetrics.Ack.Track.GetDiffTrack(), 55 | }) 56 | response.Metrics = append(response.Metrics, &Metric{ 57 | Name: "server.traffic_in", 58 | Sample: serverMetrics.TrafficIn.Track.GetDiffTrack(), 59 | }) 60 | response.Metrics = append(response.Metrics, &Metric{ 61 | Name: "server.traffic_out", 62 | Sample: serverMetrics.TrafficOut.Track.GetDiffTrack(), 63 | }) 64 | response.Metrics = append(response.Metrics, &Metric{ 65 | Name: "server.get", 66 | Sample: serverMetrics.Get.Track.GetDiffTrack(), 67 | }) 68 | response.Metrics = append(response.Metrics, &Metric{ 69 | Name: "server.ready", 70 | Sample: serverMetrics.Ready.Track.GetTrack(), 71 | }) 72 | response.Metrics = append(response.Metrics, &Metric{ 73 | Name: "server.unacked", 74 | Sample: serverMetrics.Unacked.Track.GetTrack(), 75 | }) 76 | response.Metrics = append(response.Metrics, &Metric{ 77 | Name: "server.total", 78 | Sample: serverMetrics.Total.Track.GetTrack(), 79 | }) 80 | } 81 | 82 | func (h *OverviewHandler) populateCounters(response *OverviewResponse) { 83 | response.Counters["connections"] = len(h.amqpServer.GetConnections()) 84 | response.Counters["channels"] = 0 85 | response.Counters["exchanges"] = 0 86 | response.Counters["queues"] = 0 87 | response.Counters["consumers"] = 0 88 | 89 | for _, vhost := range h.amqpServer.GetVhosts() { 90 | response.Counters["exchanges"] += len(vhost.GetExchanges()) 91 | response.Counters["queues"] += len(vhost.GetQueues()) 92 | } 93 | 94 | for _, conn := range h.amqpServer.GetConnections() { 95 | response.Counters["channels"] += len(conn.GetChannels()) 96 | 97 | for _, ch := range conn.GetChannels() { 98 | response.Counters["consumers"] += ch.GetConsumersCount() 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /admin/handler_queues.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "net/http" 5 | "sort" 6 | 7 | "github.com/valinurovam/garagemq/metrics" 8 | "github.com/valinurovam/garagemq/server" 9 | ) 10 | 11 | type QueuesHandler struct { 12 | amqpServer *server.Server 13 | } 14 | 15 | type QueuesResponse struct { 16 | Items []*Queue `json:"items"` 17 | } 18 | 19 | type Queue struct { 20 | Name string `json:"name"` 21 | Vhost string `json:"vhost"` 22 | Durable bool `json:"durable"` 23 | AutoDelete bool `json:"auto_delete"` 24 | Exclusive bool `json:"exclusive"` 25 | 26 | Counters map[string]*metrics.TrackItem `json:"counters"` 27 | } 28 | 29 | func NewQueuesHandler(amqpServer *server.Server) http.Handler { 30 | return &QueuesHandler{amqpServer: amqpServer} 31 | } 32 | 33 | func (h *QueuesHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 34 | response := &QueuesResponse{} 35 | for vhostName, vhost := range h.amqpServer.GetVhosts() { 36 | for _, queue := range vhost.GetQueues() { 37 | ready := queue.GetMetrics().Ready.Track.GetLastTrackItem() 38 | total := queue.GetMetrics().Total.Track.GetLastTrackItem() 39 | unacked := queue.GetMetrics().Unacked.Track.GetLastTrackItem() 40 | 41 | incoming := queue.GetMetrics().Incoming.Track.GetLastDiffTrackItem() 42 | deliver := queue.GetMetrics().Deliver.Track.GetLastDiffTrackItem() 43 | get := queue.GetMetrics().Get.Track.GetLastDiffTrackItem() 44 | ack := queue.GetMetrics().Ack.Track.GetLastDiffTrackItem() 45 | 46 | response.Items = append( 47 | response.Items, 48 | &Queue{ 49 | Name: queue.GetName(), 50 | Vhost: vhostName, 51 | Durable: queue.IsDurable(), 52 | AutoDelete: queue.IsAutoDelete(), 53 | Exclusive: queue.IsExclusive(), 54 | Counters: map[string]*metrics.TrackItem{ 55 | "ready": ready, 56 | "total": total, 57 | "unacked": unacked, 58 | 59 | "get": get, 60 | "ack": ack, 61 | "incoming": incoming, 62 | "deliver": deliver, 63 | }, 64 | }, 65 | ) 66 | } 67 | } 68 | 69 | sort.Slice( 70 | response.Items, 71 | func(i, j int) bool { 72 | return response.Items[i].Name > response.Items[j].Name 73 | }, 74 | ) 75 | 76 | JSONResponse(resp, response, 200) 77 | } 78 | -------------------------------------------------------------------------------- /admin/server.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/valinurovam/garagemq/server" 8 | ) 9 | 10 | type AdminServer struct { 11 | s *http.Server 12 | } 13 | 14 | func NewAdminServer(amqpServer *server.Server, host string, port string) *AdminServer { 15 | http.Handle("/", http.FileServer(http.Dir("admin-frontend/build"))) 16 | http.Handle("/overview", NewOverviewHandler(amqpServer)) 17 | http.Handle("/exchanges", NewExchangesHandler(amqpServer)) 18 | http.Handle("/queues", NewQueuesHandler(amqpServer)) 19 | http.Handle("/connections", NewConnectionsHandler(amqpServer)) 20 | http.Handle("/bindings", NewBindingsHandler(amqpServer)) 21 | http.Handle("/channels", NewChannelsHandler(amqpServer)) 22 | 23 | adminServer := &AdminServer{} 24 | adminServer.s = &http.Server{ 25 | Addr: fmt.Sprintf("%s:%s", host, port), 26 | } 27 | 28 | return adminServer 29 | } 30 | 31 | func (server *AdminServer) Start() error { 32 | return server.s.ListenAndServe() 33 | } 34 | -------------------------------------------------------------------------------- /admin/utils.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func JSONResponse(w http.ResponseWriter, data interface{}, code int) (int, error) { 9 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 10 | w.Header().Set("Access-Control-Allow-Origin", "*") 11 | w.WriteHeader(code) 12 | 13 | body, err := json.Marshal(data) 14 | if err != nil { 15 | body = []byte( 16 | `{"error": "Marshal error"}`) 17 | } 18 | 19 | return w.Write(body) 20 | } 21 | -------------------------------------------------------------------------------- /amqp/extended_constants.go: -------------------------------------------------------------------------------- 1 | package amqp 2 | 3 | // NoRoute returns when a 'mandatory' message cannot be delivered to any queue. 4 | // @see https://www.rabbitmq.com/amqp-0-9-1-errata.html#section_17 5 | const NoRoute = 312 6 | -------------------------------------------------------------------------------- /amqp/readers_writers_test.go: -------------------------------------------------------------------------------- 1 | package amqp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestReadFrame_Success(t *testing.T) { 12 | var frameType byte = 1 13 | var channelID uint16 = 1 14 | payload := []byte("some_test_data") 15 | 16 | wr := bytes.NewBuffer(make([]byte, 0)) 17 | // type 18 | binary.Write(wr, binary.BigEndian, frameType) 19 | // channelID 20 | binary.Write(wr, binary.BigEndian, channelID) 21 | // size 22 | binary.Write(wr, binary.BigEndian, uint32(len(payload))) 23 | // payload 24 | binary.Write(wr, binary.BigEndian, payload) 25 | // end 26 | binary.Write(wr, binary.BigEndian, byte(FrameEnd)) 27 | 28 | frame, err := ReadFrame(wr) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | if frame.ChannelID != channelID { 34 | t.Fatalf("Excpected ChannelID %d, actual %d", channelID, frame.ChannelID) 35 | } 36 | 37 | if frame.Type != frameType { 38 | t.Fatalf("Excpected Type %d, actual %d", frameType, frame.Type) 39 | } 40 | 41 | if !bytes.Equal(frame.Payload, payload) { 42 | t.Fatal("Payload not equal test data") 43 | } 44 | } 45 | 46 | func TestReadFrame_Failed_WrongFrameEnd(t *testing.T) { 47 | var frameType byte = 1 48 | var channelID uint16 = 1 49 | payload := []byte("some_test_data") 50 | 51 | wr := bytes.NewBuffer(make([]byte, 0)) 52 | // type 53 | binary.Write(wr, binary.BigEndian, frameType) 54 | // channelID 55 | binary.Write(wr, binary.BigEndian, channelID) 56 | // size 57 | binary.Write(wr, binary.BigEndian, uint32(len(payload))) 58 | // payload 59 | binary.Write(wr, binary.BigEndian, payload) 60 | // end 61 | binary.Write(wr, binary.BigEndian, byte('t')) 62 | 63 | _, err := ReadFrame(wr) 64 | if err == nil { 65 | t.Fatal("Expected error about frame end") 66 | } 67 | } 68 | 69 | func TestWriteFrame(t *testing.T) { 70 | frame := &Frame{ 71 | Type: 1, 72 | ChannelID: 2, 73 | Payload: []byte("some_test_data"), 74 | CloseAfter: false, 75 | } 76 | wr := bytes.NewBuffer(make([]byte, 0)) 77 | err := WriteFrame(wr, frame) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | readFrame, err := ReadFrame(wr) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | if !reflect.DeepEqual(frame, readFrame) { 88 | t.Fatal("Read and write frames not equal") 89 | } 90 | } 91 | 92 | func TestReadOctet(t *testing.T) { 93 | var data byte = 10 94 | r := bytes.NewReader([]byte{data}) 95 | rData, err := ReadOctet(r) 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | if rData != data { 101 | t.Fatalf("Expected %d, actual %d", data, rData) 102 | } 103 | } 104 | 105 | func TestWriteOctet(t *testing.T) { 106 | var data byte = 10 107 | wr := bytes.NewBuffer(make([]byte, 0)) 108 | err := WriteOctet(wr, data) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | if rData, _ := ReadOctet(wr); rData != data { 114 | t.Fatalf("Expected %d, actual %d", data, rData) 115 | } 116 | } 117 | 118 | func TestReadShort(t *testing.T) { 119 | var data uint16 = 10 120 | wr := bytes.NewBuffer(make([]byte, 0)) 121 | binary.Write(wr, binary.BigEndian, data) 122 | rData, err := ReadShort(wr) 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | 127 | if rData != data { 128 | t.Fatalf("Expected %d, actual %d", data, rData) 129 | } 130 | } 131 | 132 | func TestWriteShort(t *testing.T) { 133 | var data uint16 = 10 134 | wr := bytes.NewBuffer(make([]byte, 0)) 135 | err := WriteShort(wr, data) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | 140 | if rData, _ := ReadShort(wr); rData != data { 141 | t.Fatalf("Expected %d, actual %d", data, rData) 142 | } 143 | } 144 | 145 | func TestReadLong(t *testing.T) { 146 | var data uint32 = 10 147 | wr := bytes.NewBuffer(make([]byte, 0)) 148 | binary.Write(wr, binary.BigEndian, data) 149 | rData, err := ReadLong(wr) 150 | if err != nil { 151 | t.Fatal(err) 152 | } 153 | 154 | if rData != data { 155 | t.Fatalf("Expected %d, actual %d", data, rData) 156 | } 157 | } 158 | 159 | func TestWriteLong(t *testing.T) { 160 | var data uint32 = 10 161 | wr := bytes.NewBuffer(make([]byte, 0)) 162 | err := WriteLong(wr, data) 163 | if err != nil { 164 | t.Fatal(err) 165 | } 166 | 167 | if rData, _ := ReadLong(wr); rData != data { 168 | t.Fatalf("Expected %d, actual %d", data, rData) 169 | } 170 | } 171 | 172 | func TestReadLonglong(t *testing.T) { 173 | var data uint64 = 10 174 | wr := bytes.NewBuffer(make([]byte, 0)) 175 | binary.Write(wr, binary.BigEndian, data) 176 | rData, err := ReadLonglong(wr) 177 | if err != nil { 178 | t.Fatal(err) 179 | } 180 | 181 | if rData != data { 182 | t.Fatalf("Expected %d, actual %d", data, rData) 183 | } 184 | } 185 | 186 | func TestWriteLonglong(t *testing.T) { 187 | var data uint64 = 10 188 | wr := bytes.NewBuffer(make([]byte, 0)) 189 | err := WriteLonglong(wr, data) 190 | if err != nil { 191 | t.Fatal(err) 192 | } 193 | 194 | if rData, _ := ReadLonglong(wr); rData != data { 195 | t.Fatalf("Expected %d, actual %d", data, rData) 196 | } 197 | } 198 | 199 | func TestReadShortstr(t *testing.T) { 200 | var data = "someteststring" 201 | wr := bytes.NewBuffer(make([]byte, 0)) 202 | binary.Write(wr, binary.BigEndian, byte(len(data))) 203 | binary.Write(wr, binary.BigEndian, []byte(data)) 204 | rData, err := ReadShortstr(wr) 205 | if err != nil { 206 | t.Fatal(err) 207 | } 208 | 209 | if rData != data { 210 | t.Fatalf("Expected '%s', actual '%s'", data, rData) 211 | } 212 | } 213 | 214 | func TestWriteShortstr(t *testing.T) { 215 | var data = "someteststring" 216 | wr := bytes.NewBuffer(make([]byte, 0)) 217 | err := WriteShortstr(wr, data) 218 | if err != nil { 219 | t.Fatal(err) 220 | } 221 | 222 | if rData, _ := ReadShortstr(wr); rData != data { 223 | t.Fatalf("Expected '%s', actual '%s'", data, rData) 224 | } 225 | } 226 | 227 | func TestReadLongstr(t *testing.T) { 228 | var data = []byte("someteststring") 229 | wr := bytes.NewBuffer(make([]byte, 0)) 230 | binary.Write(wr, binary.BigEndian, uint32(len(data))) 231 | binary.Write(wr, binary.BigEndian, data) 232 | rData, err := ReadLongstr(wr) 233 | if err != nil { 234 | t.Fatal(err) 235 | } 236 | 237 | if !bytes.Equal(rData, data) { 238 | t.Fatalf("Expected '%v', actual '%v'", data, rData) 239 | } 240 | } 241 | 242 | func TestWriteLongstr(t *testing.T) { 243 | var data = []byte("someteststring") 244 | wr := bytes.NewBuffer(make([]byte, 0)) 245 | err := WriteLongstr(wr, data) 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | 250 | if rData, _ := ReadLongstr(wr); !bytes.Equal(rData, data) { 251 | t.Fatalf("Expected '%v', actual '%v'", data, rData) 252 | } 253 | } 254 | 255 | /* 256 | Rabbitmq table fields 257 | 258 | 't' bool boolean 259 | 'b' int8 short-short-int 260 | 's' int16 short-int 261 | 'I' int32 long-int 262 | 'l' int64 long-long-int 263 | 'f' float float 264 | 'd' double double 265 | 'D' Decimal decimal-value 266 | 'S' []byte long-string 267 | 'T' time.Time timestamp 268 | 'F' Table field-table 269 | 'V' nil no-field 270 | 'x' []interface{} field-array 271 | */ 272 | func TestReadWriteTable(t *testing.T) { 273 | 274 | table := Table{} 275 | table["bool_true"] = true 276 | table["bool_false"] = false 277 | table["int8"] = int8(16) 278 | table["uint8"] = uint8(16) 279 | table["int16"] = int16(16) 280 | table["uint16"] = uint16(16) 281 | table["int32"] = int32(32) 282 | table["uint32"] = uint32(32) 283 | table["int64"] = int64(64) 284 | table["uint64"] = uint64(64) 285 | table["float32"] = float32(32.32) 286 | table["float64"] = float64(64.64) 287 | table["decimal"] = Decimal{Scale: 1, Value: 10} 288 | table["byte_array"] = []byte{'a', 'r', 'r', 'a', 'y'} 289 | table["string"] = "string" 290 | table["time"] = time.Now() 291 | table["array_of_data"] = []interface{}{int8(16), Decimal{Scale: 1, Value: 10}, "string"} 292 | table["nil"] = nil 293 | table["table"] = Table{} 294 | 295 | wr := bytes.NewBuffer(make([]byte, 0)) 296 | err := WriteTable(wr, &table, ProtoRabbit) 297 | if err != nil { 298 | t.Fatal(err) 299 | } 300 | 301 | err = WriteTable(wr, &table, Proto091) 302 | if err != nil { 303 | t.Fatal(err) 304 | } 305 | 306 | _, err = ReadTable(wr, ProtoRabbit) 307 | if err != nil { 308 | t.Fatal(err) 309 | } 310 | 311 | _, err = ReadTable(wr, Proto091) 312 | if err != nil { 313 | t.Fatal(err) 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /amqp/types.go: -------------------------------------------------------------------------------- 1 | package amqp 2 | 3 | import ( 4 | "bytes" 5 | "sync/atomic" 6 | "time" 7 | 8 | "github.com/valinurovam/garagemq/pool" 9 | ) 10 | 11 | var emptyMessageBufferPool = pool.NewBufferPool(0) 12 | 13 | // Table - simple amqp-table implementation 14 | type Table map[string]interface{} 15 | 16 | // Decimal represents amqp-decimal data 17 | type Decimal struct { 18 | Scale uint8 19 | Value int32 20 | } 21 | 22 | // Frame is raw frame 23 | type Frame struct { 24 | ChannelID uint16 25 | Type byte 26 | CloseAfter bool 27 | Sync bool 28 | Payload []byte 29 | } 30 | 31 | // ContentHeader represents amqp-message content-header 32 | type ContentHeader struct { 33 | BodySize uint64 34 | ClassID uint16 35 | Weight uint16 36 | propertyFlags uint16 37 | PropertyList *BasicPropertyList 38 | } 39 | 40 | // ConfirmMeta store information for check confirms and send confirm-acks 41 | type ConfirmMeta struct { 42 | ChanID uint16 43 | ConnID uint64 44 | DeliveryTag uint64 45 | ExpectedConfirms int 46 | ActualConfirms int 47 | } 48 | 49 | // CanConfirm returns is message can be confirmed 50 | func (meta *ConfirmMeta) CanConfirm() bool { 51 | return meta.ActualConfirms == meta.ExpectedConfirms 52 | } 53 | 54 | // Message represents amqp-message and meta-data 55 | type Message struct { 56 | ID uint64 57 | BodySize uint64 58 | DeliveryCount uint32 59 | Mandatory bool 60 | Immediate bool 61 | Exchange string 62 | RoutingKey string 63 | ConfirmMeta *ConfirmMeta 64 | Header *ContentHeader 65 | Body []*Frame 66 | } 67 | 68 | // when server restart we can't start again count messages from 0 69 | var msgID = uint64(time.Now().UnixNano()) 70 | 71 | // NewMessage returns new message instance 72 | func NewMessage(method *BasicPublish) *Message { 73 | return &Message{ 74 | Exchange: method.Exchange, 75 | RoutingKey: method.RoutingKey, 76 | Mandatory: method.Mandatory, 77 | Immediate: method.Immediate, 78 | BodySize: 0, 79 | DeliveryCount: 0, 80 | } 81 | } 82 | 83 | // IsPersistent check if message should be persisted 84 | func (m *Message) IsPersistent() bool { 85 | deliveryMode := m.Header.PropertyList.DeliveryMode 86 | return deliveryMode != nil && *deliveryMode == 2 87 | } 88 | 89 | // GenerateSeq returns next message ID 90 | func (m *Message) GenerateSeq() { 91 | if m.ID == 0 { 92 | m.ID = atomic.AddUint64(&msgID, 1) 93 | } 94 | } 95 | 96 | // Append appends new body-frame into message and increase bodySize 97 | func (m *Message) Append(body *Frame) { 98 | m.Body = append(m.Body, body) 99 | m.BodySize += uint64(len(body.Payload)) 100 | } 101 | 102 | // Marshal converts message into bytes to store into db 103 | func (m *Message) Marshal(protoVersion string) (data []byte, err error) { 104 | buffer := emptyMessageBufferPool.Get() 105 | defer emptyMessageBufferPool.Put(buffer) 106 | 107 | if err = WriteLonglong(buffer, m.ID); err != nil { 108 | return nil, err 109 | } 110 | 111 | if err = WriteContentHeader(buffer, m.Header, protoVersion); err != nil { 112 | return nil, err 113 | } 114 | if err = WriteShortstr(buffer, m.Exchange); err != nil { 115 | return nil, err 116 | } 117 | if err = WriteShortstr(buffer, m.RoutingKey); err != nil { 118 | return nil, err 119 | } 120 | 121 | for _, frame := range m.Body { 122 | if err = WriteFrame(buffer, frame); err != nil { 123 | return nil, err 124 | } 125 | } 126 | 127 | data = make([]byte, buffer.Len()) 128 | copy(data, buffer.Bytes()) 129 | return 130 | } 131 | 132 | // Unmarshal restore message entity from bytes 133 | func (m *Message) Unmarshal(buffer []byte, protoVersion string) (err error) { 134 | reader := bytes.NewReader(buffer) 135 | if m.ID, err = ReadLonglong(reader); err != nil { 136 | return err 137 | } 138 | 139 | if m.Header, err = ReadContentHeader(reader, protoVersion); err != nil { 140 | return err 141 | } 142 | if m.Exchange, err = ReadShortstr(reader); err != nil { 143 | return err 144 | } 145 | if m.RoutingKey, err = ReadShortstr(reader); err != nil { 146 | return err 147 | } 148 | 149 | for m.BodySize < m.Header.BodySize { 150 | body, errFrame := ReadFrame(reader) 151 | if errFrame != nil { 152 | return errFrame 153 | } 154 | m.Append(body) 155 | } 156 | 157 | return nil 158 | } 159 | 160 | // Constants to detect connection or channel error thrown 161 | const ( 162 | ErrorOnConnection = iota 163 | ErrorOnChannel 164 | ) 165 | 166 | // Error represents AMQP-error data 167 | type Error struct { 168 | ReplyCode uint16 169 | ReplyText string 170 | ClassID uint16 171 | MethodID uint16 172 | ErrorType int 173 | } 174 | 175 | // NewConnectionError returns new connection error. If caused - connection should be closed 176 | func NewConnectionError(code uint16, text string, classID uint16, methodID uint16) *Error { 177 | err := &Error{ 178 | ReplyCode: code, 179 | ReplyText: ConstantsNameMap[code] + " - " + text, 180 | ClassID: classID, 181 | MethodID: methodID, 182 | ErrorType: ErrorOnConnection, 183 | } 184 | 185 | return err 186 | } 187 | 188 | // NewChannelError returns new channel error& If caused - channel should be closed 189 | func NewChannelError(code uint16, text string, classID uint16, methodID uint16) *Error { 190 | err := &Error{ 191 | ReplyCode: code, 192 | ReplyText: ConstantsNameMap[code] + " - " + text, 193 | ClassID: classID, 194 | MethodID: methodID, 195 | ErrorType: ErrorOnChannel, 196 | } 197 | 198 | return err 199 | } 200 | -------------------------------------------------------------------------------- /amqp/types_test.go: -------------------------------------------------------------------------------- 1 | package amqp 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestNewMessage(t *testing.T) { 9 | method := &BasicPublish{ 10 | Exchange: "ex", 11 | RoutingKey: "rk", 12 | Mandatory: false, 13 | Immediate: true, 14 | } 15 | 16 | message := NewMessage(method) 17 | 18 | if message.Exchange != method.Exchange { 19 | t.Fatalf("Expected Exchange %s, actual %s", method.Exchange, message.Exchange) 20 | } 21 | 22 | if message.RoutingKey != method.RoutingKey { 23 | t.Fatalf("Expected RoutingKey %s, actual %s", method.RoutingKey, message.RoutingKey) 24 | } 25 | 26 | if message.Mandatory != method.Mandatory { 27 | t.Fatalf("Expected Mandatory %t, actual %t", method.Mandatory, message.Mandatory) 28 | } 29 | 30 | if message.Immediate != method.Immediate { 31 | t.Fatalf("Expected Immediate %t, actual %t", method.Immediate, message.Immediate) 32 | } 33 | } 34 | 35 | func TestMessage_Append(t *testing.T) { 36 | m := &Message{ 37 | BodySize: 0, 38 | Body: make([]*Frame, 0), 39 | } 40 | 41 | m.Append(&Frame{ 42 | Type: 0, 43 | ChannelID: 0, 44 | Payload: []byte{'t', 'e', 's', 't'}, 45 | CloseAfter: false, 46 | }) 47 | 48 | if m.BodySize != 4 { 49 | t.Fatalf("Expected BodySize %d, actual %d", 4, m.BodySize) 50 | } 51 | 52 | m.Append(&Frame{ 53 | Type: 0, 54 | ChannelID: 0, 55 | Payload: []byte{'t', 'e', 's', 't'}, 56 | CloseAfter: false, 57 | }) 58 | 59 | if m.BodySize != 8 { 60 | t.Fatalf("Expected BodySize %d, actual %d", 8, m.BodySize) 61 | } 62 | 63 | if len(m.Body) != 2 { 64 | t.Fatalf("Expected Body len %d, actual %d", 2, len(m.Body)) 65 | } 66 | } 67 | 68 | func TestMessage_Marshal_Unmarshal(t *testing.T) { 69 | ctype := "text/plain" 70 | 71 | mM := &Message{ 72 | ID: 1, 73 | Header: &ContentHeader{ 74 | ClassID: ClassBasic, 75 | Weight: 0, 76 | BodySize: 4, 77 | propertyFlags: 32768, 78 | PropertyList: &BasicPropertyList{ 79 | ContentType: &ctype, 80 | ContentEncoding: nil, 81 | Headers: nil, 82 | DeliveryMode: nil, 83 | Priority: nil, 84 | CorrelationID: nil, 85 | ReplyTo: nil, 86 | Expiration: nil, 87 | MessageID: nil, 88 | Timestamp: nil, 89 | Type: nil, 90 | UserID: nil, 91 | AppID: nil, 92 | Reserved: nil, 93 | }, 94 | }, 95 | Exchange: "", 96 | RoutingKey: "test", 97 | Mandatory: false, 98 | Immediate: false, 99 | BodySize: 4, 100 | Body: []*Frame{ 101 | { 102 | Type: 3, 103 | ChannelID: 1, 104 | Payload: []byte{'t', 'e', 's', 't'}, 105 | CloseAfter: false, 106 | }, 107 | }, 108 | } 109 | 110 | bytes, err := mM.Marshal(ProtoRabbit) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | 115 | mU := &Message{} 116 | mU.Unmarshal(bytes, ProtoRabbit) 117 | if !reflect.DeepEqual(mM, mU) { 118 | t.Fatalf("Marshaled and unmarshaled structures not equal") 119 | } 120 | } 121 | 122 | func TestMessage_IsPersistent(t *testing.T) { 123 | var dMode byte = 2 124 | message := &Message{ 125 | ID: 1, 126 | Header: &ContentHeader{ 127 | ClassID: ClassBasic, 128 | Weight: 0, 129 | BodySize: 4, 130 | propertyFlags: 32768, 131 | PropertyList: &BasicPropertyList{ 132 | DeliveryMode: &dMode, 133 | }, 134 | }, 135 | } 136 | if !message.IsPersistent() { 137 | t.Fatalf("Expected persistent message") 138 | } 139 | } 140 | 141 | func TestConfirmMeta_CanConfirm(t *testing.T) { 142 | meta := &ConfirmMeta{ 143 | ExpectedConfirms: 5, 144 | ActualConfirms: 5, 145 | } 146 | 147 | if !meta.CanConfirm() { 148 | t.Fatalf("Expected CanConfirm true") 149 | } 150 | } 151 | 152 | func TestNewChannelError(t *testing.T) { 153 | er := NewChannelError(PreconditionFailed, "text", 0, 0) 154 | 155 | if er.ErrorType != ErrorOnChannel { 156 | t.Fatal("Expected channel error") 157 | } 158 | } 159 | 160 | func TestNewConnectionError(t *testing.T) { 161 | er := NewConnectionError(PreconditionFailed, "text", 0, 0) 162 | 163 | if er.ErrorType != ErrorOnConnection { 164 | t.Fatal("Expected connection error") 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | 10 | "golang.org/x/crypto/bcrypt" 11 | ) 12 | 13 | const ( 14 | // SaslPlain method 15 | SaslPlain = "PLAIN" 16 | authMD5 = "md5" 17 | authPlain = "plain" 18 | authBcrypt = "bcrypt" 19 | ) 20 | 21 | // SaslData represents standard SASL properties 22 | type SaslData struct { 23 | Identity string 24 | Username string 25 | Password string 26 | } 27 | 28 | // ParsePlain check and parse SASL-raw data and return SaslData structure 29 | func ParsePlain(response []byte) (SaslData, error) { 30 | parts := bytes.Split(response, []byte{0}) 31 | if len(parts) != 3 { 32 | return SaslData{}, errors.New("unable to parse PLAIN SALS response") 33 | } 34 | 35 | saslData := SaslData{} 36 | saslData.Identity = string(parts[0]) 37 | saslData.Username = string(parts[1]) 38 | saslData.Password = string(parts[2]) 39 | 40 | return saslData, nil 41 | } 42 | 43 | // HashPassword hash raw password and return hash for check 44 | func HashPassword(password string, authType string) (string, error) { 45 | switch authType { 46 | case authMD5: 47 | h := md5.New() 48 | // digest.Write never return any error, so skip error check 49 | h.Write([]byte(password)) 50 | return hex.EncodeToString(h.Sum(nil)), nil 51 | case authBcrypt: 52 | hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 53 | return string(hash), err 54 | case authPlain: 55 | return password, nil 56 | default: 57 | return "", fmt.Errorf("unknown auth type %s", authType) 58 | } 59 | } 60 | 61 | // CheckPasswordHash check given password and hash 62 | func CheckPasswordHash(password, hash string, authType string) bool { 63 | switch authType { 64 | case authMD5: 65 | h := md5.New() 66 | // digest.Write never return any error, so skip error check 67 | h.Write([]byte(password)) 68 | return hash == hex.EncodeToString(h.Sum(nil)) 69 | case authBcrypt: 70 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 71 | return err == nil 72 | case authPlain: 73 | return password == hash 74 | default: 75 | return false 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /auth/auth_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "testing" 4 | 5 | func TestParsePlain_Success(t *testing.T) { 6 | data := []byte{'t', 'e', 's', 't', 'i', 0, 't', 'e', 's', 't', 'u', 0, 't', 'e', 's', 't', 'p'} 7 | sasl, err := ParsePlain(data) 8 | if err != nil { 9 | t.Fatal(err) 10 | } 11 | 12 | if sasl.Identity != "testi" { 13 | t.Fatalf("identity expected %s, actual %s", "testi", sasl.Identity) 14 | } 15 | 16 | if sasl.Password != "testp" { 17 | t.Fatalf("password expected %s, actual %s", "testi", sasl.Password) 18 | } 19 | 20 | if sasl.Username != "testu" { 21 | t.Fatalf("username expected %s, actual %s", "testi", sasl.Username) 22 | } 23 | } 24 | 25 | func TestParsePlain_Failed_WrongFormat(t *testing.T) { 26 | data := []byte{'t', 'e', 's', 't', 'i', 0, 't', 'e', 's', 't', 'u', 't', 'e', 's', 't', 'p'} 27 | _, err := ParsePlain(data) 28 | if err == nil { 29 | t.Fatal("Expected parse error, actual nil") 30 | } 31 | } 32 | 33 | func TestHashPassword_Failed(t *testing.T) { 34 | password := t.Name() 35 | _, err := HashPassword(password, t.Name()) 36 | if err == nil { 37 | t.Fatal("Expected error about auth type") 38 | } 39 | } 40 | 41 | func TestCheckPasswordHash_Bcrypt(t *testing.T) { 42 | password := t.Name() 43 | hash, err := HashPassword(password, authBcrypt) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | if !CheckPasswordHash(password, hash, authBcrypt) { 49 | t.Fatal("Expected true on check password") 50 | } 51 | 52 | if CheckPasswordHash("tEsTpAsSwOrD", hash, authBcrypt) { 53 | t.Fatal("Expected false on check password") 54 | } 55 | } 56 | 57 | func TestCheckPasswordHash_MD5(t *testing.T) { 58 | password := t.Name() 59 | hash, err := HashPassword(password, authMD5) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | if !CheckPasswordHash(password, hash, authMD5) { 65 | t.Fatal("Expected true on check password") 66 | } 67 | 68 | if CheckPasswordHash("tEsTpAsSwOrD", hash, authMD5) { 69 | t.Fatal("Expected false on check password") 70 | } 71 | } 72 | 73 | func TestCheckPasswordHash_Plain(t *testing.T) { 74 | password := t.Name() 75 | hash, err := HashPassword(password, authPlain) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | if !CheckPasswordHash(password, hash, authPlain) { 81 | t.Fatal("Expected true on check password") 82 | } 83 | 84 | if CheckPasswordHash("tEsTpAsSwOrD", hash, authMD5) { 85 | t.Fatal("Expected false on check password") 86 | } 87 | } 88 | 89 | func TestCheckFailed(t *testing.T) { 90 | password := t.Name() 91 | hash, err := HashPassword(password, authPlain) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | if CheckPasswordHash(password, hash, "wrong type") { 97 | t.Fatal("Expected false on check password with wrong type") 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /bin/.gitignore: -------------------------------------------------------------------------------- 1 | cmd/server 2 | db/ 3 | node_modules 4 | .idea 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "gopkg.in/yaml.v2" 7 | ) 8 | 9 | const ( 10 | DbEngineTypeBadger string = "badger" 11 | DbEngineTypeBuntDb string = "buntdb" 12 | 13 | DbPathMemory string = ":memory:" 14 | ) 15 | 16 | // Config represents server changeable se 17 | type Config struct { 18 | Proto string 19 | Users []User 20 | TCP TCPConfig 21 | Queue Queue 22 | Db Db 23 | Vhost Vhost 24 | Security Security 25 | Connection Connection 26 | Admin AdminConfig 27 | } 28 | 29 | // User for auth check 30 | type User struct { 31 | Username string 32 | Password string 33 | } 34 | 35 | // TCPConfig represents properties for tune network connections 36 | type TCPConfig struct { 37 | IP string `yaml:"ip"` 38 | Port string 39 | Nodelay bool 40 | ReadBufSize int `yaml:"readBufSize"` 41 | WriteBufSize int `yaml:"writeBufSize"` 42 | } 43 | 44 | // AdminConfig represents properties for admin server 45 | type AdminConfig struct { 46 | IP string `yaml:"ip"` 47 | Port string 48 | } 49 | 50 | // Queue settings 51 | type Queue struct { 52 | ShardSize int `yaml:"shardSize"` 53 | MaxMessagesInRAM uint64 `yaml:"maxMessagesInRam"` 54 | } 55 | 56 | // Db settings, such as path to load/save and engine 57 | type Db struct { 58 | DefaultPath string `yaml:"defaultPath"` 59 | Engine string `yaml:"engine"` 60 | } 61 | 62 | // Vhost settings 63 | type Vhost struct { 64 | DefaultPath string `yaml:"defaultPath"` 65 | } 66 | 67 | // Security settings 68 | type Security struct { 69 | PasswordCheck string `yaml:"passwordCheck"` 70 | } 71 | 72 | // Connection settings for AMQP-connection 73 | type Connection struct { 74 | ChannelsMax uint16 `yaml:"channelsMax"` 75 | FrameMaxSize uint32 `yaml:"frameMaxSize"` 76 | } 77 | 78 | // CreateFromFile creates config from file 79 | func CreateFromFile(path string) (*Config, error) { 80 | cfg := &Config{} 81 | file, err := ioutil.ReadFile(path) 82 | if err != nil { 83 | return nil, err 84 | } 85 | err = yaml.Unmarshal(file, &cfg) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return cfg, nil 91 | } 92 | 93 | // CreateDefault creates config from default values 94 | func CreateDefault() (*Config, error) { 95 | return defaultConfig(), nil 96 | } 97 | -------------------------------------------------------------------------------- /config/default.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | dbBuntDB = "buntdb" 5 | dbBadger = "badger" 6 | ) 7 | 8 | func defaultConfig() *Config { 9 | return &Config{ 10 | Proto: "amqp-rabbit", 11 | Users: []User{ 12 | { 13 | Username: "guest", 14 | Password: "084e0343a0486ff05530df6c705c8bb4", // guest md5 hash 15 | }, 16 | }, 17 | TCP: TCPConfig{ 18 | IP: "0.0.0.0", 19 | Port: "5672", 20 | Nodelay: false, 21 | ReadBufSize: 128 << 10, // 128Kb 22 | WriteBufSize: 128 << 10, // 128Kb 23 | }, 24 | Admin: AdminConfig{ 25 | IP: "0.0.0.0", 26 | Port: "15672", 27 | }, 28 | Queue: Queue{ 29 | ShardSize: 8 << 10, // 8k 30 | MaxMessagesInRAM: 10 * 8 << 10, // 10 buckets 31 | }, 32 | Db: Db{ 33 | DefaultPath: "db", 34 | Engine: dbBadger, 35 | }, 36 | Vhost: Vhost{ 37 | DefaultPath: "/", 38 | }, 39 | Security: Security{ 40 | PasswordCheck: "md5", 41 | }, 42 | Connection: Connection{ 43 | ChannelsMax: 4096, 44 | FrameMaxSize: 65536, 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /consumer/consumer.go: -------------------------------------------------------------------------------- 1 | package consumer 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | "time" 7 | 8 | "github.com/sasha-s/go-deadlock" 9 | 10 | "github.com/valinurovam/garagemq/amqp" 11 | "github.com/valinurovam/garagemq/interfaces" 12 | "github.com/valinurovam/garagemq/qos" 13 | "github.com/valinurovam/garagemq/queue" 14 | ) 15 | 16 | const ( 17 | started = iota 18 | stopped 19 | paused 20 | ) 21 | 22 | var cid uint64 23 | 24 | // Consumer implements AMQP consumer 25 | type Consumer struct { 26 | ID uint64 27 | Queue string 28 | ConsumerTag string 29 | noAck bool 30 | channel interfaces.Channel 31 | queue *queue.Queue 32 | statusLock deadlock.RWMutex 33 | status int 34 | qos []*qos.AmqpQos 35 | consume chan struct{} 36 | } 37 | 38 | // NewConsumer returns new instance of Consumer 39 | func NewConsumer(queueName string, consumerTag string, noAck bool, channel interfaces.Channel, queue *queue.Queue, qos []*qos.AmqpQos) *Consumer { 40 | id := atomic.AddUint64(&cid, 1) 41 | if consumerTag == "" { 42 | consumerTag = generateTag(id) 43 | } 44 | return &Consumer{ 45 | ID: id, 46 | Queue: queueName, 47 | ConsumerTag: consumerTag, 48 | noAck: noAck, 49 | channel: channel, 50 | queue: queue, 51 | qos: qos, 52 | consume: make(chan struct{}, 1), 53 | } 54 | } 55 | 56 | func generateTag(id uint64) string { 57 | return fmt.Sprintf("%d_%d", time.Now().Unix(), id) 58 | } 59 | 60 | // Start starting consumer to fetch messages from queue 61 | func (consumer *Consumer) Start() { 62 | consumer.status = started 63 | go consumer.startConsume() 64 | consumer.Consume() 65 | } 66 | 67 | // startConsume waiting a signal from consume channel and try to pop message from queue 68 | // if not set noAck consumer pop message with qos rules and add message to unacked message queue 69 | func (consumer *Consumer) startConsume() { 70 | for range consumer.consume { 71 | consumer.retrieveAndSendMessage() 72 | } 73 | } 74 | 75 | func (consumer *Consumer) retrieveAndSendMessage() { 76 | var message *amqp.Message 77 | consumer.statusLock.RLock() 78 | defer consumer.statusLock.RUnlock() 79 | if consumer.status == stopped { 80 | return 81 | } 82 | 83 | if consumer.noAck { 84 | message = consumer.queue.Pop() 85 | } else { 86 | message = consumer.queue.PopQos(consumer.qos) 87 | } 88 | 89 | if message == nil { 90 | return 91 | } 92 | 93 | if consumer.noAck { 94 | consumer.queue.AckMsg(message) 95 | } 96 | 97 | dTag := consumer.channel.NextDeliveryTag() 98 | if !consumer.noAck { 99 | consumer.channel.AddUnackedMessage(dTag, consumer.ConsumerTag, consumer.queue.GetName(), message) 100 | } 101 | 102 | // handle metrics 103 | if consumer.noAck { 104 | consumer.queue.GetMetrics().Total.Counter.Dec(1) 105 | consumer.queue.GetMetrics().ServerTotal.Counter.Dec(1) 106 | } else { 107 | consumer.queue.GetMetrics().Unacked.Counter.Inc(1) 108 | consumer.queue.GetMetrics().ServerUnacked.Counter.Inc(1) 109 | } 110 | 111 | consumer.queue.GetMetrics().Ready.Counter.Dec(1) 112 | consumer.queue.GetMetrics().ServerReady.Counter.Dec(1) 113 | 114 | consumer.channel.SendContent(&amqp.BasicDeliver{ 115 | ConsumerTag: consumer.ConsumerTag, 116 | DeliveryTag: dTag, 117 | Redelivered: message.DeliveryCount > 1, 118 | Exchange: message.Exchange, 119 | RoutingKey: message.RoutingKey, 120 | }, message) 121 | 122 | consumer.queue.GetMetrics().Deliver.Counter.Inc(1) 123 | consumer.queue.GetMetrics().ServerDeliver.Counter.Inc(1) 124 | 125 | consumer.consumeMsg() 126 | 127 | return 128 | } 129 | 130 | // Pause pause consumer, used by channel.flow change 131 | func (consumer *Consumer) Pause() { 132 | consumer.statusLock.Lock() 133 | defer consumer.statusLock.Unlock() 134 | consumer.status = paused 135 | } 136 | 137 | // UnPause unpause consumer, used by channel.flow change 138 | func (consumer *Consumer) UnPause() { 139 | consumer.statusLock.Lock() 140 | defer consumer.statusLock.Unlock() 141 | consumer.status = started 142 | } 143 | 144 | // Consume send signal into consumer channel, than consumer can try to pop message from queue 145 | func (consumer *Consumer) Consume() bool { 146 | consumer.statusLock.RLock() 147 | defer consumer.statusLock.RUnlock() 148 | 149 | return consumer.consumeMsg() 150 | } 151 | 152 | func (consumer *Consumer) consumeMsg() bool { 153 | if consumer.status == stopped || consumer.status == paused { 154 | return false 155 | } 156 | 157 | select { 158 | case consumer.consume <- struct{}{}: 159 | return true 160 | default: 161 | return false 162 | } 163 | } 164 | 165 | // Stop stops consumer and remove it from queue consumers list 166 | func (consumer *Consumer) Stop() { 167 | consumer.statusLock.Lock() 168 | if consumer.status == stopped { 169 | consumer.statusLock.Unlock() 170 | return 171 | } 172 | consumer.status = stopped 173 | consumer.statusLock.Unlock() 174 | consumer.queue.RemoveConsumer(consumer.ConsumerTag) 175 | close(consumer.consume) 176 | } 177 | 178 | // Cancel stops consumer and send basic.cancel method to the client 179 | func (consumer *Consumer) Cancel() { 180 | consumer.Stop() 181 | consumer.channel.SendMethod(&amqp.BasicCancel{ConsumerTag: consumer.ConsumerTag, NoWait: true}) 182 | } 183 | 184 | // Tag returns consumer tag 185 | func (consumer *Consumer) Tag() string { 186 | return consumer.ConsumerTag 187 | } 188 | 189 | // Qos returns consumer qos rules 190 | func (consumer *Consumer) Qos() []*qos.AmqpQos { 191 | return consumer.qos 192 | } 193 | -------------------------------------------------------------------------------- /etc/config.yaml: -------------------------------------------------------------------------------- 1 | proto: amqp-rabbit 2 | users: 3 | - username: guest 4 | password: 084e0343a0486ff05530df6c705c8bb4 # guest md5 5 | tcp: 6 | ip: 0.0.0.0 7 | port: 5672 8 | nodelay: false 9 | readBufSize: 196608 10 | writeBufSize: 196608 11 | admin: 12 | ip: 0.0.0.0 13 | port: 15672 14 | queue: 15 | shardSize: 8192 16 | maxMessagesInRam: 131072 17 | db: 18 | defaultPath: db 19 | engine: badger 20 | vhost: 21 | defaultPath: / 22 | security: 23 | passwordCheck: md5 24 | connection: 25 | channelsMax: 4096 26 | frameMaxSize: 65536 -------------------------------------------------------------------------------- /exchange/exchange.go: -------------------------------------------------------------------------------- 1 | package exchange 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/sasha-s/go-deadlock" 8 | 9 | "github.com/valinurovam/garagemq/amqp" 10 | "github.com/valinurovam/garagemq/binding" 11 | "github.com/valinurovam/garagemq/metrics" 12 | ) 13 | 14 | // available exchange types 15 | const ( 16 | ExTypeDirect = iota + 1 17 | ExTypeFanout 18 | ExTypeTopic 19 | ExTypeHeaders 20 | ) 21 | 22 | var exchangeTypeIDAliasMap = map[byte]string{ 23 | ExTypeDirect: "direct", 24 | ExTypeFanout: "fanout", 25 | ExTypeTopic: "topic", 26 | ExTypeHeaders: "headers", 27 | } 28 | 29 | var exchangeTypeAliasIDMap = map[string]byte{ 30 | "direct": ExTypeDirect, 31 | "fanout": ExTypeFanout, 32 | "topic": ExTypeTopic, 33 | "headers": ExTypeHeaders, 34 | } 35 | 36 | // MetricsState implements exchange's metrics state 37 | type MetricsState struct { 38 | MsgIn *metrics.TrackCounter 39 | MsgOut *metrics.TrackCounter 40 | } 41 | 42 | // Exchange implements AMQP-exchange 43 | type Exchange struct { 44 | Name string 45 | exType byte 46 | durable bool 47 | autoDelete bool 48 | internal bool 49 | system bool 50 | bindLock deadlock.Mutex 51 | bindings []*binding.Binding 52 | metrics *MetricsState 53 | } 54 | 55 | // NewExchange returns new instance of Exchange 56 | func NewExchange(name string, exType byte, durable bool, autoDelete bool, internal bool, system bool) *Exchange { 57 | return &Exchange{ 58 | Name: name, 59 | exType: exType, 60 | durable: durable, 61 | autoDelete: autoDelete, 62 | internal: internal, 63 | system: system, 64 | metrics: &MetricsState{ 65 | MsgIn: metrics.NewTrackCounter(0, true), 66 | MsgOut: metrics.NewTrackCounter(0, true), 67 | }, 68 | } 69 | } 70 | 71 | // GetExchangeTypeAlias returns exchange type alias by id 72 | func GetExchangeTypeAlias(id byte) (alias string, err error) { 73 | if alias, ok := exchangeTypeIDAliasMap[id]; ok { 74 | return alias, nil 75 | } 76 | return "", fmt.Errorf("undefined exchange type '%d'", id) 77 | } 78 | 79 | // GetExchangeTypeID returns exchange type id by alias 80 | func GetExchangeTypeID(alias string) (id byte, err error) { 81 | if id, ok := exchangeTypeAliasIDMap[alias]; ok { 82 | return id, nil 83 | } 84 | return 0, fmt.Errorf("undefined exchange alias '%s'", alias) 85 | } 86 | 87 | // GetTypeAlias returns exchange type alias by id 88 | func (ex *Exchange) GetTypeAlias() string { 89 | alias, _ := GetExchangeTypeAlias(ex.exType) 90 | 91 | return alias 92 | } 93 | 94 | // AppendBinding check and append binding 95 | // method check if binding already exists and ignore it 96 | func (ex *Exchange) AppendBinding(newBind *binding.Binding) { 97 | ex.bindLock.Lock() 98 | defer ex.bindLock.Unlock() 99 | 100 | // @spec-note 101 | // A server MUST allow ignore duplicate bindings ­ that is, two or more bind methods for a specific queue, 102 | // with identical arguments ­ without treating these as an error. 103 | for _, bind := range ex.bindings { 104 | if bind.Equal(newBind) { 105 | return 106 | } 107 | } 108 | ex.bindings = append(ex.bindings, newBind) 109 | } 110 | 111 | // RemoveBinding remove binding 112 | func (ex *Exchange) RemoveBinding(rmBind *binding.Binding) { 113 | ex.bindLock.Lock() 114 | defer ex.bindLock.Unlock() 115 | for i, bind := range ex.bindings { 116 | if bind.Equal(rmBind) { 117 | ex.bindings = append(ex.bindings[:i], ex.bindings[i+1:]...) 118 | return 119 | } 120 | } 121 | } 122 | 123 | // RemoveQueueBindings remove bindings for queue and return removed bindings 124 | func (ex *Exchange) RemoveQueueBindings(queueName string) []*binding.Binding { 125 | var newBindings []*binding.Binding 126 | var removedBindings []*binding.Binding 127 | ex.bindLock.Lock() 128 | defer ex.bindLock.Unlock() 129 | for _, bind := range ex.bindings { 130 | if bind.GetQueue() != queueName { 131 | newBindings = append(newBindings, bind) 132 | } else { 133 | removedBindings = append(removedBindings, bind) 134 | } 135 | } 136 | 137 | ex.bindings = newBindings 138 | return removedBindings 139 | } 140 | 141 | // GetMatchedQueues returns queues matched for message routing key 142 | func (ex *Exchange) GetMatchedQueues(message *amqp.Message) (matchedQueues map[string]bool) { 143 | // @spec-note 144 | // The server MUST implement these standard exchange types: fanout, direct. 145 | // The server SHOULD implement these standard exchange types: topic, headers. 146 | 147 | // TODO implement "headers" exchange 148 | matchedQueues = make(map[string]bool) 149 | switch ex.exType { 150 | case ExTypeDirect: 151 | for _, bind := range ex.bindings { 152 | if bind.MatchDirect(message.Exchange, message.RoutingKey) { 153 | matchedQueues[bind.GetQueue()] = true 154 | return 155 | } 156 | } 157 | case ExTypeFanout: 158 | for _, bind := range ex.bindings { 159 | if bind.MatchFanout(message.Exchange) { 160 | matchedQueues[bind.GetQueue()] = true 161 | } 162 | } 163 | case ExTypeTopic: 164 | for _, bind := range ex.bindings { 165 | if bind.MatchTopic(message.Exchange, message.RoutingKey) { 166 | matchedQueues[bind.GetQueue()] = true 167 | } 168 | } 169 | case ExTypeHeaders: 170 | if message.Header == nil { 171 | return 172 | } 173 | props := message.Header.PropertyList 174 | if props == nil { 175 | return 176 | } 177 | header := props.Headers 178 | for _, bind := range ex.bindings { 179 | if bind.MatchHeader(message.Exchange, header) { 180 | matchedQueues[bind.GetQueue()] = true 181 | } 182 | } 183 | } 184 | return 185 | } 186 | 187 | // EqualWithErr returns is given exchange equal to current 188 | func (ex *Exchange) EqualWithErr(exB *Exchange) error { 189 | errTemplate := "inequivalent arg '%s' for exchange '%s': received '%s' but current is '%s'" 190 | if ex.exType != exB.ExType() { 191 | aliasA, err := GetExchangeTypeAlias(ex.exType) 192 | if err != nil { 193 | return err 194 | } 195 | aliasB, err := GetExchangeTypeAlias(exB.ExType()) 196 | if err != nil { 197 | return err 198 | } 199 | return fmt.Errorf( 200 | errTemplate, 201 | "type", 202 | ex.Name, 203 | aliasB, 204 | aliasA, 205 | ) 206 | } 207 | if ex.durable != exB.IsDurable() { 208 | return fmt.Errorf(errTemplate, "durable", ex.Name, exB.IsDurable(), ex.durable) 209 | } 210 | if ex.autoDelete != exB.IsAutoDelete() { 211 | return fmt.Errorf(errTemplate, "autoDelete", ex.Name, exB.IsAutoDelete(), ex.autoDelete) 212 | } 213 | if ex.internal != exB.IsInternal() { 214 | return fmt.Errorf(errTemplate, "internal", ex.Name, exB.IsInternal(), ex.internal) 215 | } 216 | return nil 217 | } 218 | 219 | // GetBindings returns exchange's bindings 220 | func (ex *Exchange) GetBindings() []*binding.Binding { 221 | ex.bindLock.Lock() 222 | defer ex.bindLock.Unlock() 223 | return ex.bindings 224 | } 225 | 226 | // IsDurable returns is exchange durable 227 | func (ex *Exchange) IsDurable() bool { 228 | return ex.durable 229 | } 230 | 231 | // IsSystem returns is exchange system 232 | func (ex *Exchange) IsSystem() bool { 233 | return ex.system 234 | } 235 | 236 | // IsAutoDelete returns should be exchange deleted when all queues have finished using it 237 | func (ex *Exchange) IsAutoDelete() bool { 238 | return ex.autoDelete 239 | } 240 | 241 | // IsInternal returns that the exchange may not be used directly by publishers, 242 | // but only when bound to other exchanges 243 | func (ex *Exchange) IsInternal() bool { 244 | return ex.internal 245 | } 246 | 247 | // Marshal returns raw representation of exchange to store into storage 248 | func (ex *Exchange) Marshal(protoVersion string) (data []byte, err error) { 249 | buf := bytes.NewBuffer(make([]byte, 0)) 250 | if err = amqp.WriteShortstr(buf, ex.Name); err != nil { 251 | return nil, err 252 | } 253 | if err = amqp.WriteOctet(buf, ex.exType); err != nil { 254 | return nil, err 255 | } 256 | return buf.Bytes(), nil 257 | } 258 | 259 | // Unmarshal returns exchange from storage raw bytes data 260 | func (ex *Exchange) Unmarshal(data []byte) (err error) { 261 | buf := bytes.NewReader(data) 262 | if ex.Name, err = amqp.ReadShortstr(buf); err != nil { 263 | return err 264 | } 265 | if ex.exType, err = amqp.ReadOctet(buf); err != nil { 266 | return err 267 | } 268 | ex.durable = true 269 | return 270 | } 271 | 272 | // GetName returns exchange name 273 | func (ex *Exchange) GetName() string { 274 | return ex.Name 275 | } 276 | 277 | // ExType returns exchange type 278 | func (ex *Exchange) ExType() byte { 279 | return ex.exType 280 | } 281 | 282 | // SetMetrics set external metrics 283 | func (ex *Exchange) SetMetrics(m *MetricsState) { 284 | ex.metrics = m 285 | } 286 | 287 | // GetMetrics returns metrics 288 | func (ex *Exchange) GetMetrics() *MetricsState { 289 | return ex.metrics 290 | } 291 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/valinurovam/garagemq 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf 7 | github.com/dgraph-io/badger v1.6.2 8 | github.com/rabbitmq/amqp091-go v1.7.0 9 | github.com/sasha-s/go-deadlock v0.3.1 10 | github.com/sirupsen/logrus v1.9.0 11 | github.com/spf13/pflag v1.0.5 12 | github.com/spf13/viper v1.14.0 13 | github.com/tidwall/buntdb v1.2.10 14 | golang.org/x/crypto v0.17.0 15 | gopkg.in/yaml.v2 v2.4.0 16 | ) 17 | 18 | require ( 19 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect 20 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 21 | github.com/dgraph-io/ristretto v0.1.1 // indirect 22 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect 23 | github.com/dustin/go-humanize v1.0.0 // indirect 24 | github.com/fsnotify/fsnotify v1.6.0 // indirect 25 | github.com/golang/glog v1.0.0 // indirect 26 | github.com/golang/protobuf v1.5.2 // indirect 27 | github.com/hashicorp/hcl v1.0.0 // indirect 28 | github.com/magiconair/properties v1.8.7 // indirect 29 | github.com/mitchellh/mapstructure v1.5.0 // indirect 30 | github.com/pelletier/go-toml v1.9.5 // indirect 31 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 32 | github.com/petermattis/goid v0.0.0-20230904192822-1876fd5063bc // indirect 33 | github.com/pkg/errors v0.9.1 // indirect 34 | github.com/spf13/afero v1.9.3 // indirect 35 | github.com/spf13/cast v1.5.0 // indirect 36 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 37 | github.com/subosito/gotenv v1.4.1 // indirect 38 | github.com/tidwall/btree v1.6.0 // indirect 39 | github.com/tidwall/gjson v1.14.4 // indirect 40 | github.com/tidwall/grect v0.1.4 // indirect 41 | github.com/tidwall/match v1.1.1 // indirect 42 | github.com/tidwall/pretty v1.2.1 // indirect 43 | github.com/tidwall/rtred v0.1.2 // indirect 44 | github.com/tidwall/tinyqueue v0.1.1 // indirect 45 | golang.org/x/net v0.17.0 // indirect 46 | golang.org/x/sys v0.15.0 // indirect 47 | golang.org/x/text v0.14.0 // indirect 48 | google.golang.org/protobuf v1.33.0 // indirect 49 | gopkg.in/ini.v1 v1.67.0 // indirect 50 | gopkg.in/yaml.v3 v3.0.1 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /interfaces/interfaces.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "github.com/valinurovam/garagemq/amqp" 5 | ) 6 | 7 | // Channel represents base channel public interface 8 | type Channel interface { 9 | SendContent(method amqp.Method, message *amqp.Message) *amqp.Error 10 | SendMethod(method amqp.Method) 11 | NextDeliveryTag() uint64 12 | AddUnackedMessage(dTag uint64, cTag string, queue string, message *amqp.Message) 13 | } 14 | 15 | // Consumer represents base consumer public interface 16 | type Consumer interface { 17 | Consume() bool 18 | Tag() string 19 | Cancel() 20 | } 21 | 22 | // OpSet identifier for set data into storeage 23 | const OpSet = 1 24 | 25 | // OpDel identifier for delete data from storage 26 | const OpDel = 2 27 | 28 | // Operation represents structure to set/del from storage 29 | type Operation struct { 30 | Key string 31 | Value []byte 32 | Op byte 33 | } 34 | 35 | // DbStorage represent base db storage interface 36 | type DbStorage interface { 37 | Set(key string, value []byte) (err error) 38 | Del(key string) (err error) 39 | Get(key string) (value []byte, err error) 40 | Iterate(fn func(key []byte, value []byte)) 41 | IterateByPrefix(prefix []byte, limit uint64, fn func(key []byte, value []byte)) uint64 42 | IterateByPrefixFrom(prefix []byte, from []byte, limit uint64, fn func(key []byte, value []byte)) uint64 43 | DeleteByPrefix(prefix []byte) 44 | KeysByPrefixCount(prefix []byte) uint64 45 | ProcessBatch(batch []*Operation) (err error) 46 | Close() error 47 | } 48 | 49 | // MsgStorage represent interface for messages storage 50 | type MsgStorage interface { 51 | Del(message *amqp.Message, queue string) error 52 | PurgeQueue(queue string) 53 | Add(message *amqp.Message, queue string) error 54 | Update(message *amqp.Message, queue string) error 55 | IterateByQueueFromMsgID(queue string, msgID uint64, limit uint64, fn func(message *amqp.Message)) uint64 56 | GetQueueLength(queue string) uint64 57 | } 58 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | _ "net/http/pprof" 9 | "os" 10 | "runtime" 11 | "strings" 12 | "time" 13 | 14 | "github.com/sasha-s/go-deadlock" 15 | "github.com/sirupsen/logrus" 16 | "github.com/spf13/pflag" 17 | "github.com/spf13/viper" 18 | 19 | "github.com/valinurovam/garagemq/admin" 20 | "github.com/valinurovam/garagemq/config" 21 | "github.com/valinurovam/garagemq/metrics" 22 | "github.com/valinurovam/garagemq/server" 23 | ) 24 | 25 | func init() { 26 | viper.SetEnvPrefix("gmq") 27 | viper.AutomaticEnv() 28 | replacer := strings.NewReplacer("-", "_") 29 | viper.SetEnvKeyReplacer(replacer) 30 | 31 | flag.Bool("help", false, "Shows the help message") 32 | flag.String("config", "", "The configuration file to use for the GarageMQ.") 33 | 34 | var levels []string 35 | for _, l := range logrus.AllLevels { 36 | levels = append(levels, l.String()) 37 | } 38 | flag.String("log-file", "stdout", "Log file") 39 | flag.String("log-level", "info", fmt.Sprintf("Log level (%s)", strings.Join(levels, ", "))) 40 | flag.Bool("hprof", false, "Starts server with hprof profiler.") 41 | flag.String("hprof-host", "0.0.0.0", "hprof profiler host.") 42 | flag.String("hprof-port", "8080", "hprof profiler port.") 43 | 44 | pflag.CommandLine.AddGoFlagSet(flag.CommandLine) 45 | pflag.Parse() 46 | viper.BindPFlags(pflag.CommandLine) 47 | } 48 | 49 | func main() { 50 | // server has an issue with locks deadlock 51 | // solving is in progress 52 | deadlock.Opts.Disable = true 53 | 54 | if viper.GetBool("help") { 55 | flag.Usage() 56 | os.Exit(0) 57 | } 58 | 59 | initLogger(viper.GetString("log-level"), viper.GetString("log-file")) 60 | var cfg *config.Config 61 | var err error 62 | if viper.GetString("config") != "" { 63 | if cfg, err = config.CreateFromFile(viper.GetString("config")); err != nil { 64 | fmt.Println(err) 65 | os.Exit(1) 66 | } 67 | } else { 68 | cfg, _ = config.CreateDefault() 69 | } 70 | 71 | if viper.GetBool("hprof") { 72 | // for hprof debugging 73 | go http.ListenAndServe(fmt.Sprintf("%s:%s", viper.GetString("hprof-host"), viper.GetString("hprof-port")), nil) 74 | } 75 | 76 | runtime.GOMAXPROCS(runtime.NumCPU()) 77 | 78 | metrics.NewTrackRegistry(15, time.Second, false) 79 | 80 | srv := server.NewServer(cfg.TCP.IP, cfg.TCP.Port, cfg.Proto, cfg) 81 | adminServer := admin.NewAdminServer(srv, cfg.Admin.IP, cfg.Admin.Port) 82 | 83 | // Start admin server 84 | go func() { 85 | if err := adminServer.Start(); err != nil { 86 | panic("Failed to start adminServer - " + err.Error()) 87 | } 88 | }() 89 | 90 | // Start GarageMQ broker 91 | srv.Start() 92 | } 93 | 94 | func initLogger(lvl string, path string) { 95 | level, err := logrus.ParseLevel(lvl) 96 | if err != nil { 97 | panic(err) 98 | } 99 | 100 | var output io.Writer 101 | switch path { 102 | case "stdout": 103 | output = os.Stdout 104 | case "stderr": 105 | output = os.Stderr 106 | default: 107 | output, err = os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) 108 | if err != nil { 109 | panic(err) 110 | } 111 | } 112 | 113 | logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) 114 | logrus.SetOutput(output) 115 | logrus.SetLevel(level) 116 | } 117 | -------------------------------------------------------------------------------- /metrics/counter.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | // Counter implements int64 counter 8 | type Counter interface { 9 | Clear() 10 | Count() int64 11 | Dec(int64) 12 | Inc(int64) 13 | } 14 | 15 | // NilCounter is a no-op Counter. 16 | type NilCounter struct{} 17 | 18 | // Clear is a no-op. 19 | func (NilCounter) Clear() {} 20 | 21 | // Count is a no-op. 22 | func (NilCounter) Count() int64 { return 0 } 23 | 24 | // Dec is a no-op. 25 | func (NilCounter) Dec(i int64) {} 26 | 27 | // Inc is a no-op. 28 | func (NilCounter) Inc(i int64) {} 29 | 30 | // StandardCounter implements basic int64 counter with atomic ops 31 | type StandardCounter struct { 32 | count int64 33 | } 34 | 35 | // NewCounter returns Nil or Standard counter 36 | func NewCounter(isNil bool) Counter { 37 | if isNil { 38 | return NilCounter{} 39 | } 40 | return &StandardCounter{0} 41 | } 42 | 43 | // Clear clears counter 44 | func (c *StandardCounter) Clear() { 45 | atomic.StoreInt64(&c.count, 0) 46 | } 47 | 48 | // Count returns current counter value 49 | func (c *StandardCounter) Count() int64 { 50 | return atomic.LoadInt64(&c.count) 51 | } 52 | 53 | // Dec decrement current counter value 54 | func (c *StandardCounter) Dec(i int64) { 55 | atomic.AddInt64(&c.count, -i) 56 | } 57 | 58 | // Inc increment current counter value 59 | func (c *StandardCounter) Inc(i int64) { 60 | atomic.AddInt64(&c.count, i) 61 | } 62 | 63 | // TrackCounter implement counter with tracked values 64 | type TrackCounter struct { 65 | Counter Counter 66 | Track *TrackBuffer 67 | } 68 | 69 | // NewTrackCounter returns new TrackCounter 70 | func NewTrackCounter(trackLength int, isNil bool) *TrackCounter { 71 | return &TrackCounter{ 72 | Counter: NewCounter(isNil), 73 | Track: NewTrackBuffer(trackLength), 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /metrics/registry.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/sasha-s/go-deadlock" 7 | ) 8 | 9 | var r *TrackRegistry 10 | 11 | // TrackRegistry is a registry of track counters or other track metrics 12 | type TrackRegistry struct { 13 | cntLock deadlock.Mutex 14 | Counters map[string]*TrackCounter 15 | trackLength int 16 | trackTick *time.Ticker 17 | isNil bool 18 | } 19 | 20 | // NewTrackRegistry returns new TrackRegistry 21 | // Each counter will be tracked every d duration 22 | // Each counter track length will be trackLength items 23 | func NewTrackRegistry(trackLength int, d time.Duration, isNil bool) { 24 | r = &TrackRegistry{ 25 | Counters: make(map[string]*TrackCounter), 26 | trackLength: trackLength, 27 | trackTick: time.NewTicker(d), 28 | isNil: isNil, 29 | } 30 | if !isNil { 31 | go r.trackMetrics() 32 | } 33 | 34 | } 35 | 36 | // Destroy release current registry 37 | func Destroy() { 38 | r = nil 39 | } 40 | 41 | // AddCounter add counter into registry andd return it 42 | // TODO check if already exists 43 | func AddCounter(name string) *TrackCounter { 44 | r.cntLock.Lock() 45 | defer r.cntLock.Unlock() 46 | 47 | c := NewTrackCounter(r.trackLength, r.isNil) 48 | r.Counters[name] = c 49 | return c 50 | } 51 | 52 | // GetCounter returns counter by name 53 | func GetCounter(name string) *TrackCounter { 54 | return r.Counters[name] 55 | } 56 | 57 | func (r *TrackRegistry) trackMetrics() { 58 | for range r.trackTick.C { 59 | r.cntLock.Lock() 60 | for _, counter := range r.Counters { 61 | value := counter.Counter.Count() 62 | counter.Track.Add(value) 63 | } 64 | r.cntLock.Unlock() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /metrics/trackBuffer.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/sasha-s/go-deadlock" 7 | ) 8 | 9 | // TrackItem implements tracked item with value and timestamp 10 | type TrackItem struct { 11 | Value int64 `json:"value"` 12 | Timestamp int64 `json:"timestamp"` 13 | } 14 | 15 | // TrackBuffer implements buffer of TrackItems 16 | type TrackBuffer struct { 17 | track []*TrackItem 18 | trackDiff []*TrackItem 19 | lock deadlock.RWMutex 20 | pos int 21 | length int 22 | 23 | last *TrackItem 24 | lastDiff *TrackItem 25 | } 26 | 27 | // NewTrackBuffer returns new TrackBuffer 28 | func NewTrackBuffer(length int) *TrackBuffer { 29 | return &TrackBuffer{ 30 | track: make([]*TrackItem, length), 31 | trackDiff: make([]*TrackItem, length), 32 | pos: 0, 33 | length: length, 34 | } 35 | } 36 | 37 | // Add adds counter value into track 38 | func (t *TrackBuffer) Add(item int64) { 39 | t.lock.Lock() 40 | defer t.lock.Unlock() 41 | t.track[t.pos] = &TrackItem{ 42 | Value: item, 43 | Timestamp: time.Now().Unix(), 44 | } 45 | 46 | var diffItem int64 47 | t.trackDiff[t.pos] = &TrackItem{ 48 | Timestamp: time.Now().Unix(), 49 | } 50 | if t.pos > 0 { 51 | diffItem = item - t.track[t.pos-1].Value 52 | } else if t.track[t.length-1] != nil { 53 | diffItem = item - t.track[t.length-1].Value 54 | } else { 55 | diffItem = item 56 | } 57 | t.trackDiff[t.pos].Value = diffItem 58 | 59 | t.last = t.track[t.pos] 60 | t.lastDiff = t.trackDiff[t.pos] 61 | 62 | t.pos = (t.pos + 1) % t.length 63 | } 64 | 65 | // GetTrack returns current recorded track 66 | func (t *TrackBuffer) GetTrack() []*TrackItem { 67 | t.lock.RLock() 68 | defer t.lock.RUnlock() 69 | track := make([]*TrackItem, t.length) 70 | copy(track[0:t.length-t.pos], t.track[t.pos:]) 71 | copy(track[t.length-t.pos:], t.track[:t.pos]) 72 | 73 | return track 74 | } 75 | 76 | // GetDiffTrack returns current recorded diff-track 77 | func (t *TrackBuffer) GetDiffTrack() []*TrackItem { 78 | t.lock.RLock() 79 | defer t.lock.RUnlock() 80 | track := make([]*TrackItem, t.length) 81 | copy(track[0:t.length-t.pos], t.trackDiff[t.pos:]) 82 | copy(track[t.length-t.pos:], t.trackDiff[:t.pos]) 83 | 84 | return track 85 | } 86 | 87 | // GetLastTrackItem returns last tracked item 88 | func (t *TrackBuffer) GetLastTrackItem() *TrackItem { 89 | t.lock.RLock() 90 | defer t.lock.RUnlock() 91 | 92 | return t.last 93 | } 94 | 95 | // GetLastDiffTrackItem returns last tracked diff item 96 | func (t *TrackBuffer) GetLastDiffTrackItem() *TrackItem { 97 | t.lock.RLock() 98 | defer t.lock.RUnlock() 99 | 100 | return t.lastDiff 101 | } 102 | -------------------------------------------------------------------------------- /msgstorage/msgstorage.go: -------------------------------------------------------------------------------- 1 | package msgstorage 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | 8 | "github.com/sasha-s/go-deadlock" 9 | 10 | "github.com/valinurovam/garagemq/amqp" 11 | "github.com/valinurovam/garagemq/interfaces" 12 | ) 13 | 14 | // MsgStorage represents storage for store all durable messages 15 | // All operations (add, update and delete) store into little queues and 16 | // periodically persist every 20ms 17 | // If storage in confirm-mode - in every persisted message storage send confirm to vhost 18 | type MsgStorage struct { 19 | db interfaces.DbStorage 20 | persistLock deadlock.Mutex 21 | add map[string]*amqp.Message 22 | update map[string]*amqp.Message 23 | del map[string]*amqp.Message 24 | protoVersion string 25 | closeCh chan bool 26 | confirmSyncCh chan *amqp.Message 27 | confirmMode bool 28 | writeCh chan struct{} 29 | } 30 | 31 | // NewMsgStorage returns new instance of message storage 32 | func NewMsgStorage(db interfaces.DbStorage, protoVersion string) *MsgStorage { 33 | msgStorage := &MsgStorage{ 34 | db: db, 35 | protoVersion: protoVersion, 36 | closeCh: make(chan bool), 37 | confirmSyncCh: make(chan *amqp.Message, 4096), 38 | writeCh: make(chan struct{}, 5), 39 | } 40 | msgStorage.cleanPersistQueue() 41 | go msgStorage.periodicPersist() 42 | return msgStorage 43 | } 44 | 45 | func (storage *MsgStorage) cleanPersistQueue() { 46 | storage.add = make(map[string]*amqp.Message) 47 | storage.update = make(map[string]*amqp.Message) 48 | storage.del = make(map[string]*amqp.Message) 49 | } 50 | 51 | // We try to persist messages every 20ms and every 1000msg 52 | func (storage *MsgStorage) periodicPersist() { 53 | tick := time.NewTicker(20 * time.Millisecond) 54 | 55 | go func() { 56 | for range tick.C { 57 | storage.writeCh <- struct{}{} 58 | } 59 | }() 60 | 61 | for range storage.writeCh { 62 | select { 63 | case <-storage.closeCh: 64 | return 65 | default: 66 | storage.persist() 67 | } 68 | } 69 | } 70 | 71 | // no need to be thread safe 72 | func (storage *MsgStorage) getQueueLen() int { 73 | return len(storage.add) + len(storage.update) + len(storage.del) 74 | } 75 | 76 | func (storage *MsgStorage) persist() { 77 | storage.persistLock.Lock() 78 | add := storage.add 79 | del := storage.del 80 | update := storage.update 81 | storage.cleanPersistQueue() 82 | storage.persistLock.Unlock() 83 | 84 | rmDel := make([]string, 0) 85 | for delKey := range del { 86 | if _, ok := add[delKey]; ok { 87 | delete(add, delKey) 88 | rmDel = append(rmDel, delKey) 89 | } 90 | 91 | delete(update, delKey) 92 | } 93 | 94 | for _, delKey := range rmDel { 95 | delete(del, delKey) 96 | } 97 | 98 | batch := make([]*interfaces.Operation, 0, len(add)+len(update)+len(del)) 99 | for key, message := range add { 100 | data, _ := message.Marshal(storage.protoVersion) 101 | batch = append( 102 | batch, 103 | &interfaces.Operation{ 104 | Key: key, 105 | Value: data, 106 | Op: interfaces.OpSet, 107 | }, 108 | ) 109 | } 110 | 111 | for key, message := range update { 112 | data, _ := message.Marshal(storage.protoVersion) 113 | batch = append( 114 | batch, 115 | &interfaces.Operation{ 116 | Key: key, 117 | Value: data, 118 | Op: interfaces.OpSet, 119 | }, 120 | ) 121 | } 122 | 123 | for key := range del { 124 | batch = append( 125 | batch, 126 | &interfaces.Operation{ 127 | Key: key, 128 | Op: interfaces.OpDel, 129 | }, 130 | ) 131 | } 132 | 133 | if err := storage.db.ProcessBatch(batch); err != nil { 134 | panic(err) 135 | } 136 | 137 | for _, message := range add { 138 | if message.ConfirmMeta != nil && storage.confirmMode && message.ConfirmMeta.DeliveryTag > 0 { 139 | message.ConfirmMeta.ActualConfirms++ 140 | storage.confirmSyncCh <- message 141 | } 142 | } 143 | } 144 | 145 | // ReceiveConfirms set message storage in confirm mode and return channel for receive confirms 146 | func (storage *MsgStorage) ReceiveConfirms() chan *amqp.Message { 147 | storage.confirmMode = true 148 | return storage.confirmSyncCh 149 | } 150 | 151 | // Add append message into add-queue 152 | func (storage *MsgStorage) Add(message *amqp.Message, queue string) error { 153 | if storage.getQueueLen() > 1000 { 154 | storage.writeCh <- struct{}{} 155 | } 156 | 157 | storage.persistLock.Lock() 158 | defer storage.persistLock.Unlock() 159 | 160 | storage.add[makeKey(message.ID, queue)] = message 161 | return nil 162 | } 163 | 164 | // Update append message into update-queue 165 | func (storage *MsgStorage) Update(message *amqp.Message, queue string) error { 166 | storage.persistLock.Lock() 167 | defer storage.persistLock.Unlock() 168 | storage.update[makeKey(message.ID, queue)] = message 169 | return nil 170 | } 171 | 172 | // Del append message into del-queue 173 | func (storage *MsgStorage) Del(message *amqp.Message, queue string) error { 174 | storage.persistLock.Lock() 175 | defer storage.persistLock.Unlock() 176 | storage.del[makeKey(message.ID, queue)] = message 177 | return nil 178 | } 179 | 180 | // Iterate iterates over all messages 181 | func (storage *MsgStorage) Iterate(fn func(queue string, message *amqp.Message)) { 182 | storage.db.Iterate( 183 | func(key []byte, value []byte) { 184 | queueName := getQueueFromKey(string(key)) 185 | message := &amqp.Message{} 186 | message.Unmarshal(value, storage.protoVersion) 187 | fn(queueName, message) 188 | }, 189 | ) 190 | } 191 | 192 | // IterateByQueue iterates over queue and call fn on each message 193 | func (storage *MsgStorage) IterateByQueue(queue string, limit uint64, fn func(message *amqp.Message)) { 194 | prefix := "msg." + queue + "." 195 | storage.db.IterateByPrefix( 196 | []byte(prefix), 197 | limit, 198 | func(key []byte, value []byte) { 199 | message := &amqp.Message{} 200 | message.Unmarshal(value, storage.protoVersion) 201 | fn(message) 202 | }, 203 | ) 204 | } 205 | 206 | // IterateByQueueFromMsgID iterates over queue from specific msgId and call fn on each message 207 | func (storage *MsgStorage) IterateByQueueFromMsgID(queue string, msgID uint64, limit uint64, fn func(message *amqp.Message)) uint64 { 208 | prefix := "msg." + queue + "." 209 | from := makeKey(msgID, queue) 210 | return storage.db.IterateByPrefixFrom( 211 | []byte(prefix), 212 | []byte(from), 213 | limit, 214 | func(key []byte, value []byte) { 215 | message := &amqp.Message{} 216 | message.Unmarshal(value, storage.protoVersion) 217 | fn(message) 218 | }, 219 | ) 220 | } 221 | 222 | // GetQueueLength returns queue length in message storage 223 | func (storage *MsgStorage) GetQueueLength(queue string) uint64 { 224 | prefix := "msg." + queue + "." 225 | return storage.db.KeysByPrefixCount([]byte(prefix)) 226 | } 227 | 228 | // PurgeQueue delete messages 229 | func (storage *MsgStorage) PurgeQueue(queue string) { 230 | prefix := []byte("msg." + queue + ".") 231 | storage.db.DeleteByPrefix(prefix) 232 | } 233 | 234 | // Close properly "stop" message storage 235 | func (storage *MsgStorage) Close() error { 236 | storage.closeCh <- true 237 | storage.persistLock.Lock() 238 | defer storage.persistLock.Unlock() 239 | return storage.db.Close() 240 | } 241 | 242 | func makeKey(id uint64, queue string) string { 243 | return "msg." + queue + "." + strconv.FormatInt(int64(id), 10) 244 | } 245 | 246 | func getQueueFromKey(key string) string { 247 | parts := strings.Split(key, ".") 248 | return parts[1] 249 | } 250 | -------------------------------------------------------------------------------- /pool/buffer.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | // BufferPool represents a thread safe buffer pool 9 | type BufferPool struct { 10 | sync.Pool 11 | } 12 | 13 | // NewBufferPool returns a new BufferPool 14 | func NewBufferPool(bufferSize int) *BufferPool { 15 | return &BufferPool{ 16 | sync.Pool{ 17 | New: func() interface{} { 18 | return bytes.NewBuffer(make([]byte, 0, bufferSize)) 19 | }, 20 | }, 21 | } 22 | } 23 | 24 | // Get gets a Buffer from the pool 25 | func (bp *BufferPool) Get() *bytes.Buffer { 26 | return bp.Pool.Get().(*bytes.Buffer) 27 | } 28 | 29 | // Put returns the given Buffer to the pool. 30 | func (bp *BufferPool) Put(b *bytes.Buffer) { 31 | b.Reset() 32 | bp.Pool.Put(b) 33 | } 34 | -------------------------------------------------------------------------------- /protocol/protogen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | var baseDomainsMap = map[string]string{ 13 | "octet": "byte", 14 | "short": "uint16", 15 | "long": "uint32", 16 | "longlong": "uint64", 17 | "timestamp": "time.Time", 18 | "shortstr": "string", 19 | "longstr": "[]byte", 20 | "bit": "bool", 21 | "table": "*Table", 22 | } 23 | 24 | // Amqp represents root XML-spec structure 25 | type Amqp struct { 26 | Constants []*Constant `xml:"constant"` 27 | Domains []*Domain `xml:"domain"` 28 | Classes []*Class `xml:"class"` 29 | } 30 | 31 | // Constant represents specific constants 32 | type Constant struct { 33 | Name string `xml:"name,attr"` 34 | Value uint16 `xml:"value,attr"` 35 | Doc string `xml:"doc"` 36 | GoName string 37 | GoStr string 38 | IgnoreOnMap bool 39 | } 40 | 41 | // Domain represent domain amqp types 42 | type Domain struct { 43 | Name string `xml:"name,attr"` 44 | Type string `xml:"type,attr"` 45 | GoName string 46 | GoType string 47 | } 48 | 49 | // Class represents collection of amqp-methods 50 | type Class struct { 51 | Name string `xml:"name,attr"` 52 | ID uint16 `xml:"index,attr"` 53 | Methods []*Method `xml:"method"` 54 | Fields []*Field `xml:"field"` 55 | GoName string 56 | } 57 | 58 | // Method represents amqp method 59 | type Method struct { 60 | Name string `xml:"name,attr"` 61 | ID uint16 `xml:"index,attr"` 62 | Fields []*Field `xml:"field"` 63 | GoName string 64 | Doc string `xml:"doc"` 65 | Synchronous byte `xml:"synchronous,attr"` 66 | } 67 | 68 | // Field represents field inside amqp-method 69 | type Field struct { 70 | Name string `xml:"name,attr"` 71 | Domain string `xml:"domain,attr"` 72 | Type string `xml:"type,attr"` 73 | GoName string 74 | GoType string 75 | ReaderFunc string 76 | IsBit bool 77 | BitOrder int 78 | LastBit bool 79 | HeaderIndex int 80 | } 81 | 82 | // SaveConstants parse and save amqp-constants 83 | func (amqp Amqp) SaveConstants(wr io.Writer) error { 84 | t := getConstantsTemplate() 85 | 86 | for _, class := range amqp.Classes { 87 | constant := &Constant{Name: strings.Join([]string{"class", class.Name}, "-"), Value: class.ID, IgnoreOnMap: true} 88 | amqp.Constants = append(amqp.Constants, constant) 89 | 90 | for _, method := range class.Methods { 91 | constant := &Constant{Name: strings.Join([]string{"method", class.Name, method.Name}, "-"), Value: method.ID, IgnoreOnMap: true} 92 | amqp.Constants = append(amqp.Constants, constant) 93 | } 94 | } 95 | 96 | for _, constant := range amqp.Constants { 97 | constant.GoName = kebabToCamel(constant.Name) 98 | constant.GoStr = kebabToStr(constant.Name) 99 | constant.Doc = normalizeDoc(constant.GoName+" identifier", constant.Doc) 100 | } 101 | 102 | return t.Execute(wr, amqp.Constants) 103 | } 104 | 105 | // SaveMethods parse and save amqp-methods 106 | func (amqp Amqp) SaveMethods(wr io.Writer) error { 107 | t := getMethodsTemplate() 108 | 109 | domainAliases := map[string]string{} 110 | 111 | for _, domain := range amqp.Domains { 112 | if _, ok := baseDomainsMap[domain.Name]; !ok { 113 | domainAliases[domain.Name] = domain.Type 114 | } 115 | } 116 | 117 | for _, class := range amqp.Classes { 118 | class.GoName = kebabToCamel(class.Name) 119 | 120 | headerIndex := 15 121 | for _, field := range class.Fields { 122 | field.GoName = kebabToCamel(field.Name) 123 | domainKey := calcDomainKey(field, domainAliases) 124 | field.GoType = baseDomainsMap[domainKey] 125 | field.ReaderFunc = kebabToCamel(domainKey) 126 | field.HeaderIndex = headerIndex 127 | headerIndex-- 128 | } 129 | 130 | for _, method := range class.Methods { 131 | method.GoName = kebabToCamel(class.Name + "-" + method.Name) 132 | method.Doc = normalizeDoc(method.GoName, method.Doc) 133 | bitOrder := 0 134 | methodsCount := len(method.Fields) 135 | for idx, field := range method.Fields { 136 | field.LastBit = false 137 | field.GoName = kebabToCamel(field.Name) 138 | domainKey := calcDomainKey(field, domainAliases) 139 | field.GoType = baseDomainsMap[domainKey] 140 | field.IsBit = domainKey == "bit" 141 | if field.IsBit { 142 | field.BitOrder = bitOrder 143 | bitOrder++ 144 | } else if bitOrder > 0 { 145 | method.Fields[idx-1].LastBit = true 146 | bitOrder = 0 147 | } 148 | 149 | if field.IsBit && methodsCount == idx+1 { 150 | method.Fields[idx].LastBit = true 151 | bitOrder = 0 152 | } 153 | 154 | field.ReaderFunc = kebabToCamel(domainKey) 155 | } 156 | } 157 | } 158 | 159 | return t.Execute(wr, amqp.Classes) 160 | } 161 | 162 | func calcDomainKey(field *Field, domainAliases map[string]string) string { 163 | var domainKey string 164 | 165 | if field.Domain != "" { 166 | domainKey = field.Domain 167 | } else { 168 | domainKey = field.Type 169 | } 170 | if dk, ok := domainAliases[field.Domain]; ok { 171 | domainKey = dk 172 | } 173 | return domainKey 174 | } 175 | 176 | func main() { 177 | file, err := ioutil.ReadFile("protocol/amqp0-9-1.extended.xml") 178 | if err != nil { 179 | fmt.Println(err) 180 | os.Exit(1) 181 | } 182 | var amqp Amqp 183 | if err := xml.Unmarshal(file, &amqp); err != nil { 184 | fmt.Println(err) 185 | os.Exit(1) 186 | } 187 | 188 | constantsFile, err := os.Create("amqp/constants_generated.go") 189 | if err != nil { 190 | fmt.Println(err) 191 | os.Exit(1) 192 | } 193 | methodsFile, err := os.Create("amqp/methods_generated.go") 194 | if err != nil { 195 | fmt.Println(err) 196 | os.Exit(1) 197 | } 198 | 199 | if err := amqp.SaveConstants(constantsFile); err != nil { 200 | fmt.Println(err) 201 | os.Exit(1) 202 | } 203 | if err := amqp.SaveMethods(methodsFile); err != nil { 204 | fmt.Println(err) 205 | os.Exit(1) 206 | } 207 | } 208 | 209 | func kebabToCamel(kebab string) (camel string) { 210 | parts := strings.Split(kebab, "-") 211 | for _, part := range parts { 212 | if part == "id" { 213 | camel += strings.ToUpper(part) 214 | } else { 215 | camel += strings.Title(part) 216 | } 217 | } 218 | return 219 | } 220 | 221 | func kebabToStr(kebab string) (str string) { 222 | parts := strings.Split(kebab, "-") 223 | var upperParts []string 224 | for _, part := range parts { 225 | upperParts = append(upperParts, strings.ToUpper(part)) 226 | } 227 | return strings.Join(upperParts, "_") 228 | } 229 | 230 | func normalizeDoc(goName string, doc string) string { 231 | doc = strings.TrimSpace(doc) 232 | 233 | var docLines []string 234 | 235 | for idx, line := range strings.Split(doc, "\n") { 236 | line = strings.TrimSpace(line) 237 | if idx == 0 { 238 | line = goName + " " + line 239 | } 240 | line = "// " + line 241 | docLines = append(docLines, line) 242 | } 243 | return strings.Join(docLines, "\n") 244 | } 245 | -------------------------------------------------------------------------------- /protocol/templates.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "text/template" 4 | 5 | func getConstantsTemplate() *template.Template { 6 | const constTemplate = ` 7 | // Package amqp for read, write, parse amqp frames 8 | // Autogenerated code. Do not edit. 9 | package amqp 10 | {{range .}} 11 | {{.Doc}} 12 | const {{.GoName}} = {{.Value}} 13 | {{end}} 14 | 15 | // ConstantsNameMap map for mapping error codes into error messages 16 | var ConstantsNameMap = map[uint16]string{ 17 | {{range .}} 18 | {{if eq .IgnoreOnMap false}} 19 | {{.Value}}: "{{.GoStr}}",{{end}} 20 | {{end}} 21 | } 22 | ` 23 | return template.Must(template.New("constTemplate").Parse(constTemplate)) 24 | } 25 | 26 | func getMethodsTemplate() *template.Template { 27 | const methodsTemplate = ` 28 | // Package amqp for read, write, parse amqp frames 29 | // Autogenerated code. Do not edit. 30 | package amqp 31 | 32 | import ( 33 | "io" 34 | "fmt" 35 | "time" 36 | ) 37 | 38 | // Method represents base method interface 39 | type Method interface { 40 | Name() string 41 | FrameType() byte 42 | ClassIdentifier() uint16 43 | MethodIdentifier() uint16 44 | Read(reader io.Reader, protoVersion string) (err error) 45 | Write(writer io.Writer, protoVersion string) (err error) 46 | Sync() bool 47 | } 48 | {{range .}} 49 | {{$classId := .ID}} 50 | // {{.GoName}} methods 51 | 52 | {{ if .Fields }} 53 | // {{.GoName}}PropertyList represents properties for {{.GoName}} method 54 | type {{.GoName}}PropertyList struct { 55 | {{range .Fields}}{{.GoName}} {{if ne .GoType "*Table"}}*{{end}}{{.GoType}} 56 | {{end}} 57 | } 58 | // {{.GoName}}PropertyList reads properties from io reader 59 | func (pList *{{.GoName}}PropertyList) Read(reader io.Reader, propertyFlags uint16, protoVersion string) (err error) { 60 | {{range .Fields}} 61 | if propertyFlags&(1<<{{.HeaderIndex}}) != 0 { 62 | value, err := Read{{.ReaderFunc}}(reader{{if eq .ReaderFunc "Table"}}, protoVersion{{end}}) 63 | if err != nil { 64 | return err 65 | } 66 | pList.{{.GoName}} = {{if ne .GoType "*Table"}}&{{end}}value 67 | } 68 | {{end}} 69 | return 70 | } 71 | // {{.GoName}}PropertyList wiretes properties into io writer 72 | func (pList *{{.GoName}}PropertyList) Write(writer io.Writer, protoVersion string) (propertyFlags uint16, err error) { 73 | {{range .Fields}} 74 | if pList.{{.GoName}} != nil { 75 | propertyFlags |= 1 << {{.HeaderIndex}} 76 | if err = Write{{.ReaderFunc}}(writer, {{if ne .GoType "*Table"}}*{{end}}pList.{{.GoName}}{{if eq .ReaderFunc "Table"}}, protoVersion{{end}}); err != nil { 77 | return 78 | } 79 | } 80 | {{end}} 81 | return 82 | } 83 | {{end}} 84 | {{range .Methods}} 85 | {{.Doc}} 86 | type {{.GoName}} struct { 87 | {{range .Fields}}{{.GoName}} {{.GoType}} 88 | {{end}} 89 | } 90 | // Name returns method name as string, usefully for logging 91 | func (method *{{.GoName}}) Name() string { 92 | return "{{.GoName}}" 93 | } 94 | 95 | // FrameType returns method frame type 96 | func (method *{{.GoName}}) FrameType() byte { 97 | return 1 98 | } 99 | 100 | // ClassIdentifier returns method classID 101 | func (method *{{.GoName}}) ClassIdentifier() uint16 { 102 | return {{$classId}} 103 | } 104 | 105 | // MethodIdentifier returns method methodID 106 | func (method *{{.GoName}}) MethodIdentifier() uint16 { 107 | return {{.ID}} 108 | } 109 | 110 | // Sync is method should me sent synchronous 111 | func (method *{{.GoName}}) Sync() bool { 112 | return {{if eq .Synchronous 1}}true{{else}}false{{end}} 113 | } 114 | 115 | // Read method from io reader 116 | func (method *{{.GoName}}) Read(reader io.Reader, protoVersion string) (err error) { 117 | {{range .Fields}} 118 | {{if .IsBit }} 119 | {{if eq .BitOrder 0}} 120 | bits, err := ReadOctet(reader) 121 | if err != nil { 122 | return err 123 | } 124 | {{end}} 125 | method.{{.GoName}} = bits&(1<<{{.BitOrder}}) != 0 126 | {{else}} 127 | method.{{.GoName}}, err = Read{{.ReaderFunc}}(reader{{if eq .ReaderFunc "Table"}}, protoVersion{{end}}) 128 | if err != nil { 129 | return err 130 | } 131 | {{end}} 132 | 133 | {{end}} 134 | return 135 | } 136 | 137 | // Write method from io reader 138 | func (method *{{.GoName}}) Write(writer io.Writer, protoVersion string) (err error) { 139 | {{$bitFieldsStarted := false}} 140 | {{range .Fields}} 141 | {{if .IsBit }} 142 | {{$bitFieldsStarted := true}} 143 | {{if eq .BitOrder 0}} 144 | var bits byte 145 | {{end}} 146 | if method.{{.GoName}} { 147 | bits |= 1 << {{.BitOrder}} 148 | } 149 | {{if .LastBit}} 150 | if err = WriteOctet(writer, bits); err != nil { 151 | return err 152 | } 153 | {{end}} 154 | {{else}} 155 | if err = Write{{.ReaderFunc}}(writer, method.{{.GoName}}{{if eq .ReaderFunc "Table"}}, protoVersion{{end}}); err != nil { 156 | return err 157 | } 158 | {{end}} 159 | {{end}} 160 | return 161 | } 162 | {{end}} 163 | {{end}} 164 | 165 | /* 166 | ReadMethod reads method from frame's payload 167 | 168 | Method frames carry the high-level protocol commands (which we call "methods"). 169 | One method frame carries one command. The method frame payload has this format: 170 | 171 | 0 2 4 172 | +----------+-----------+-------------- - - 173 | | class-id | method-id | arguments... 174 | +----------+-----------+-------------- - - 175 | short short ... 176 | 177 | */ 178 | func ReadMethod(reader io.Reader, protoVersion string) (Method, error) { 179 | classID, err := ReadShort(reader) 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | methodID, err := ReadShort(reader) 185 | if err != nil { 186 | return nil, err 187 | } 188 | switch classID { 189 | {{range .}} 190 | case {{.ID}}: 191 | switch methodID { 192 | {{range .Methods}} 193 | case {{.ID}}: 194 | var method = &{{.GoName}}{} 195 | if err := method.Read(reader, protoVersion); err != nil { 196 | return nil, err 197 | } 198 | return method, nil{{end}} 199 | }{{end}} 200 | } 201 | 202 | return nil, fmt.Errorf("unknown classID and methodID: [%d. %d]", classID, methodID) 203 | } 204 | 205 | // WriteMethod writes method into frame's payload 206 | func WriteMethod(writer io.Writer, method Method, protoVersion string) (err error) { 207 | if err = WriteShort(writer, method.ClassIdentifier()); err != nil { 208 | return err 209 | } 210 | if err = WriteShort(writer, method.MethodIdentifier()); err != nil { 211 | return err 212 | } 213 | 214 | if err = method.Write(writer, protoVersion); err != nil { 215 | return err 216 | } 217 | 218 | return 219 | } 220 | ` 221 | return template.Must(template.New("methodsTemplate").Parse(methodsTemplate)) 222 | } 223 | -------------------------------------------------------------------------------- /qos/qos.go: -------------------------------------------------------------------------------- 1 | package qos 2 | 3 | import "github.com/sasha-s/go-deadlock" 4 | 5 | // AmqpQos represents qos system 6 | type AmqpQos struct { 7 | deadlock.Mutex 8 | prefetchCount uint16 9 | currentCount uint16 10 | prefetchSize uint32 11 | currentSize uint32 12 | } 13 | 14 | // NewAmqpQos returns new instance of AmqpQos 15 | func NewAmqpQos(prefetchCount uint16, prefetchSize uint32) *AmqpQos { 16 | return &AmqpQos{ 17 | prefetchCount: prefetchCount, 18 | prefetchSize: prefetchSize, 19 | currentCount: 0, 20 | currentSize: 0, 21 | } 22 | } 23 | 24 | // PrefetchCount returns prefetchCount 25 | func (qos *AmqpQos) PrefetchCount() uint16 { 26 | return qos.prefetchCount 27 | } 28 | 29 | // PrefetchSize returns prefetchSize 30 | func (qos *AmqpQos) PrefetchSize() uint32 { 31 | return qos.prefetchSize 32 | } 33 | 34 | // Update set new prefetchCount and prefetchSize 35 | func (qos *AmqpQos) Update(prefetchCount uint16, prefetchSize uint32) { 36 | qos.prefetchCount = prefetchCount 37 | qos.prefetchSize = prefetchSize 38 | } 39 | 40 | // IsActive check is qos rules are active 41 | // both prefetchSize and prefetchCount must be 0 42 | func (qos *AmqpQos) IsActive() bool { 43 | return qos.prefetchCount != 0 || qos.prefetchSize != 0 44 | } 45 | 46 | // Inc increment current count and size 47 | // Returns true if increment success 48 | // Returns false if after increment size or count will be more than prefetchCount or prefetchSize 49 | func (qos *AmqpQos) Inc(count uint16, size uint32) bool { 50 | qos.Lock() 51 | defer qos.Unlock() 52 | 53 | newCount := qos.currentCount + count 54 | newSize := qos.currentSize + size 55 | 56 | if (qos.prefetchCount == 0 || newCount <= qos.prefetchCount) && (qos.prefetchSize == 0 || newSize <= qos.prefetchSize) { 57 | qos.currentCount = newCount 58 | qos.currentSize = newSize 59 | return true 60 | } 61 | 62 | return false 63 | } 64 | 65 | // Dec decrement current count and size 66 | func (qos *AmqpQos) Dec(count uint16, size uint32) { 67 | qos.Lock() 68 | defer qos.Unlock() 69 | 70 | if qos.currentCount < count { 71 | qos.currentCount = 0 72 | } else { 73 | qos.currentCount = qos.currentCount - count 74 | } 75 | 76 | if qos.currentSize < size { 77 | qos.currentSize = 0 78 | } else { 79 | qos.currentSize = qos.currentSize - size 80 | } 81 | } 82 | 83 | // Release reset current count and size 84 | func (qos *AmqpQos) Release() { 85 | qos.Lock() 86 | defer qos.Unlock() 87 | qos.currentCount = 0 88 | qos.currentSize = 0 89 | } 90 | 91 | // Copy safe copy current qos instance to new one 92 | func (qos *AmqpQos) Copy() *AmqpQos { 93 | qos.Lock() 94 | defer qos.Unlock() 95 | return &AmqpQos{ 96 | prefetchCount: qos.prefetchCount, 97 | prefetchSize: qos.prefetchSize, 98 | currentCount: qos.currentCount, 99 | currentSize: qos.currentSize, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /qos/qos_test.go: -------------------------------------------------------------------------------- 1 | package qos 2 | 3 | import "testing" 4 | 5 | func TestAmqpQos_IsActive(t *testing.T) { 6 | q := NewAmqpQos(1, 10) 7 | 8 | if q.PrefetchSize() != 10 { 9 | t.Fatalf("PrefetchSize: Expected %d, actual %d", 10, q.PrefetchSize()) 10 | } 11 | 12 | if q.PrefetchCount() != 1 { 13 | t.Fatalf("PrefetchCount: Expected %d, actual %d", 1, q.PrefetchCount()) 14 | } 15 | 16 | if !q.IsActive() { 17 | t.Fatalf("Expected active qos") 18 | } 19 | 20 | q = NewAmqpQos(0, 0) 21 | 22 | if q.IsActive() { 23 | t.Fatalf("Expected inactive qos") 24 | } 25 | } 26 | 27 | func TestAmqpQos_Dec(t *testing.T) { 28 | q := NewAmqpQos(5, 10) 29 | q.Dec(1, 1) 30 | 31 | if q.currentCount != 0 { 32 | t.Fatalf("Dec: Expected currentCount %d, actual %d", 0, q.currentCount) 33 | } 34 | 35 | if q.currentSize != 0 { 36 | t.Fatalf("Dec: Expected currentSize %d, actual %d", 0, q.currentCount) 37 | } 38 | 39 | q.Inc(4, 8) 40 | q.Dec(1, 1) 41 | 42 | if q.currentCount != 3 { 43 | t.Fatalf("Dec: Expected currentCount %d, actual %d", 3, q.currentCount) 44 | } 45 | 46 | if q.currentSize != 7 { 47 | t.Fatalf("Dec: Expected currentSize %d, actual %d", 7, q.currentCount) 48 | } 49 | } 50 | 51 | func TestAmqpQos_Inc(t *testing.T) { 52 | q := NewAmqpQos(5, 10) 53 | res := q.Inc(1, 1) 54 | 55 | if !res { 56 | t.Fatalf("Inc: Expected successful inc") 57 | } 58 | if q.currentCount != 1 { 59 | t.Fatalf("Inc: Expected currentCount %d, actual %d", 1, q.currentCount) 60 | } 61 | 62 | if q.currentSize != 1 { 63 | t.Fatalf("Inc: Expected currentSize %d, actual %d", 1, q.currentCount) 64 | } 65 | 66 | q = NewAmqpQos(5, 10) 67 | if q.Inc(6, 1) { 68 | t.Fatalf("Inc: Expected failed inc") 69 | } 70 | } 71 | 72 | func TestAmqpQos_Update(t *testing.T) { 73 | q := NewAmqpQos(5, 10) 74 | q.Update(10, 20) 75 | 76 | if q.prefetchCount != 10 { 77 | t.Fatalf("Update: Expected prefetchCount %d, actual %d", 10, q.prefetchCount) 78 | } 79 | 80 | if q.prefetchSize != 20 { 81 | t.Fatalf("Update: Expected prefetchSize %d, actual %d", 20, q.prefetchSize) 82 | } 83 | } 84 | 85 | func TestAmqpQos_Release(t *testing.T) { 86 | q := NewAmqpQos(5, 10) 87 | q.Inc(1, 1) 88 | q.Release() 89 | 90 | if q.currentCount != 0 { 91 | t.Fatalf("Release: Expected currentCount %d, actual %d", 0, q.currentCount) 92 | } 93 | 94 | if q.currentSize != 0 { 95 | t.Fatalf("Release: Expected currentSize %d, actual %d", 0, q.currentCount) 96 | } 97 | } 98 | 99 | func TestAmqpQos_Copy(t *testing.T) { 100 | q := NewAmqpQos(5, 10) 101 | q.Inc(1, 6) 102 | 103 | q2 := q.Copy() 104 | q.Release() 105 | 106 | if q2.currentCount != 1 { 107 | t.Fatalf("Expected currentCount %d, actual %d", 0, q.currentCount) 108 | } 109 | 110 | if q2.currentSize != 6 { 111 | t.Fatalf("Expected currentSize %d, actual %d", 0, q.currentCount) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /queue/queue_consumer_test.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "github.com/valinurovam/garagemq/qos" 5 | ) 6 | 7 | // ConsumerMock implements AMQP consumer mock 8 | type ConsumerMock struct { 9 | tag string 10 | cancel bool 11 | } 12 | 13 | // Consume send signal into consumer channel, than consumer can try to pop message from queue 14 | func (consumer *ConsumerMock) Consume() bool { 15 | return true 16 | } 17 | 18 | // Stop stops consumer and remove it from queue consumers list 19 | func (consumer *ConsumerMock) Stop() { 20 | 21 | } 22 | 23 | // Cancel stops consumer and send basic.cancel method to the client 24 | func (consumer *ConsumerMock) Cancel() { 25 | consumer.cancel = true 26 | } 27 | 28 | // Tag returns consumer tag 29 | func (consumer *ConsumerMock) Tag() string { 30 | return consumer.tag 31 | } 32 | 33 | // Qos returns consumer qos rules 34 | func (consumer *ConsumerMock) Qos() []*qos.AmqpQos { 35 | return []*qos.AmqpQos{} 36 | } 37 | -------------------------------------------------------------------------------- /queue/queue_storage_test.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "github.com/valinurovam/garagemq/amqp" 5 | ) 6 | 7 | type MsgStorageMock struct { 8 | add bool 9 | update bool 10 | del bool 11 | purged bool 12 | 13 | messages []*amqp.Message 14 | index map[uint64]int 15 | pos int 16 | } 17 | 18 | func NewStorageMock(msgCap int) *MsgStorageMock { 19 | mock := &MsgStorageMock{} 20 | mock.messages = make([]*amqp.Message, msgCap) 21 | mock.index = make(map[uint64]int) 22 | 23 | return mock 24 | } 25 | 26 | // Add append message into add-queue 27 | func (storage *MsgStorageMock) Add(message *amqp.Message, queue string) error { 28 | storage.add = true 29 | 30 | if storage.messages != nil { 31 | storage.messages[storage.pos] = message 32 | storage.index[message.ID] = storage.pos 33 | storage.pos++ 34 | } 35 | 36 | return nil 37 | } 38 | 39 | // Update append message into update-queue 40 | func (storage *MsgStorageMock) Update(message *amqp.Message, queue string) error { 41 | storage.update = true 42 | return nil 43 | } 44 | 45 | // Del append message into del-queue 46 | func (storage *MsgStorageMock) Del(message *amqp.Message, queue string) error { 47 | storage.del = true 48 | return nil 49 | } 50 | 51 | // PurgeQueue delete messages 52 | func (storage *MsgStorageMock) PurgeQueue(queue string) { 53 | storage.purged = true 54 | } 55 | 56 | func (storage *MsgStorageMock) GetQueueLength(queue string) uint64 { 57 | return uint64(len(storage.messages)) 58 | } 59 | 60 | func (storage *MsgStorageMock) IterateByQueueFromMsgID(queue string, msgID uint64, limit uint64, fn func(message *amqp.Message)) uint64 { 61 | if storage.messages != nil { 62 | var startPos int 63 | var ok bool 64 | if startPos, ok = storage.index[msgID]; !ok { 65 | msgID++ 66 | 67 | if startPos, ok = storage.index[msgID]; !ok { 68 | return 0 69 | } 70 | } 71 | 72 | var iterated uint64 73 | for i := startPos; i < len(storage.messages); i++ { 74 | fn(storage.messages[i]) 75 | iterated++ 76 | 77 | if iterated == limit { 78 | break 79 | } 80 | } 81 | 82 | return iterated 83 | } 84 | 85 | return 0 86 | } 87 | -------------------------------------------------------------------------------- /readme/channels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valinurovam/garagemq/becd122222a0028eb64343f4d7391ee2ec7a321e/readme/channels.jpg -------------------------------------------------------------------------------- /readme/connections.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valinurovam/garagemq/becd122222a0028eb64343f4d7391ee2ec7a321e/readme/connections.jpg -------------------------------------------------------------------------------- /readme/exchanges.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valinurovam/garagemq/becd122222a0028eb64343f4d7391ee2ec7a321e/readme/exchanges.jpg -------------------------------------------------------------------------------- /readme/overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valinurovam/garagemq/becd122222a0028eb64343f4d7391ee2ec7a321e/readme/overview.jpg -------------------------------------------------------------------------------- /readme/queues.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valinurovam/garagemq/becd122222a0028eb64343f4d7391ee2ec7a321e/readme/queues.jpg -------------------------------------------------------------------------------- /safequeue/safequeue.go: -------------------------------------------------------------------------------- 1 | package safequeue 2 | 3 | import ( 4 | "github.com/sasha-s/go-deadlock" 5 | 6 | "github.com/valinurovam/garagemq/amqp" 7 | ) 8 | 9 | // We change item's type from {}interface to *amqp.Message, cause we know that we use safequeue only in AMQP context 10 | // TODO Move safe queue into amqp package 11 | 12 | // SafeQueue represents simple FIFO queue 13 | // TODO Is that implementation faster? test simple slice queue 14 | type SafeQueue struct { 15 | deadlock.RWMutex 16 | shards [][]*amqp.Message 17 | shardSize int 18 | tailIdx int 19 | tail []*amqp.Message 20 | tailPos int 21 | headIdx int 22 | head []*amqp.Message 23 | headPos int 24 | length uint64 25 | } 26 | 27 | // NewSafeQueue returns new instance of queue 28 | func NewSafeQueue(shardSize int) *SafeQueue { 29 | queue := &SafeQueue{ 30 | shardSize: shardSize, 31 | shards: [][]*amqp.Message{make([]*amqp.Message, shardSize)}, 32 | } 33 | 34 | queue.tailIdx = 0 35 | queue.tail = queue.shards[queue.tailIdx] 36 | queue.headIdx = 0 37 | queue.head = queue.shards[queue.headIdx] 38 | return queue 39 | } 40 | 41 | // Push adds message into queue tail 42 | func (queue *SafeQueue) Push(item *amqp.Message) { 43 | queue.Lock() 44 | defer queue.Unlock() 45 | 46 | queue.tail[queue.tailPos] = item 47 | queue.tailPos++ 48 | queue.length++ 49 | 50 | if queue.tailPos == queue.shardSize { 51 | queue.tailPos = 0 52 | queue.tailIdx = len(queue.shards) 53 | 54 | buffer := make([][]*amqp.Message, len(queue.shards)+1) 55 | buffer[queue.tailIdx] = make([]*amqp.Message, queue.shardSize) 56 | copy(buffer, queue.shards) 57 | 58 | queue.shards = buffer 59 | queue.tail = queue.shards[queue.tailIdx] 60 | } 61 | } 62 | 63 | // PushHead adds message into queue head 64 | func (queue *SafeQueue) PushHead(item *amqp.Message) { 65 | queue.Lock() 66 | defer queue.Unlock() 67 | 68 | if queue.headPos == 0 { 69 | buffer := make([][]*amqp.Message, len(queue.shards)+1) 70 | copy(buffer[1:], queue.shards) 71 | buffer[queue.headIdx] = make([]*amqp.Message, queue.shardSize) 72 | 73 | queue.shards = buffer 74 | queue.tailIdx++ 75 | queue.headPos = queue.shardSize 76 | queue.tail = queue.shards[queue.tailIdx] 77 | queue.head = queue.shards[queue.headIdx] 78 | } 79 | queue.length++ 80 | queue.headPos-- 81 | queue.head[queue.headPos] = item 82 | } 83 | 84 | // Pop retrieves message from head 85 | func (queue *SafeQueue) Pop() (item *amqp.Message) { 86 | queue.Lock() 87 | item = queue.DirtyPop() 88 | queue.Unlock() 89 | return 90 | } 91 | 92 | // DirtyPop retrieves message from head 93 | // This method is not thread safe 94 | func (queue *SafeQueue) DirtyPop() (item *amqp.Message) { 95 | item, queue.head[queue.headPos] = queue.head[queue.headPos], nil 96 | if item == nil { 97 | return item 98 | } 99 | queue.headPos++ 100 | queue.length-- 101 | if queue.headPos == queue.shardSize { 102 | buffer := make([][]*amqp.Message, len(queue.shards)-1) 103 | copy(buffer, queue.shards[queue.headIdx+1:]) 104 | 105 | queue.shards = buffer 106 | 107 | queue.headPos = 0 108 | queue.tailIdx-- 109 | queue.head = queue.shards[queue.headIdx] 110 | } 111 | return 112 | } 113 | 114 | // Length returns queue length 115 | func (queue *SafeQueue) Length() uint64 { 116 | queue.RLock() 117 | defer queue.RUnlock() 118 | return queue.length 119 | } 120 | 121 | // DirtyLength returns queue length 122 | // This method is not thread safe 123 | func (queue *SafeQueue) DirtyLength() uint64 { 124 | return queue.length 125 | } 126 | 127 | // HeadItem returns current head message 128 | // This method is not thread safe 129 | func (queue *SafeQueue) HeadItem() (res *amqp.Message) { 130 | return queue.head[queue.headPos] 131 | } 132 | 133 | // DirtyPurge clear queue 134 | // This method is not thread safe 135 | func (queue *SafeQueue) DirtyPurge() { 136 | queue.shards = [][]*amqp.Message{make([]*amqp.Message, queue.shardSize)} 137 | queue.tailIdx = 0 138 | queue.tail = queue.shards[queue.tailIdx] 139 | queue.headIdx = 0 140 | queue.head = queue.shards[queue.headIdx] 141 | queue.length = 0 142 | } 143 | 144 | // Purge clear queue 145 | func (queue *SafeQueue) Purge() { 146 | queue.Lock() 147 | defer queue.Unlock() 148 | queue.DirtyPurge() 149 | } 150 | -------------------------------------------------------------------------------- /safequeue/safequeue_test.go: -------------------------------------------------------------------------------- 1 | package safequeue 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/valinurovam/garagemq/amqp" 7 | ) 8 | 9 | const SIZE = 65536 10 | 11 | func TestSafeQueue(t *testing.T) { 12 | queue := NewSafeQueue(SIZE) 13 | queueLength := SIZE * 8 14 | for item := 0; item < queueLength; item++ { 15 | message := &amqp.Message{ID: uint64(item)} 16 | queue.Push(message) 17 | } 18 | 19 | if queue.Length() != uint64(queueLength) { 20 | t.Fatalf("expected %d elements, have %d", queueLength, queue.Length()) 21 | } 22 | 23 | for item := 0; item < queueLength; item++ { 24 | pop := queue.Pop() 25 | if uint64(item) != pop.ID { 26 | t.Fatalf("Pop: expected %d, actual %d", item, pop.ID) 27 | } 28 | } 29 | 30 | if queue.Length() != 0 { 31 | t.Fatalf("expected %d elements, have %d", 0, queue.Length()) 32 | } 33 | } 34 | 35 | func TestSafeQueue_PushHead(t *testing.T) { 36 | queue := NewSafeQueue(SIZE) 37 | queueLength := SIZE * 8 38 | for item := 0; item < queueLength; item++ { 39 | message := &amqp.Message{ID: uint64(item)} 40 | queue.Push(message) 41 | queue.PushHead(message) 42 | } 43 | 44 | if queue.Length() != uint64(queueLength*2) { 45 | t.Fatalf("expected %d elements, have %d", queueLength, queue.Length()) 46 | } 47 | 48 | var expected int 49 | for item := 0; item < queueLength*2; item++ { 50 | pop := queue.Pop() 51 | if queueLength > item { 52 | expected = queueLength - item - 1 53 | } else { 54 | expected = item - queueLength 55 | } 56 | if uint64(expected) != pop.ID { 57 | t.Fatalf("Pop: expected %d, actual %d", expected, pop.ID) 58 | } 59 | } 60 | 61 | if queue.Length() != 0 { 62 | t.Fatalf("expected %d elements, have %d", 0, queue.Length()) 63 | } 64 | } 65 | 66 | func TestSafeQueue_HeadItem(t *testing.T) { 67 | queue := NewSafeQueue(SIZE) 68 | queueLength := SIZE 69 | for item := 0; item < queueLength; item++ { 70 | message := &amqp.Message{ID: uint64(item)} 71 | queue.Push(message) 72 | } 73 | 74 | if h := queue.HeadItem(); h.ID != 0 { 75 | t.Fatalf("expected head %v, actual %v", 0, h) 76 | } 77 | } 78 | 79 | func TestSafeQueue_DirtyLength(t *testing.T) { 80 | queue := NewSafeQueue(SIZE) 81 | queueLength := SIZE 82 | for item := 0; item < queueLength; item++ { 83 | message := &amqp.Message{ID: uint64(item)} 84 | queue.Push(message) 85 | } 86 | 87 | if queue.Length() != queue.DirtyLength() { 88 | t.Fatal("Single thread DirtyLength must be equal Length") 89 | } 90 | } 91 | 92 | func TestSafeQueue_Purge(t *testing.T) { 93 | queue := NewSafeQueue(SIZE) 94 | queueLength := SIZE 95 | for item := 0; item < queueLength; item++ { 96 | message := &amqp.Message{ID: uint64(item)} 97 | queue.Push(message) 98 | } 99 | 100 | queue.Purge() 101 | 102 | if queue.Length() != 0 { 103 | t.Fatalf("expected %d elements, actual %d", 0, queue.Length()) 104 | } 105 | 106 | pop := queue.Pop() 107 | if nil != pop { 108 | t.Fatalf("Pop: expected %v, actual %v", nil, pop) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /server/basicMethods.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/valinurovam/garagemq/amqp" 5 | "github.com/valinurovam/garagemq/consumer" 6 | "github.com/valinurovam/garagemq/qos" 7 | "github.com/valinurovam/garagemq/queue" 8 | ) 9 | 10 | func (channel *Channel) basicRoute(method amqp.Method) *amqp.Error { 11 | switch method := method.(type) { 12 | case *amqp.BasicQos: 13 | return channel.basicQos(method) 14 | case *amqp.BasicPublish: 15 | return channel.basicPublish(method) 16 | case *amqp.BasicConsume: 17 | return channel.basicConsume(method) 18 | case *amqp.BasicAck: 19 | return channel.basicAck(method) 20 | case *amqp.BasicNack: 21 | return channel.basicNack(method) 22 | case *amqp.BasicReject: 23 | return channel.basicReject(method) 24 | case *amqp.BasicCancel: 25 | return channel.basicCancel(method) 26 | case *amqp.BasicGet: 27 | return channel.basicGet(method) 28 | } 29 | 30 | return amqp.NewConnectionError(amqp.NotImplemented, "unable to route basic method "+method.Name(), method.ClassIdentifier(), method.MethodIdentifier()) 31 | } 32 | 33 | func (channel *Channel) basicQos(method *amqp.BasicQos) (err *amqp.Error) { 34 | channel.updateQos(method.PrefetchCount, method.PrefetchSize, method.Global) 35 | channel.SendMethod(&amqp.BasicQosOk{}) 36 | 37 | return nil 38 | } 39 | 40 | func (channel *Channel) basicAck(method *amqp.BasicAck) (err *amqp.Error) { 41 | return channel.handleAck(method) 42 | } 43 | 44 | func (channel *Channel) basicNack(method *amqp.BasicNack) (err *amqp.Error) { 45 | return channel.handleReject(method.DeliveryTag, method.Multiple, method.Requeue, method) 46 | } 47 | 48 | func (channel *Channel) basicReject(method *amqp.BasicReject) (err *amqp.Error) { 49 | return channel.handleReject(method.DeliveryTag, false, method.Requeue, method) 50 | } 51 | 52 | func (channel *Channel) basicPublish(method *amqp.BasicPublish) (err *amqp.Error) { 53 | if method.Immediate { 54 | return amqp.NewChannelError(amqp.NotImplemented, "Immediate = true", method.ClassIdentifier(), method.MethodIdentifier()) 55 | } 56 | 57 | if _, err = channel.getExchangeWithError(method.Exchange, method); err != nil { 58 | return err 59 | } 60 | 61 | channel.currentMessage = amqp.NewMessage(method) 62 | if channel.confirmMode { 63 | channel.currentMessage.ConfirmMeta = &amqp.ConfirmMeta{ 64 | ChanID: channel.id, 65 | ConnID: channel.conn.id, 66 | DeliveryTag: channel.nextConfirmDeliveryTag(), 67 | } 68 | } 69 | return nil 70 | } 71 | 72 | func (channel *Channel) basicConsume(method *amqp.BasicConsume) (err *amqp.Error) { 73 | var cmr *consumer.Consumer 74 | if cmr, err = channel.addConsumer(method); err != nil { 75 | return err 76 | } 77 | 78 | if !method.NoWait { 79 | channel.SendMethod(&amqp.BasicConsumeOk{ConsumerTag: cmr.Tag()}) 80 | } 81 | 82 | cmr.Start() 83 | 84 | return nil 85 | } 86 | 87 | func (channel *Channel) basicCancel(method *amqp.BasicCancel) (err *amqp.Error) { 88 | if _, ok := channel.consumers[method.ConsumerTag]; !ok { 89 | return amqp.NewChannelError(amqp.NotFound, "Consumer not found", method.ClassIdentifier(), method.MethodIdentifier()) 90 | } 91 | channel.removeConsumer(method.ConsumerTag) 92 | channel.SendMethod(&amqp.BasicCancelOk{ConsumerTag: method.ConsumerTag}) 93 | return nil 94 | } 95 | 96 | func (channel *Channel) basicGet(method *amqp.BasicGet) (err *amqp.Error) { 97 | var qu *queue.Queue 98 | var message *amqp.Message 99 | if qu, err = channel.getQueueWithError(method.Queue, method); err != nil { 100 | return err 101 | } 102 | 103 | if method.NoAck { 104 | message = qu.Pop() 105 | } else { 106 | message = qu.PopQos([]*qos.AmqpQos{channel.qos, channel.conn.qos}) 107 | } 108 | 109 | // how to handle if queue is not empty, but qos triggered and message is nil 110 | if message == nil { 111 | channel.SendMethod(&amqp.BasicGetEmpty{}) 112 | return nil 113 | } 114 | 115 | dTag := channel.NextDeliveryTag() 116 | if !method.NoAck { 117 | channel.AddUnackedMessage(dTag, "", qu.GetName(), message) 118 | 119 | qu.GetMetrics().Unacked.Counter.Inc(1) 120 | channel.server.GetMetrics().Unacked.Counter.Inc(1) 121 | } else { 122 | qu.GetMetrics().Total.Counter.Dec(1) 123 | qu.GetMetrics().ServerTotal.Counter.Dec(1) 124 | } 125 | 126 | channel.SendContent(&amqp.BasicGetOk{ 127 | DeliveryTag: dTag, 128 | Redelivered: false, 129 | Exchange: message.Exchange, 130 | RoutingKey: message.RoutingKey, 131 | MessageCount: 1, 132 | }, message) 133 | 134 | channel.server.GetMetrics().Get.Counter.Inc(1) 135 | channel.metrics.Get.Counter.Inc(1) 136 | qu.GetMetrics().Get.Counter.Inc(1) 137 | 138 | qu.GetMetrics().Ready.Counter.Dec(1) 139 | qu.GetMetrics().ServerReady.Counter.Dec(1) 140 | 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /server/channelMethods.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/valinurovam/garagemq/amqp" 5 | ) 6 | 7 | func (channel *Channel) channelRoute(method amqp.Method) *amqp.Error { 8 | switch method := method.(type) { 9 | case *amqp.ChannelOpen: 10 | return channel.channelOpen(method) 11 | case *amqp.ChannelClose: 12 | return channel.channelClose(method) 13 | case *amqp.ChannelCloseOk: 14 | return channel.channelCloseOk(method) 15 | case *amqp.ChannelFlow: 16 | return channel.channelFlow(method) 17 | } 18 | 19 | return amqp.NewConnectionError(amqp.NotImplemented, "unable to route channel method "+method.Name(), method.ClassIdentifier(), method.MethodIdentifier()) 20 | } 21 | 22 | func (channel *Channel) channelOpen(method *amqp.ChannelOpen) (err *amqp.Error) { 23 | // @spec-note 24 | // The client MUST NOT use this method on an already­opened channel. 25 | if channel.status == channelOpen { 26 | return amqp.NewConnectionError(amqp.ChannelError, "channel already open", method.ClassIdentifier(), method.MethodIdentifier()) 27 | } 28 | 29 | channel.SendMethod(&amqp.ChannelOpenOk{}) 30 | channel.status = channelOpen 31 | 32 | return nil 33 | } 34 | 35 | func (channel *Channel) channelClose(method *amqp.ChannelClose) (err *amqp.Error) { 36 | channel.status = channelClosed 37 | channel.SendMethod(&amqp.ChannelCloseOk{}) 38 | channel.close() 39 | return nil 40 | } 41 | 42 | func (channel *Channel) channelCloseOk(method *amqp.ChannelCloseOk) (err *amqp.Error) { 43 | channel.status = channelClosed 44 | return nil 45 | } 46 | 47 | func (channel *Channel) channelFlow(method *amqp.ChannelFlow) (err *amqp.Error) { 48 | channel.changeFlow(method.Active) 49 | channel.SendMethod(&amqp.ChannelFlowOk{Active: method.Active}) 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /server/confirmMethods.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/valinurovam/garagemq/amqp" 5 | ) 6 | 7 | func (channel *Channel) confirmRoute(method amqp.Method) *amqp.Error { 8 | switch method := method.(type) { 9 | case *amqp.ConfirmSelect: 10 | return channel.confirmSelect(method) 11 | } 12 | 13 | return amqp.NewConnectionError(amqp.NotImplemented, "unable to route channel method "+method.Name(), method.ClassIdentifier(), method.MethodIdentifier()) 14 | } 15 | 16 | func (channel *Channel) confirmSelect(method *amqp.ConfirmSelect) (err *amqp.Error) { 17 | channel.confirmMode = true 18 | go channel.sendConfirms() 19 | if !method.Nowait { 20 | channel.SendMethod(&amqp.ConfirmSelectOk{}) 21 | } 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /server/connectionMethods.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | 7 | "github.com/valinurovam/garagemq/amqp" 8 | "github.com/valinurovam/garagemq/auth" 9 | ) 10 | 11 | func (channel *Channel) connectionRoute(method amqp.Method) *amqp.Error { 12 | switch method := method.(type) { 13 | case *amqp.ConnectionStartOk: 14 | return channel.connectionStartOk(method) 15 | case *amqp.ConnectionTuneOk: 16 | return channel.connectionTuneOk(method) 17 | case *amqp.ConnectionOpen: 18 | return channel.connectionOpen(method) 19 | case *amqp.ConnectionClose: 20 | return channel.connectionClose(method) 21 | case *amqp.ConnectionCloseOk: 22 | return channel.connectionCloseOk(method) 23 | } 24 | 25 | return amqp.NewConnectionError(amqp.NotImplemented, "unable to route connection method", method.ClassIdentifier(), method.MethodIdentifier()) 26 | } 27 | 28 | func (channel *Channel) connectionStart() { 29 | defer channel.wg.Done() 30 | 31 | var capabilities = amqp.Table{} 32 | capabilities["publisher_confirms"] = true 33 | capabilities["exchange_exchange_bindings"] = false 34 | capabilities["basic.nack"] = true 35 | capabilities["consumer_cancel_notify"] = true 36 | capabilities["connection.blocked"] = false 37 | capabilities["consumer_priorities"] = false 38 | capabilities["authentication_failure_close"] = true 39 | capabilities["per_consumer_qos"] = true 40 | 41 | var serverProps = amqp.Table{} 42 | serverProps["product"] = "garagemq" 43 | serverProps["version"] = "0.1" 44 | serverProps["copyright"] = "Alexander Valinurov, 2018" 45 | serverProps["platform"] = runtime.GOARCH 46 | serverProps["capabilities"] = capabilities 47 | host, err := os.Hostname() 48 | if err != nil { 49 | serverProps["host"] = "UnknownHostError" 50 | } else { 51 | serverProps["host"] = host 52 | } 53 | 54 | var method = amqp.ConnectionStart{VersionMajor: 0, VersionMinor: 9, ServerProperties: &serverProps, Mechanisms: []byte("PLAIN"), Locales: []byte("en_US")} 55 | channel.SendMethod(&method) 56 | 57 | channel.conn.status = ConnStart 58 | } 59 | 60 | func (channel *Channel) connectionStartOk(method *amqp.ConnectionStartOk) *amqp.Error { 61 | channel.conn.status = ConnStartOK 62 | 63 | var saslData auth.SaslData 64 | var err error 65 | if saslData, err = auth.ParsePlain(method.Response); err != nil { 66 | return amqp.NewConnectionError(amqp.NotAllowed, "login failure", method.ClassIdentifier(), method.MethodIdentifier()) 67 | } 68 | 69 | if method.Mechanism != auth.SaslPlain { 70 | channel.conn.close() 71 | } 72 | 73 | if !channel.server.checkAuth(saslData) { 74 | return amqp.NewConnectionError(amqp.NotAllowed, "login failure", method.ClassIdentifier(), method.MethodIdentifier()) 75 | } 76 | channel.conn.userName = saslData.Username 77 | channel.conn.clientProperties = method.ClientProperties 78 | 79 | // @todo Send HeartBeat 0 cause not supported yet 80 | channel.SendMethod(&amqp.ConnectionTune{ 81 | ChannelMax: channel.conn.maxChannels, 82 | FrameMax: channel.conn.maxFrameSize, 83 | Heartbeat: channel.conn.heartbeatInterval, 84 | }) 85 | channel.conn.status = ConnTune 86 | 87 | return nil 88 | } 89 | 90 | func (channel *Channel) connectionTuneOk(method *amqp.ConnectionTuneOk) *amqp.Error { 91 | channel.conn.status = ConnTuneOK 92 | 93 | if method.ChannelMax > channel.conn.maxChannels || method.FrameMax > channel.conn.maxFrameSize { 94 | channel.conn.close() 95 | return nil 96 | } 97 | 98 | channel.conn.maxChannels = method.ChannelMax 99 | channel.conn.maxFrameSize = method.FrameMax 100 | 101 | if method.Heartbeat > 0 { 102 | if method.Heartbeat < channel.conn.heartbeatInterval { 103 | channel.conn.heartbeatInterval = method.Heartbeat 104 | } 105 | channel.conn.heartbeatTimeout = channel.conn.heartbeatInterval * 3 106 | 107 | channel.conn.wg.Add(1) 108 | go channel.conn.heartBeater() 109 | } 110 | 111 | return nil 112 | } 113 | 114 | func (channel *Channel) connectionOpen(method *amqp.ConnectionOpen) *amqp.Error { 115 | channel.conn.status = ConnOpen 116 | var vhostFound bool 117 | if channel.conn.virtualHost, vhostFound = channel.server.vhosts[method.VirtualHost]; !vhostFound { 118 | return amqp.NewConnectionError(amqp.InvalidPath, "virtualHost '"+method.VirtualHost+"' does not exist", method.ClassIdentifier(), method.MethodIdentifier()) 119 | } 120 | 121 | channel.conn.vhostName = method.VirtualHost 122 | 123 | channel.SendMethod(&amqp.ConnectionOpenOk{}) 124 | channel.conn.status = ConnOpenOK 125 | 126 | channel.logger.Info("AMQP connection open") 127 | return nil 128 | } 129 | 130 | func (channel *Channel) connectionClose(method *amqp.ConnectionClose) *amqp.Error { 131 | channel.logger.Infof("Connection closed by client, reason - [%d] %s", method.ReplyCode, method.ReplyText) 132 | channel.SendMethod(&amqp.ConnectionCloseOk{}) 133 | return nil 134 | } 135 | 136 | func (channel *Channel) connectionCloseOk(method *amqp.ConnectionCloseOk) *amqp.Error { 137 | go channel.conn.close() 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /server/daemon.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package server 5 | 6 | func daemonReady() { 7 | } 8 | -------------------------------------------------------------------------------- /server/daemon_linux.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/coreos/go-systemd/daemon" 5 | ) 6 | 7 | func daemonReady() { 8 | // signal readiness, ignore errors 9 | daemon.SdNotify(false, daemon.SdNotifyReady) 10 | } 11 | -------------------------------------------------------------------------------- /server/exchangeMethods.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/valinurovam/garagemq/amqp" 8 | "github.com/valinurovam/garagemq/exchange" 9 | ) 10 | 11 | func (channel *Channel) exchangeRoute(method amqp.Method) *amqp.Error { 12 | switch method := method.(type) { 13 | case *amqp.ExchangeDeclare: 14 | return channel.exchangeDeclare(method) 15 | case *amqp.ExchangeDelete: 16 | return channel.exchangeDelete(method) 17 | } 18 | 19 | return amqp.NewConnectionError(amqp.NotImplemented, "unable to route queue method "+method.Name(), method.ClassIdentifier(), method.MethodIdentifier()) 20 | } 21 | 22 | func (channel *Channel) exchangeDeclare(method *amqp.ExchangeDeclare) *amqp.Error { 23 | exTypeId, err := exchange.GetExchangeTypeID(method.Type) 24 | if err != nil { 25 | return amqp.NewChannelError(amqp.NotImplemented, err.Error(), method.ClassIdentifier(), method.MethodIdentifier()) 26 | } 27 | 28 | if method.Exchange == "" { 29 | return amqp.NewChannelError( 30 | amqp.CommandInvalid, 31 | "exchange name is required", 32 | method.ClassIdentifier(), 33 | method.MethodIdentifier(), 34 | ) 35 | } 36 | 37 | existingExchange := channel.conn.GetVirtualHost().GetExchange(method.Exchange) 38 | if method.Passive { 39 | if method.NoWait { 40 | return nil 41 | } 42 | 43 | if existingExchange == nil { 44 | return amqp.NewChannelError( 45 | amqp.NotFound, 46 | fmt.Sprintf("exchange '%s' not found", method.Exchange), 47 | method.ClassIdentifier(), 48 | method.MethodIdentifier(), 49 | ) 50 | } 51 | 52 | channel.SendMethod(&amqp.ExchangeDeclareOk{}) 53 | 54 | return nil 55 | } 56 | 57 | if strings.HasPrefix(method.Exchange, "amq.") { 58 | return amqp.NewChannelError( 59 | amqp.AccessRefused, 60 | fmt.Sprintf("exchange name '%s' contains reserved prefix 'amq.*'", method.Exchange), 61 | method.ClassIdentifier(), 62 | method.MethodIdentifier(), 63 | ) 64 | } 65 | 66 | newExchange := exchange.NewExchange( 67 | method.Exchange, 68 | exTypeId, 69 | method.Durable, 70 | method.AutoDelete, 71 | method.Internal, 72 | false, 73 | ) 74 | 75 | if existingExchange != nil { 76 | if err := existingExchange.EqualWithErr(newExchange); err != nil { 77 | return amqp.NewChannelError( 78 | amqp.PreconditionFailed, 79 | err.Error(), 80 | method.ClassIdentifier(), 81 | method.MethodIdentifier(), 82 | ) 83 | } 84 | channel.SendMethod(&amqp.ExchangeDeclareOk{}) 85 | return nil 86 | } 87 | 88 | channel.conn.GetVirtualHost().AppendExchange(newExchange) 89 | if !method.NoWait { 90 | channel.SendMethod(&amqp.ExchangeDeclareOk{}) 91 | } 92 | 93 | return nil 94 | } 95 | 96 | func (channel *Channel) exchangeDelete(method *amqp.ExchangeDelete) *amqp.Error { 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /server/queueMethods.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/valinurovam/garagemq/amqp" 5 | "github.com/valinurovam/garagemq/binding" 6 | "github.com/valinurovam/garagemq/exchange" 7 | "github.com/valinurovam/garagemq/queue" 8 | ) 9 | 10 | func (channel *Channel) queueRoute(method amqp.Method) *amqp.Error { 11 | switch method := method.(type) { 12 | case *amqp.QueueDeclare: 13 | return channel.queueDeclare(method) 14 | case *amqp.QueueBind: 15 | return channel.queueBind(method) 16 | case *amqp.QueueUnbind: 17 | return channel.queueUnbind(method) 18 | case *amqp.QueuePurge: 19 | return channel.queuePurge(method) 20 | case *amqp.QueueDelete: 21 | return channel.queueDelete(method) 22 | } 23 | 24 | return amqp.NewConnectionError(amqp.NotImplemented, "Unable to route queue method "+method.Name(), method.ClassIdentifier(), method.MethodIdentifier()) 25 | } 26 | 27 | func (channel *Channel) queueDeclare(method *amqp.QueueDeclare) *amqp.Error { 28 | var existingQueue *queue.Queue 29 | var notFoundErr, exclusiveErr *amqp.Error 30 | 31 | if method.Queue == "" { 32 | return amqp.NewChannelError( 33 | amqp.CommandInvalid, 34 | "queue name is required", 35 | method.ClassIdentifier(), 36 | method.MethodIdentifier(), 37 | ) 38 | } 39 | 40 | existingQueue, notFoundErr = channel.getQueueWithError(method.Queue, method) 41 | exclusiveErr = channel.checkQueueLockWithError(existingQueue, method) 42 | 43 | if method.Passive { 44 | if method.NoWait { 45 | return nil 46 | } 47 | 48 | if existingQueue == nil { 49 | return notFoundErr 50 | } 51 | 52 | if exclusiveErr != nil { 53 | return exclusiveErr 54 | } 55 | 56 | channel.SendMethod(&amqp.QueueDeclareOk{ 57 | Queue: method.Queue, 58 | MessageCount: uint32(existingQueue.Length()), 59 | ConsumerCount: uint32(existingQueue.ConsumersCount()), 60 | }) 61 | 62 | return nil 63 | } 64 | 65 | newQueue := channel.conn.GetVirtualHost().NewQueue( 66 | method.Queue, 67 | channel.conn.id, 68 | method.Exclusive, 69 | method.AutoDelete, 70 | method.Durable, 71 | channel.server.config.Queue.ShardSize, 72 | ) 73 | 74 | if existingQueue != nil { 75 | if exclusiveErr != nil { 76 | return exclusiveErr 77 | } 78 | 79 | if err := existingQueue.EqualWithErr(newQueue); err != nil { 80 | return amqp.NewChannelError( 81 | amqp.PreconditionFailed, 82 | err.Error(), 83 | method.ClassIdentifier(), 84 | method.MethodIdentifier(), 85 | ) 86 | } 87 | 88 | channel.SendMethod(&amqp.QueueDeclareOk{ 89 | Queue: method.Queue, 90 | MessageCount: uint32(existingQueue.Length()), 91 | ConsumerCount: uint32(existingQueue.ConsumersCount()), 92 | }) 93 | return nil 94 | } 95 | 96 | if err := newQueue.Start(); err != nil { 97 | return amqp.NewChannelError( 98 | amqp.InternalError, 99 | err.Error(), 100 | method.ClassIdentifier(), 101 | method.MethodIdentifier(), 102 | ) 103 | } 104 | err := channel.conn.GetVirtualHost().AppendQueue(newQueue) 105 | if err != nil { 106 | return amqp.NewChannelError( 107 | amqp.PreconditionFailed, 108 | err.Error(), 109 | method.ClassIdentifier(), 110 | method.MethodIdentifier(), 111 | ) 112 | } 113 | channel.SendMethod(&amqp.QueueDeclareOk{ 114 | Queue: method.Queue, 115 | MessageCount: 0, 116 | ConsumerCount: 0, 117 | }) 118 | 119 | return nil 120 | } 121 | 122 | func (channel *Channel) queueBind(method *amqp.QueueBind) *amqp.Error { 123 | var ex *exchange.Exchange 124 | var qu *queue.Queue 125 | var err *amqp.Error 126 | 127 | if ex, err = channel.getExchangeWithError(method.Exchange, method); err != nil { 128 | return err 129 | } 130 | 131 | // @spec-note 132 | // The server MUST NOT allow clients to access the default exchange except by specifying an empty exchange name in the Queue.Bind and content Publish methods. 133 | if ex.GetName() == exDefaultName { 134 | return amqp.NewChannelError( 135 | amqp.AccessRefused, 136 | "operation not permitted on the default exchange", 137 | method.ClassIdentifier(), 138 | method.MethodIdentifier(), 139 | ) 140 | } 141 | 142 | if qu, err = channel.getQueueWithError(method.Queue, method); err != nil { 143 | return err 144 | } 145 | 146 | if err = channel.checkQueueLockWithError(qu, method); err != nil { 147 | return err 148 | } 149 | 150 | bind, bindErr := binding.NewBinding(method.Queue, method.Exchange, 151 | method.RoutingKey, method.Arguments, ex.ExType() == exchange.ExTypeTopic) 152 | if bindErr != nil { 153 | return amqp.NewChannelError( 154 | amqp.PreconditionFailed, 155 | bindErr.Error(), 156 | method.ClassIdentifier(), 157 | method.MethodIdentifier(), 158 | ) 159 | 160 | } 161 | 162 | ex.AppendBinding(bind) 163 | 164 | // @spec-note 165 | // Bindings of durable queues to durable exchanges are automatically durable and the server MUST restore such bindings after a server restart. 166 | if ex.IsDurable() && qu.IsDurable() { 167 | channel.conn.GetVirtualHost().PersistBinding(bind) 168 | } 169 | 170 | if !method.NoWait { 171 | channel.SendMethod(&amqp.QueueBindOk{}) 172 | } 173 | 174 | return nil 175 | } 176 | 177 | func (channel *Channel) queueUnbind(method *amqp.QueueUnbind) *amqp.Error { 178 | var ex *exchange.Exchange 179 | var qu *queue.Queue 180 | var err *amqp.Error 181 | 182 | if ex, err = channel.getExchangeWithError(method.Exchange, method); err != nil { 183 | return err 184 | } 185 | 186 | if qu, err = channel.getQueueWithError(method.Queue, method); err != nil { 187 | return err 188 | } 189 | 190 | if err = channel.checkQueueLockWithError(qu, method); err != nil { 191 | return err 192 | } 193 | 194 | bind, bindErr := binding.NewBinding(method.Queue, method.Exchange, method.RoutingKey, method.Arguments, ex.ExType() == exchange.ExTypeTopic) 195 | 196 | if bindErr != nil { 197 | return amqp.NewConnectionError( 198 | amqp.PreconditionFailed, 199 | bindErr.Error(), 200 | method.ClassIdentifier(), 201 | method.MethodIdentifier(), 202 | ) 203 | } 204 | 205 | ex.RemoveBinding(bind) 206 | channel.conn.GetVirtualHost().RemoveBindings([]*binding.Binding{bind}) 207 | channel.SendMethod(&amqp.QueueUnbindOk{}) 208 | 209 | return nil 210 | } 211 | 212 | func (channel *Channel) queuePurge(method *amqp.QueuePurge) *amqp.Error { 213 | var qu *queue.Queue 214 | var err *amqp.Error 215 | 216 | if qu, err = channel.getQueueWithError(method.Queue, method); err != nil { 217 | return err 218 | } 219 | 220 | if err = channel.checkQueueLockWithError(qu, method); err != nil { 221 | return err 222 | } 223 | 224 | msgCnt := qu.Purge() 225 | if !method.NoWait { 226 | channel.SendMethod(&amqp.QueuePurgeOk{MessageCount: uint32(msgCnt)}) 227 | } 228 | return nil 229 | } 230 | 231 | func (channel *Channel) queueDelete(method *amqp.QueueDelete) *amqp.Error { 232 | var qu *queue.Queue 233 | var err *amqp.Error 234 | 235 | if qu, err = channel.getQueueWithError(method.Queue, method); err != nil { 236 | return err 237 | } 238 | 239 | if err = channel.checkQueueLockWithError(qu, method); err != nil { 240 | return err 241 | } 242 | 243 | var length, errDel = channel.conn.GetVirtualHost().DeleteQueue(method.Queue, method.IfUnused, method.IfEmpty) 244 | if errDel != nil { 245 | return amqp.NewChannelError(amqp.PreconditionFailed, errDel.Error(), method.ClassIdentifier(), method.MethodIdentifier()) 246 | } 247 | 248 | channel.SendMethod(&amqp.QueueDeleteOk{MessageCount: uint32(length)}) 249 | return nil 250 | } 251 | -------------------------------------------------------------------------------- /server/server_channel_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | amqpclient "github.com/rabbitmq/amqp091-go" 9 | 10 | "github.com/valinurovam/garagemq/amqp" 11 | ) 12 | 13 | func Test_ChannelOpen_Success(t *testing.T) { 14 | sc, _ := getNewSC(getDefaultTestConfig()) 15 | defer sc.clean() 16 | _, err := sc.client.Channel() 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | } 21 | 22 | func Test_ChannelOpen_FailedReopen(t *testing.T) { 23 | sc, _ := getNewSC(getDefaultTestConfig()) 24 | defer sc.clean() 25 | sc.client.Channel() 26 | 27 | // Is here way to test without internal source of server? 28 | channel := getServerChannel(sc, 1) 29 | amqpErr := channel.handleMethod(&amqp.ChannelOpen{}) 30 | if amqpErr == nil { 31 | t.Error("Expected 'channel already open' error") 32 | } 33 | } 34 | 35 | func Test_ChannelClose_Success(t *testing.T) { 36 | sc, _ := getNewSC(getDefaultTestConfig()) 37 | defer sc.clean() 38 | ch, err := sc.client.Channel() 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | 43 | err = ch.Close() 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | } 48 | 49 | func Test_ChannelFlow_Active_Success(t *testing.T) { 50 | sc, _ := getNewSC(getDefaultTestConfig()) 51 | defer sc.clean() 52 | ch, _ := sc.client.Channel() 53 | err := ch.Flow(true) 54 | 55 | if err != nil { 56 | t.Error(err) 57 | } 58 | 59 | channel := getServerChannel(sc, 1) 60 | if channel.isActive() == false { 61 | t.Error("Channel inactive after change flow 'true'") 62 | } 63 | } 64 | 65 | func Test_ChannelFlow_InActive_Success(t *testing.T) { 66 | sc, _ := getNewSC(getDefaultTestConfig()) 67 | defer sc.clean() 68 | ch, _ := sc.client.Channel() 69 | err := ch.Flow(false) 70 | 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | 75 | channel := getServerChannel(sc, 1) 76 | if channel.isActive() == true { 77 | t.Error("Channel active after change flow 'false'") 78 | } 79 | } 80 | 81 | // useless, for coverage only 82 | func Test_ChannelFlow_Failed_FlowOkSend(t *testing.T) { 83 | sc, _ := getNewSC(getDefaultTestConfig()) 84 | defer sc.clean() 85 | ch, _ := sc.client.Channel() 86 | 87 | flowChan := make(chan bool) 88 | closeChan := make(chan *amqpclient.Error, 1) 89 | ch.NotifyFlow(flowChan) 90 | ch.NotifyClose(closeChan) 91 | 92 | channel := getServerChannel(sc, 1) 93 | channel.SendMethod(&amqp.ChannelFlow{Active: false}) 94 | 95 | select { 96 | case <-flowChan: 97 | case <-time.After(100 * time.Millisecond): 98 | } 99 | 100 | for notify := range flowChan { 101 | fmt.Println(notify) 102 | } 103 | 104 | var closeErr *amqpclient.Error 105 | 106 | select { 107 | case closeErr = <-closeChan: 108 | case <-time.After(100 * time.Millisecond): 109 | } 110 | 111 | if closeErr == nil { 112 | t.Error("Expected NOT_IMPLEMENTED error") 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /server/server_confirm_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | amqpclient "github.com/rabbitmq/amqp091-go" 9 | ) 10 | 11 | func Test_Confirm_Success(t *testing.T) { 12 | sc, _ := getNewSC(getDefaultTestConfig()) 13 | defer sc.clean() 14 | ch, _ := sc.client.Channel() 15 | err := ch.Confirm(false) 16 | 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | channel := getServerChannel(sc, 1) 22 | if channel.confirmMode == false { 23 | t.Error("Channel non confirm mode") 24 | } 25 | } 26 | 27 | func Test_ConfirmReceive_Acks_Success(t *testing.T) { 28 | sc, _ := getNewSC(getDefaultTestConfig()) 29 | defer sc.clean() 30 | ch, _ := sc.client.Channel() 31 | ch.Confirm(false) 32 | 33 | msgCount := 10 34 | acks := make(chan uint64, msgCount) 35 | nacks := make(chan uint64, msgCount) 36 | ch.NotifyConfirm(acks, nacks) 37 | 38 | queue, _ := ch.QueueDeclare(t.Name(), false, false, false, false, emptyTable) 39 | 40 | for i := 0; i < msgCount; i++ { 41 | ch.PublishWithContext(context.Background(), "", queue.Name, false, false, amqpclient.Publishing{ContentType: "text/plain", Body: []byte("test")}) 42 | } 43 | 44 | tick := time.After(50 * time.Millisecond) 45 | confirmsCount := 0 46 | leave := false 47 | for { 48 | select { 49 | case <-acks: 50 | confirmsCount++ 51 | case <-nacks: 52 | confirmsCount-- 53 | case <-tick: 54 | leave = true 55 | default: 56 | 57 | } 58 | if leave { 59 | break 60 | } 61 | } 62 | 63 | if confirmsCount != msgCount { 64 | t.Errorf("Expected %d confirms, actual %d", msgCount, confirmsCount) 65 | } 66 | } 67 | 68 | func Test_ConfirmReceive_Acks_NoRoute_Success(t *testing.T) { 69 | sc, _ := getNewSC(getDefaultTestConfig()) 70 | defer sc.clean() 71 | ch, _ := sc.client.Channel() 72 | ch.Confirm(false) 73 | 74 | msgCount := 10 75 | acks := make(chan uint64, msgCount) 76 | nacks := make(chan uint64, msgCount) 77 | ch.NotifyConfirm(acks, nacks) 78 | 79 | ch.QueueDeclare(t.Name(), false, false, false, false, emptyTable) 80 | 81 | for i := 0; i < msgCount; i++ { 82 | ch.PublishWithContext(context.Background(), "", "bad-route", false, false, amqpclient.Publishing{ContentType: "text/plain", Body: []byte("test")}) 83 | } 84 | 85 | tick := time.After(50 * time.Millisecond) 86 | confirmsCount := 0 87 | leave := false 88 | for { 89 | select { 90 | case <-acks: 91 | confirmsCount++ 92 | case <-nacks: 93 | confirmsCount-- 94 | case <-tick: 95 | leave = true 96 | default: 97 | 98 | } 99 | if leave { 100 | break 101 | } 102 | } 103 | 104 | if confirmsCount != msgCount { 105 | t.Errorf("Expected %d confirms, actual %d", msgCount, confirmsCount) 106 | } 107 | } 108 | 109 | func Test_ConfirmReceive_Acks_Persistent_Success(t *testing.T) { 110 | sc, _ := getNewSC(getDefaultTestConfig()) 111 | defer sc.clean() 112 | ch, _ := sc.client.Channel() 113 | ch.Confirm(false) 114 | 115 | msgCount := 10 116 | acks := make(chan uint64, msgCount) 117 | nacks := make(chan uint64, msgCount) 118 | ch.NotifyConfirm(acks, nacks) 119 | 120 | queue, _ := ch.QueueDeclare(t.Name(), true, false, false, false, emptyTable) 121 | 122 | for i := 0; i < msgCount; i++ { 123 | ch.PublishWithContext(context.Background(), "", queue.Name, false, false, amqpclient.Publishing{ContentType: "text/plain", Body: []byte("test"), DeliveryMode: amqpclient.Persistent}) 124 | } 125 | 126 | tick := time.After(50 * time.Millisecond) 127 | confirmsCount := 0 128 | leave := false 129 | for { 130 | select { 131 | case <-acks: 132 | confirmsCount++ 133 | case <-nacks: 134 | confirmsCount-- 135 | case <-tick: 136 | leave = true 137 | default: 138 | 139 | } 140 | if leave { 141 | break 142 | } 143 | } 144 | 145 | if confirmsCount != msgCount { 146 | t.Errorf("Expected %d confirms, actual %d", msgCount, confirmsCount) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /server/server_connection_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/valinurovam/garagemq/config" 7 | ) 8 | 9 | func Test_Connection_Success(t *testing.T) { 10 | sc, err := getNewSC(getDefaultTestConfig()) 11 | defer sc.clean() 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | } 16 | 17 | func Test_Connection_FailedVhostAccess(t *testing.T) { 18 | cfg := getDefaultTestConfig() 19 | cfg.srvConfig.Vhost.DefaultPath = "test" 20 | sc, err := getNewSC(cfg) 21 | defer sc.clean() 22 | if err == nil { 23 | t.Error("Expected no access to vhost error") 24 | } 25 | } 26 | 27 | func Test_Connection_Failed_WhenWrongAuth(t *testing.T) { 28 | cfg := getDefaultTestConfig() 29 | cfg.srvConfig.Users = []config.User{ 30 | { 31 | Username: "guest", 32 | Password: "guest?", 33 | }, 34 | } 35 | sc, err := getNewSC(cfg) 36 | defer sc.clean() 37 | if err == nil { 38 | t.Error("Expected auth error") 39 | } 40 | } 41 | 42 | func Test_Connection_Failed_WhenWrongAuth_UnknownUser(t *testing.T) { 43 | cfg := getDefaultTestConfig() 44 | cfg.srvConfig.Users = []config.User{ 45 | { 46 | Username: "guest_unknown", 47 | Password: "guest", 48 | }, 49 | } 50 | sc, err := getNewSC(cfg) 51 | defer sc.clean() 52 | if err == nil { 53 | t.Error("Expected auth error") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server/server_exchange_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/valinurovam/garagemq/amqp" 7 | "github.com/valinurovam/garagemq/exchange" 8 | ) 9 | 10 | func Test_DefaultExchanges(t *testing.T) { 11 | sc, _ := getNewSC(getDefaultTestConfig()) 12 | defer sc.clean() 13 | vhost := sc.server.getVhost("/") 14 | 15 | exchanges := []string{"direct", "fanout", "topic"} 16 | if vhost.srv.protoVersion == amqp.ProtoRabbit { 17 | exchanges = append(exchanges, "header") 18 | } else { 19 | exchanges = append(exchanges, "match") 20 | } 21 | 22 | for _, name := range exchanges { 23 | name = "amq." + name 24 | if vhost.GetExchange(name) == nil { 25 | t.Errorf("Default exchange '%s' does not exists", name) 26 | } 27 | } 28 | 29 | systemExchange := vhost.GetDefaultExchange() 30 | if systemExchange == nil { 31 | t.Error("System exchange does not exists") 32 | } 33 | 34 | if systemExchange.ExType() != exchange.ExTypeDirect { 35 | t.Errorf("Expected: 'direct' system exchange kind") 36 | } 37 | } 38 | 39 | func Test_ExchangeDeclare_Success(t *testing.T) { 40 | sc, _ := getNewSC(getDefaultTestConfig()) 41 | defer sc.clean() 42 | ch, _ := sc.client.Channel() 43 | 44 | if err := ch.ExchangeDeclare("test", "direct", false, false, false, false, emptyTable); err != nil { 45 | t.Error(err) 46 | } 47 | 48 | if sc.server.getVhost("/").GetExchange("test") == nil { 49 | t.Error("Exchange does not exists after 'ExchangeDeclare'") 50 | } 51 | } 52 | 53 | func Test_ExchangeDeclareDurable_Success(t *testing.T) { 54 | sc, _ := getNewSC(getDefaultTestConfig()) 55 | defer sc.clean() 56 | ch, _ := sc.client.Channel() 57 | 58 | if err := ch.ExchangeDeclare("test", "direct", true, false, false, false, emptyTable); err != nil { 59 | t.Error(err) 60 | } 61 | 62 | if sc.server.getVhost("/").GetExchange("test") == nil { 63 | t.Error("Exchange does not exists after 'ExchangeDeclareDurable'") 64 | } 65 | 66 | storedExchanges := sc.server.storage.GetVhostExchanges("/") 67 | if len(storedExchanges) == 0 { 68 | t.Error("Queue does not exists into storage after 'ExchangeDeclareDurable'") 69 | } 70 | found := false 71 | for _, ex := range storedExchanges { 72 | if ex.GetName() == "test" { 73 | found = true 74 | } 75 | } 76 | 77 | if !found { 78 | t.Error("Exchange does not exists into storage after 'ExchangeDeclareDurable'") 79 | } 80 | } 81 | 82 | func Test_ExchangeDeclare_Success_RedeclareEqual(t *testing.T) { 83 | sc, _ := getNewSC(getDefaultTestConfig()) 84 | defer sc.clean() 85 | ch, _ := sc.client.Channel() 86 | 87 | ch.ExchangeDeclare("test", "direct", false, false, false, false, emptyTable) 88 | 89 | if err := ch.ExchangeDeclare("test", "direct", false, false, false, false, emptyTable); err != nil { 90 | t.Error(err) 91 | } 92 | } 93 | 94 | func Test_ExchangeDeclare_Failed_WrongType(t *testing.T) { 95 | sc, _ := getNewSC(getDefaultTestConfig()) 96 | defer sc.clean() 97 | ch, _ := sc.client.Channel() 98 | 99 | if err := ch.ExchangeDeclare("test", "test", false, false, false, false, emptyTable); err == nil { 100 | t.Error("Expected NotImplemented error") 101 | } 102 | } 103 | 104 | func Test_ExchangeDeclare_Failed_RedeclareNotEqual(t *testing.T) { 105 | sc, _ := getNewSC(getDefaultTestConfig()) 106 | defer sc.clean() 107 | ch, _ := sc.client.Channel() 108 | 109 | ch.ExchangeDeclare("test", "direct", false, false, false, false, emptyTable) 110 | 111 | if err := ch.ExchangeDeclare("test", "direct", false, true, false, false, emptyTable); err == nil { 112 | t.Error("Expected: args inequivalent error") 113 | } 114 | } 115 | 116 | func Test_ExchangeDeclare_Failed_EmptyName(t *testing.T) { 117 | sc, _ := getNewSC(getDefaultTestConfig()) 118 | defer sc.clean() 119 | ch, _ := sc.client.Channel() 120 | 121 | if err := ch.ExchangeDeclare("", "direct", false, false, false, false, emptyTable); err == nil { 122 | t.Error("Expected: exchange name is required error") 123 | } 124 | } 125 | 126 | func Test_ExchangeDeclare_Failed_DefaultName(t *testing.T) { 127 | sc, _ := getNewSC(getDefaultTestConfig()) 128 | defer sc.clean() 129 | ch, _ := sc.client.Channel() 130 | 131 | if err := ch.ExchangeDeclare("amq.direct", "direct", false, false, false, false, emptyTable); err == nil { 132 | t.Error("Expected: access refused error") 133 | } 134 | } 135 | 136 | func Test_ExchangeDeclarePassive_Success(t *testing.T) { 137 | sc, _ := getNewSC(getDefaultTestConfig()) 138 | defer sc.clean() 139 | ch, _ := sc.client.Channel() 140 | 141 | ch.ExchangeDeclare("test", "direct", false, false, false, false, emptyTable) 142 | 143 | if err := ch.ExchangeDeclarePassive("test", "direct", false, false, false, false, emptyTable); err != nil { 144 | t.Error(err) 145 | } 146 | } 147 | 148 | func Test_ExchangeDeclarePassive_Failed_NotExists(t *testing.T) { 149 | sc, _ := getNewSC(getDefaultTestConfig()) 150 | defer sc.clean() 151 | ch, _ := sc.client.Channel() 152 | 153 | ch.ExchangeDeclare("test", "direct", false, false, false, false, emptyTable) 154 | 155 | if err := ch.ExchangeDeclarePassive("test2", "direct", false, false, false, false, emptyTable); err == nil { 156 | t.Error("Expected: exchange not found error") 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /server/server_persist_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/valinurovam/garagemq/exchange" 7 | ) 8 | 9 | func Test_ServerPersist_Queue_Success(t *testing.T) { 10 | sc, _ := getNewSC(getDefaultTestConfig()) 11 | defer sc.clean() 12 | ch, _ := sc.client.Channel() 13 | 14 | ch.QueueDeclare(t.Name(), true, false, false, false, emptyTable) 15 | sc.server.Stop() 16 | 17 | sc, _ = getNewSC(getDefaultTestConfig()) 18 | ch, _ = sc.client.Channel() 19 | 20 | if _, err := ch.QueueDeclarePassive(t.Name(), false, false, false, false, emptyTable); err != nil { 21 | t.Error("Expected queue exists after server restart", err) 22 | } 23 | } 24 | 25 | func Test_ServerPersist_Exchange_Success(t *testing.T) { 26 | sc, _ := getNewSC(getDefaultTestConfig()) 27 | defer sc.clean() 28 | ch, _ := sc.client.Channel() 29 | 30 | ch.ExchangeDeclare("testExDirect", "direct", true, false, false, false, emptyTable) 31 | ch.ExchangeDeclare("testExTopic", "topic", true, false, false, false, emptyTable) 32 | sc.server.Stop() 33 | 34 | sc, _ = getNewSC(getDefaultTestConfig()) 35 | ch, _ = sc.client.Channel() 36 | 37 | if err := ch.ExchangeDeclarePassive("testExDirect", "direct", true, false, false, false, emptyTable); err != nil { 38 | t.Error("Expected exchange exists after server restart", err) 39 | } 40 | ex := sc.server.getVhost("/").GetExchange("testExDirect") 41 | if ex.ExType() != exchange.ExTypeDirect { 42 | t.Error("Expected direct exchange") 43 | } 44 | 45 | if err := ch.ExchangeDeclarePassive("testExTopic", "topic", true, false, false, false, emptyTable); err != nil { 46 | t.Error("Expected exchange exists after server restart", err) 47 | } 48 | ex = sc.server.getVhost("/").GetExchange("testExTopic") 49 | if ex.ExType() != exchange.ExTypeTopic { 50 | t.Error("Expected topic exchange") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /srvstorage/srvstorage.go: -------------------------------------------------------------------------------- 1 | package srvstorage 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/valinurovam/garagemq/binding" 11 | "github.com/valinurovam/garagemq/exchange" 12 | "github.com/valinurovam/garagemq/interfaces" 13 | "github.com/valinurovam/garagemq/queue" 14 | ) 15 | 16 | const queuePrefix = "vhost.queue" 17 | const exchangePrefix = "vhost.exchange" 18 | const bindingPrefix = "vhost.binding" 19 | const vhostPrefix = "server.vhost" 20 | 21 | // SrvStorage implements storage for store all durable server entities 22 | type SrvStorage struct { 23 | db interfaces.DbStorage 24 | protoVersion string 25 | } 26 | 27 | // NewSrvStorage returns instance of server storage 28 | func NewSrvStorage(db interfaces.DbStorage, protoVersion string) *SrvStorage { 29 | return &SrvStorage{ 30 | db: db, 31 | protoVersion: protoVersion, 32 | } 33 | } 34 | 35 | // IsFirstStart checks is storage has lastStartTime key 36 | func (storage *SrvStorage) IsFirstStart() bool { 37 | // TODO Handle error 38 | data, _ := storage.db.Get("lastStartTime") 39 | 40 | if len(data) != 0 { 41 | return false 42 | } 43 | 44 | return true 45 | } 46 | 47 | // UpdateLastStart update last server start time 48 | func (storage *SrvStorage) UpdateLastStart() error { 49 | buf := bytes.NewBuffer([]byte{}) 50 | binary.Write(buf, binary.BigEndian, time.Now().Unix()) 51 | return storage.db.Set("lastStartTime", buf.Bytes()) 52 | } 53 | 54 | // AddVhost add vhost into storage 55 | func (storage *SrvStorage) AddVhost(vhost string, system bool) error { 56 | key := fmt.Sprintf("%s.%s", vhostPrefix, vhost) 57 | if system { 58 | return storage.db.Set(key, []byte{1}) 59 | } 60 | 61 | return storage.db.Set(key, []byte{}) 62 | } 63 | 64 | // GetVhosts returns stored virtual hosts 65 | func (storage *SrvStorage) GetVhosts() map[string]bool { 66 | vhosts := make(map[string]bool) 67 | storage.db.Iterate( 68 | func(key []byte, value []byte) { 69 | if !bytes.HasPrefix(key, []byte(vhostPrefix)) { 70 | return 71 | } 72 | vhost := getVhostFromKey(string(key)) 73 | system := bytes.Equal(value, []byte{1}) 74 | vhosts[vhost] = system 75 | }, 76 | ) 77 | 78 | return vhosts 79 | } 80 | 81 | // AddBinding add binding into storage 82 | func (storage *SrvStorage) AddBinding(vhost string, bind *binding.Binding) error { 83 | key := fmt.Sprintf("%s.%s.%s", bindingPrefix, vhost, bind.GetName()) 84 | data, err := bind.Marshal(storage.protoVersion) 85 | if err != nil { 86 | return err 87 | } 88 | return storage.db.Set(key, data) 89 | } 90 | 91 | // DelBinding remove binding from storage 92 | func (storage *SrvStorage) DelBinding(vhost string, bind *binding.Binding) error { 93 | key := fmt.Sprintf("%s.%s.%s", bindingPrefix, vhost, bind.GetName()) 94 | return storage.db.Del(key) 95 | } 96 | 97 | // AddExchange add exchange into storage 98 | func (storage *SrvStorage) AddExchange(vhost string, ex *exchange.Exchange) error { 99 | key := fmt.Sprintf("%s.%s.%s", exchangePrefix, vhost, ex.GetName()) 100 | data, err := ex.Marshal(storage.protoVersion) 101 | if err != nil { 102 | return err 103 | } 104 | return storage.db.Set(key, data) 105 | } 106 | 107 | // DelExchange remove exchange from storage 108 | func (storage *SrvStorage) DelExchange(vhost string, ex *exchange.Exchange) error { 109 | key := fmt.Sprintf("%s.%s.%s", exchangePrefix, vhost, ex.GetName()) 110 | return storage.db.Del(key) 111 | } 112 | 113 | // AddQueue add queue into storage 114 | func (storage *SrvStorage) AddQueue(vhost string, queue *queue.Queue) error { 115 | key := fmt.Sprintf("%s.%s.%s", queuePrefix, vhost, queue.GetName()) 116 | data, err := queue.Marshal(storage.protoVersion) 117 | if err != nil { 118 | return err 119 | } 120 | return storage.db.Set(key, data) 121 | } 122 | 123 | // DelQueue remove queue from storage 124 | func (storage *SrvStorage) DelQueue(vhost string, queue *queue.Queue) error { 125 | key := fmt.Sprintf("%s.%s.%s", queuePrefix, vhost, queue.GetName()) 126 | return storage.db.Del(key) 127 | } 128 | 129 | // GetVhostQueues returns queue names that has given vhost 130 | func (storage *SrvStorage) GetVhostQueues(vhost string) []*queue.Queue { 131 | var queues []*queue.Queue 132 | storage.db.Iterate( 133 | func(key []byte, value []byte) { 134 | if !bytes.HasPrefix(key, []byte(queuePrefix)) || getVhostFromKey(string(key)) != vhost { 135 | return 136 | } 137 | q := &queue.Queue{} 138 | q.Unmarshal(value, storage.protoVersion) 139 | queues = append(queues, q) 140 | }, 141 | ) 142 | 143 | return queues 144 | } 145 | 146 | // GetVhostExchanges returns exchanges that has given vhost 147 | func (storage *SrvStorage) GetVhostExchanges(vhost string) []*exchange.Exchange { 148 | var exchanges []*exchange.Exchange 149 | storage.db.Iterate( 150 | func(key []byte, value []byte) { 151 | if !bytes.HasPrefix(key, []byte(exchangePrefix)) || getVhostFromKey(string(key)) != vhost { 152 | return 153 | } 154 | ex := &exchange.Exchange{} 155 | ex.Unmarshal(value) 156 | exchanges = append(exchanges, ex) 157 | }, 158 | ) 159 | 160 | return exchanges 161 | } 162 | 163 | // GetVhostBindings returns bindings that has given vhost 164 | func (storage *SrvStorage) GetVhostBindings(vhost string) []*binding.Binding { 165 | var bindings []*binding.Binding 166 | storage.db.Iterate( 167 | func(key []byte, value []byte) { 168 | if !bytes.HasPrefix(key, []byte(bindingPrefix)) || getVhostFromKey(string(key)) != vhost { 169 | return 170 | } 171 | bind := &binding.Binding{} 172 | bind.Unmarshal(value, storage.protoVersion) 173 | bindings = append(bindings, bind) 174 | }, 175 | ) 176 | 177 | return bindings 178 | } 179 | 180 | func getVhostFromKey(key string) string { 181 | parts := strings.Split(key, ".") 182 | return parts[2] 183 | } 184 | 185 | // Close properly close storage database 186 | func (storage *SrvStorage) Close() error { 187 | return storage.db.Close() 188 | } 189 | -------------------------------------------------------------------------------- /storage/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valinurovam/garagemq/becd122222a0028eb64343f4d7391ee2ec7a321e/storage/.DS_Store -------------------------------------------------------------------------------- /storage/storage_badger.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/dgraph-io/badger" 7 | 8 | "github.com/valinurovam/garagemq/interfaces" 9 | ) 10 | 11 | // Badger implements wrapper for badger database 12 | type Badger struct { 13 | db *badger.DB 14 | } 15 | 16 | // NewBadger returns new instance of badger wrapper 17 | func NewBadger(storageDir string) *Badger { 18 | storage := &Badger{} 19 | opts := badger.DefaultOptions(storageDir) 20 | opts.SyncWrites = true 21 | opts.Dir = storageDir 22 | opts.ValueDir = storageDir 23 | var err error 24 | storage.db, err = badger.Open(opts) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | go storage.runStorageGC() 30 | 31 | return storage 32 | } 33 | 34 | // ProcessBatch process batch of operations 35 | func (storage *Badger) ProcessBatch(batch []*interfaces.Operation) (err error) { 36 | return storage.db.Update(func(txn *badger.Txn) error { 37 | for _, op := range batch { 38 | if op.Op == interfaces.OpSet { 39 | if err = txn.Set([]byte(op.Key), op.Value); err != nil { 40 | return err 41 | } 42 | } 43 | if op.Op == interfaces.OpDel { 44 | if err = txn.Delete([]byte(op.Key)); err != nil { 45 | return err 46 | } 47 | } 48 | } 49 | return nil 50 | }) 51 | } 52 | 53 | // Close properly closes badger database 54 | func (storage *Badger) Close() error { 55 | return storage.db.Close() 56 | } 57 | 58 | // Set adds a key-value pair to the database 59 | func (storage *Badger) Set(key string, value []byte) (err error) { 60 | return storage.db.Update(func(txn *badger.Txn) error { 61 | err := txn.Set([]byte(key), value) 62 | return err 63 | }) 64 | } 65 | 66 | // Del deletes a key 67 | func (storage *Badger) Del(key string) (err error) { 68 | return storage.db.Update(func(txn *badger.Txn) error { 69 | err := txn.Delete([]byte(key)) 70 | return err 71 | }) 72 | } 73 | 74 | // Get returns value by key 75 | func (storage *Badger) Get(key string) (value []byte, err error) { 76 | err = storage.db.View(func(txn *badger.Txn) error { 77 | item, err := txn.Get([]byte(key)) 78 | if err != nil { 79 | return err 80 | } 81 | value, err = item.ValueCopy(value) 82 | if err != nil { 83 | return err 84 | } 85 | return nil 86 | }) 87 | return 88 | } 89 | 90 | // Iterate iterates over all keys 91 | func (storage *Badger) Iterate(fn func(key []byte, value []byte)) { 92 | storage.db.View(func(txn *badger.Txn) error { 93 | opts := badger.DefaultIteratorOptions 94 | opts.AllVersions = false 95 | it := txn.NewIterator(opts) 96 | defer it.Close() 97 | for it.Rewind(); it.Valid(); it.Next() { 98 | item := it.Item() 99 | k := item.KeyCopy(nil) 100 | v, err := item.ValueCopy(nil) 101 | if err != nil { 102 | return err 103 | } 104 | fn(k, v) 105 | } 106 | return nil 107 | }) 108 | } 109 | 110 | // Iterate iterates over keys with prefix 111 | func (storage *Badger) IterateByPrefix(prefix []byte, limit uint64, fn func(key []byte, value []byte)) uint64 { 112 | var totalIterated uint64 113 | storage.db.View(func(txn *badger.Txn) error { 114 | opts := badger.DefaultIteratorOptions 115 | opts.AllVersions = false 116 | it := txn.NewIterator(opts) 117 | defer it.Close() 118 | 119 | for it.Seek(prefix); it.ValidForPrefix(prefix) && ((limit > 0 && totalIterated < limit) || limit <= 0); it.Next() { 120 | item := it.Item() 121 | k := item.KeyCopy(nil) 122 | v, err := item.ValueCopy(nil) 123 | if err != nil { 124 | return err 125 | } 126 | fn(k, v) 127 | totalIterated++ 128 | } 129 | return nil 130 | }) 131 | 132 | return totalIterated 133 | } 134 | 135 | func (storage *Badger) KeysByPrefixCount(prefix []byte) uint64 { 136 | var count uint64 137 | storage.db.View(func(txn *badger.Txn) error { 138 | opts := badger.DefaultIteratorOptions 139 | opts.AllVersions = false 140 | opts.PrefetchValues = false 141 | it := txn.NewIterator(opts) 142 | defer it.Close() 143 | 144 | for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { 145 | count++ 146 | } 147 | 148 | return nil 149 | }) 150 | 151 | return count 152 | } 153 | 154 | // Iterate iterates over keys with prefix 155 | func (storage *Badger) DeleteByPrefix(prefix []byte) { 156 | deleteKeys := func(keysForDelete [][]byte) error { 157 | if err := storage.db.Update(func(txn *badger.Txn) error { 158 | for _, key := range keysForDelete { 159 | if err := txn.Delete(key); err != nil { 160 | return err 161 | } 162 | } 163 | return nil 164 | }); err != nil { 165 | return err 166 | } 167 | return nil 168 | } 169 | 170 | collectSize := 100000 171 | keysForDeleteBunches := make([][][]byte, 0) 172 | keysForDelete := make([][]byte, 0, collectSize) 173 | keysCollected := 0 174 | 175 | // создать банчи и удалять банчами после итератора же ну 176 | storage.db.View(func(txn *badger.Txn) error { 177 | opts := badger.DefaultIteratorOptions 178 | opts.AllVersions = false 179 | opts.PrefetchValues = false 180 | it := txn.NewIterator(opts) 181 | defer it.Close() 182 | 183 | for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { 184 | key := it.Item().KeyCopy(nil) 185 | keysForDelete = append(keysForDelete, key) 186 | keysCollected++ 187 | if keysCollected == collectSize { 188 | keysForDeleteBunches = append(keysForDeleteBunches, keysForDelete) 189 | keysForDelete = make([][]byte, 0, collectSize) 190 | keysCollected = 0 191 | } 192 | } 193 | if keysCollected > 0 { 194 | keysForDeleteBunches = append(keysForDeleteBunches, keysForDelete) 195 | } 196 | 197 | return nil 198 | }) 199 | 200 | for _, keys := range keysForDeleteBunches { 201 | deleteKeys(keys) 202 | } 203 | } 204 | 205 | // Iterate iterates over keys with prefix 206 | func (storage *Badger) IterateByPrefixFrom(prefix []byte, from []byte, limit uint64, fn func(key []byte, value []byte)) uint64 { 207 | var totalIterated uint64 208 | storage.db.View(func(txn *badger.Txn) error { 209 | opts := badger.DefaultIteratorOptions 210 | opts.AllVersions = false 211 | it := txn.NewIterator(opts) 212 | defer it.Close() 213 | 214 | for it.Seek(from); it.ValidForPrefix(prefix) && ((limit > 0 && totalIterated < limit) || limit <= 0); it.Next() { 215 | item := it.Item() 216 | k := item.KeyCopy(nil) 217 | v, err := item.ValueCopy(nil) 218 | if err != nil { 219 | return err 220 | } 221 | fn(k, v) 222 | totalIterated++ 223 | } 224 | return nil 225 | }) 226 | 227 | return totalIterated 228 | } 229 | 230 | func (storage *Badger) runStorageGC() { 231 | timer := time.NewTicker(10 * time.Minute) 232 | for range timer.C { 233 | storage.storageGC() 234 | } 235 | } 236 | 237 | func (storage *Badger) storageGC() { 238 | again: 239 | err := storage.db.RunValueLogGC(0.5) 240 | if err == nil { 241 | goto again 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /storage/storage_bunt.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/tidwall/buntdb" 8 | 9 | "github.com/valinurovam/garagemq/config" 10 | "github.com/valinurovam/garagemq/interfaces" 11 | ) 12 | 13 | // BuntDB implements wrapper for BuntDB database 14 | type BuntDB struct { 15 | db *buntdb.DB 16 | } 17 | 18 | // NewBuntDB returns new instance of BuntDB wrapper 19 | func NewBuntDB(storagePath string) *BuntDB { 20 | storage := &BuntDB{} 21 | 22 | if storagePath != config.DbPathMemory { 23 | storagePath = fmt.Sprintf("%s/%s", storagePath, "db") 24 | } 25 | 26 | var db, err = buntdb.Open(storagePath) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | _ = db.SetConfig(buntdb.Config{ 32 | SyncPolicy: buntdb.Always, 33 | AutoShrinkDisabled: true, 34 | }) 35 | 36 | storage.db = db 37 | go storage.runStorageGC() 38 | 39 | return storage 40 | } 41 | 42 | // ProcessBatch process batch of operations 43 | func (storage *BuntDB) ProcessBatch(batch []*interfaces.Operation) (err error) { 44 | return storage.db.Update(func(tx *buntdb.Tx) error { 45 | for _, op := range batch { 46 | if op.Op == interfaces.OpSet { 47 | if _, _, err := tx.Set(op.Key, string(op.Value), nil); err != nil { 48 | return err 49 | } 50 | } 51 | if op.Op == interfaces.OpDel { 52 | if _, err := tx.Delete(op.Key); err != nil { 53 | return err 54 | } 55 | } 56 | } 57 | return nil 58 | }) 59 | } 60 | 61 | // Close properly closes BuntDB database 62 | func (storage *BuntDB) Close() error { 63 | return storage.db.Close() 64 | } 65 | 66 | // Set adds a key-value pair to the database 67 | func (storage *BuntDB) Set(key string, value []byte) (err error) { 68 | return storage.db.Update(func(tx *buntdb.Tx) error { 69 | _, _, err := tx.Set(key, string(value), nil) 70 | return err 71 | }) 72 | } 73 | 74 | // Del deletes a key 75 | func (storage *BuntDB) Del(key string) (err error) { 76 | return storage.db.Update(func(tx *buntdb.Tx) error { 77 | _, err := tx.Delete(key) 78 | return err 79 | }) 80 | } 81 | 82 | // Get returns value by key 83 | func (storage *BuntDB) Get(key string) (value []byte, err error) { 84 | storage.db.View(func(tx *buntdb.Tx) error { 85 | data, err := tx.Get(key) 86 | if err != nil { 87 | return err 88 | } 89 | value = make([]byte, len(data)) 90 | copy(value, data) 91 | return nil 92 | }) 93 | return 94 | } 95 | 96 | // Iterate iterates over all keys 97 | func (storage *BuntDB) Iterate(fn func(key []byte, value []byte)) { 98 | storage.db.View(func(tx *buntdb.Tx) error { 99 | err := tx.Ascend("", func(key, value string) bool { 100 | fn([]byte(key), []byte(value)) 101 | return true 102 | }) 103 | return err 104 | }) 105 | } 106 | 107 | // Iterate iterates over keys with prefix 108 | func (storage *BuntDB) IterateByPrefix(prefix []byte, limit uint64, fn func(key []byte, value []byte)) uint64 { 109 | storage.db.View(func(tx *buntdb.Tx) error { 110 | err := tx.AscendKeys(string(prefix), func(key, value string) bool { 111 | fn([]byte(key), []byte(value)) 112 | return true 113 | }) 114 | return err 115 | }) 116 | 117 | return 0 118 | } 119 | 120 | func (storage *BuntDB) IterateByPrefixFrom(prefix []byte, from []byte, limit uint64, fn func(key []byte, value []byte)) uint64 { 121 | return 0 122 | } 123 | 124 | func (storage *BuntDB) DeleteByPrefix(prefix []byte) { 125 | 126 | } 127 | 128 | func (storage *BuntDB) KeysByPrefixCount(prefix []byte) uint64 { 129 | return 0 130 | } 131 | 132 | func (storage *BuntDB) runStorageGC() { 133 | timer := time.NewTicker(30 * time.Minute) 134 | for range timer.C { 135 | storage.db.Shrink() 136 | } 137 | } 138 | --------------------------------------------------------------------------------