├── .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 [](https://github.com/valinurovam/garagemq/actions) [](https://coveralls.io/github/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 | 
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 |
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 alreadyopened 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 |
--------------------------------------------------------------------------------