mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 06:40:19 +00:00
Add website and docs page (#4)
* add initial website Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * refactor homepage sections Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * update usecase section Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * fix hero section react flow Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * update docs Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * add /web to broker dockerignore Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * update quickstart Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * update sidebar colors Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * fix flux capitalization Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * add open source wording Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * update the hero section key words Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * create text size emphasis Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * update edges Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> * update client docs Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com> --------- Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com>
This commit is contained in:
committed by
GitHub
parent
910e013a45
commit
6fc27ccf5b
@@ -0,0 +1 @@
|
||||
/web
|
||||
@@ -0,0 +1,26 @@
|
||||
# deps
|
||||
/node_modules
|
||||
|
||||
# generated content
|
||||
.source
|
||||
|
||||
# test & build
|
||||
/coverage
|
||||
/.next/
|
||||
/out/
|
||||
/build
|
||||
*.tsbuildinfo
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
/.pnp
|
||||
.pnp.js
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# others
|
||||
.env*.local
|
||||
.vercel
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,47 @@
|
||||
# web
|
||||
|
||||
This is a Next.js application generated with
|
||||
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
|
||||
|
||||
It is a Next.js app with [Static Export](https://nextjs.org/docs/app/guides/static-exports) configured.
|
||||
|
||||
Run development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open http://localhost:3000 with your browser to see the result.
|
||||
|
||||
## Explore
|
||||
|
||||
In the project, you can see:
|
||||
|
||||
- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.
|
||||
- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep.
|
||||
|
||||
| Route | Description |
|
||||
| ------------------------- | ------------------------------------------------------ |
|
||||
| `app/(home)` | The route group for your landing page and other pages. |
|
||||
| `app/docs` | The documentation layout and pages. |
|
||||
| `app/api/search/route.ts` | The Route Handler for search. |
|
||||
|
||||
### Fumadocs MDX
|
||||
|
||||
A `source.config.ts` config file has been included, you can customise different options like frontmatter schema.
|
||||
|
||||
Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js and Fumadocs, take a look at the following
|
||||
resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
|
||||
features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs
|
||||
@@ -0,0 +1,7 @@
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import '../global.css';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ArchitectureSection } from "@/components/homepage/section-architecture";
|
||||
import { FeaturesSection } from "@/components/homepage/section-features";
|
||||
import { FooterSection } from "@/components/homepage/section-footer";
|
||||
import { HeroSection } from "@/components/homepage/section-hero";
|
||||
import { NewsletterSection } from "@/components/homepage/section-newsletter";
|
||||
import { PerformanceSection } from "@/components/homepage/section-performance";
|
||||
import { QuickStartSection } from "@/components/homepage/section-quickstart";
|
||||
import { UseCasesSection } from "@/components/homepage/section-usecases";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="min-h-screen bg-theme">
|
||||
<HeroSection />
|
||||
<FeaturesSection />
|
||||
<PerformanceSection />
|
||||
<UseCasesSection />
|
||||
<ArchitectureSection />
|
||||
<QuickStartSection />
|
||||
<NewsletterSection />
|
||||
<FooterSection />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export const { staticGET: GET } = createFromSource(source, {
|
||||
// https://docs.orama.com/docs/orama-js/supported-languages
|
||||
language: 'english',
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { getMDXComponents } from '@/mdx-components';
|
||||
import type { Metadata } from 'next';
|
||||
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
||||
|
||||
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
if (!page) notFound();
|
||||
|
||||
const MDX = page.data.body;
|
||||
|
||||
return (
|
||||
<DocsPage toc={page.data.toc} full={page.data.full}>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<MDX
|
||||
components={getMDXComponents({
|
||||
// this allows you to link to other pages with relative file paths
|
||||
a: createRelativeLink(source, page),
|
||||
})}
|
||||
/>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return source.generateParams();
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
if (!page) notFound();
|
||||
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
openGraph: {
|
||||
images: getPageImage(page).url,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { source } from "@/lib/source";
|
||||
import { DocsLayout } from "fumadocs-ui/layouts/docs";
|
||||
import { baseOptions } from "@/lib/layout.shared";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const base = baseOptions();
|
||||
return (
|
||||
// @ts-expect-error - fumadocs type definitions don't match implementation for children prop
|
||||
<DocsLayout
|
||||
{...base}
|
||||
tree={source.getPageTree()}
|
||||
links={base.links?.filter((item) => item.type === "icon")}
|
||||
nav={{
|
||||
...base.nav,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/neutral.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
|
||||
/* FluxMQ Brand Colors - Light Theme */
|
||||
:root {
|
||||
--flux-blue: hsl(213.64deg 58.41% 44.31%);
|
||||
/* #2F69B3 */
|
||||
--flux-orange: hsl(35.07deg 94.52% 57.06%);
|
||||
/* #F9A32A */
|
||||
--flux-bg: hsl(0 0% 100%);
|
||||
--flux-fg: hsl(0 0% 10%);
|
||||
--flux-border: hsl(0 0% 20%);
|
||||
--flux-grid: hsl(0 0% 90%);
|
||||
--flux-text: hsl(0 0% 10%);
|
||||
--flux-text-muted: hsl(0 0% 40%);
|
||||
--flux-bg-alt: hsl(0 0% 97%);
|
||||
}
|
||||
|
||||
/* FluxMQ Brand Colors - Dark Theme */
|
||||
:root[class~="dark"] {
|
||||
--flux-blue: hsl(213.64deg 70% 60%);
|
||||
/* Brighter blue for dark mode */
|
||||
--flux-orange: hsl(35.07deg 94.52% 57.06%);
|
||||
/* Keep orange same */
|
||||
--flux-bg: hsl(0 0% 10%);
|
||||
--flux-fg: hsl(0 0% 95%);
|
||||
--flux-border: hsl(0 0% 30%);
|
||||
--flux-grid: hsl(0 0% 20%);
|
||||
--flux-text: hsl(0 0% 95%);
|
||||
--flux-text-muted: hsl(0 0% 60%);
|
||||
--flux-bg-alt: hsl(0 0% 8%);
|
||||
}
|
||||
|
||||
/* Technical Grid Pattern */
|
||||
.grid-pattern {
|
||||
background-image:
|
||||
linear-gradient(to right, var(--flux-grid) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--flux-grid) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
/* Brutalist Elements */
|
||||
.brutalist-border {
|
||||
border: 2px solid var(--flux-border);
|
||||
}
|
||||
|
||||
.brutalist-card {
|
||||
border: 2px solid var(--flux-border);
|
||||
background: var(--flux-bg);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.brutalist-card:hover {
|
||||
border-color: var(--flux-orange);
|
||||
}
|
||||
|
||||
/* Code/Terminal Styling */
|
||||
.terminal {
|
||||
background: hsl(0 0% 10%);
|
||||
color: hsl(120 100% 80%);
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
border: 2px solid var(--flux-border);
|
||||
}
|
||||
|
||||
:root[class~="dark"] .terminal {
|
||||
background: hsl(0 0% 5%);
|
||||
border-color: var(--flux-border);
|
||||
}
|
||||
|
||||
/* Accent Lines */
|
||||
.accent-line {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.accent-line::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: var(--flux-orange);
|
||||
}
|
||||
|
||||
/* Technical Typography */
|
||||
.mono {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Performance Metrics Table */
|
||||
.metrics-table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.metrics-table th,
|
||||
.metrics-table td {
|
||||
border: 1px solid var(--flux-border);
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.metrics-table th {
|
||||
background: var(--flux-fg);
|
||||
color: var(--flux-bg);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.metrics-table tbody {
|
||||
background: var(--flux-bg);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
/* Theme-aware backgrounds */
|
||||
.bg-theme {
|
||||
background: var(--flux-bg);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.bg-theme-alt {
|
||||
background: var(--flux-bg-alt);
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.bg-theme-inverse {
|
||||
background: var(--flux-fg);
|
||||
color: var(--flux-bg);
|
||||
}
|
||||
|
||||
.text-theme {
|
||||
color: var(--flux-text);
|
||||
}
|
||||
|
||||
.text-theme-muted {
|
||||
color: var(--flux-text-muted);
|
||||
}
|
||||
|
||||
.border-theme {
|
||||
border-color: var(--flux-border);
|
||||
}
|
||||
|
||||
/* Smooth scroll for anchor links */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Base font size improvements */
|
||||
p {
|
||||
font-size: 1.0625rem;
|
||||
/* 17px - slightly larger for better readability */
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* Remove default focus outline, add custom */
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--flux-orange);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.8s ease-out;
|
||||
animation-fill-mode: backwards;
|
||||
}
|
||||
|
||||
.animate-slide-up:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.animate-slide-up:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
/* Respect reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
.animate-fade-in,
|
||||
.animate-slide-up {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fumadocs Sidebar Styling */
|
||||
#nd-sidebar [data-active="true"] {
|
||||
color: var(--flux-orange) !important;
|
||||
background-color: rgba(249, 163, 42, 0.1) !important;
|
||||
border-left: 3px solid var(--flux-orange) !important;
|
||||
}
|
||||
|
||||
#nd-sidebar a:hover {
|
||||
color: var(--flux-blue) !important;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { type Metadata } from 'next';
|
||||
import { Provider } from '@/components/provider';
|
||||
import './global.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: 'FluxMQ — High-Performance Multi-Protocol Message Broker',
|
||||
template: '%s | FluxMQ',
|
||||
},
|
||||
description: 'FluxMQ is a high-performance, multi-protocol message broker written in Go. Supports MQTT 3.1.1/5.0, WebSocket, HTTP and CoAP with durable queues, clustering, and event-driven architecture.',
|
||||
keywords: ['FluxMQ', 'MQTT broker', 'message broker', 'IoT', 'high-performance', 'durable queues', 'clustering', 'event-driven', 'open-source', 'Go'],
|
||||
authors: [{ name: 'Abstract Machines' }],
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
siteName: 'FluxMQ',
|
||||
title: 'FluxMQ — High-Performance Multi-Protocol Message Broker',
|
||||
description: 'High-performance, multi-protocol message broker for IoT and event-driven architectures. MQTT 3.1.1/5.0, WebSocket, HTTP, CoAP with durable queues and clustering.',
|
||||
images: [
|
||||
{
|
||||
url: '/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'FluxMQ - Multi-Protocol Message Broker',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
site: '@absmach',
|
||||
title: 'FluxMQ — High-Performance Multi-Protocol Message Broker',
|
||||
description: 'FluxMQ is a high-performance, multi-protocol message broker written in Go. Supports MQTT 3.1.1/5.0, WebSocket, HTTP and CoAP with durable queues, clustering, and event-driven architecture.',
|
||||
images: ['/og-image.png'],
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="canonical" href="https://absmach.eu/fluxmq/" />
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'FluxMQ',
|
||||
operatingSystem: 'Linux, Multi-platform',
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
description: 'FluxMQ is a high-performance, multi-protocol message broker for IoT and event-driven architectures, supporting MQTT 3.1.1/5.0, WebSocket, HTTP and CoAP.',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
creator: {
|
||||
'@type': 'Organization',
|
||||
name: 'Abstract Machines',
|
||||
url: 'https://absmach.eu',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className="flex flex-col min-h-screen" style={{ fontFamily: 'Verdana, Geneva, sans-serif' }}>
|
||||
<Provider>{children}</Provider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { getLLMText, source } from '@/lib/source';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET() {
|
||||
const scan = source.getPages().map(getLLMText);
|
||||
const scanned = await Promise.all(scan);
|
||||
|
||||
return new Response(scanned.join('\n\n'));
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { generate as DefaultImage } from 'fumadocs-ui/og';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) {
|
||||
const { slug } = await params;
|
||||
const page = source.getPage(slug.slice(0, -1));
|
||||
if (!page) notFound();
|
||||
|
||||
return new ImageResponse(
|
||||
<DefaultImage title={page.data.title} description={page.data.description} site="My App" />,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.getPages().map((page) => ({
|
||||
lang: page.locale,
|
||||
slug: getPageImage(page).segments,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": [
|
||||
"**",
|
||||
"!node_modules",
|
||||
"!.next",
|
||||
"!dist",
|
||||
"!build",
|
||||
"!.source"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
},
|
||||
"domains": {
|
||||
"next": "recommended",
|
||||
"react": "recommended"
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Copy, Check } from "lucide-react";
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export function CodeBlock({ code, language = "bash" }: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative terminal p-6 mb-4 group">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="absolute top-4 right-4 p-2 hover:bg-(--flux-orange) hover:text-white transition-colors brutalist-border opacity-70 group-hover:opacity-100"
|
||||
aria-label="Copy code"
|
||||
>
|
||||
{copied ? <Check size={16} /> : <Copy size={16} />}
|
||||
</button>
|
||||
<pre className="overflow-x-auto">
|
||||
<code className="text-sm md:text-base">{code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { MermaidDiagram } from "@/components/mermaid-diagram";
|
||||
|
||||
const architectureDiagram = `
|
||||
graph TD
|
||||
A[Setup and configuration] --> B[TCP/WS/HTTP/CoAP<br/>Servers]
|
||||
A --> C[AMQP 1.0<br/>Server]
|
||||
A --> D[AMQP 0.9.1<br/>Server]
|
||||
B --> E[MQTT Broker]
|
||||
C --> F[AMQP Broker<br/> 1.0]
|
||||
D --> G[AMQP Broker<br/>0.9.1]
|
||||
E --> H[Queue Manager<br/>Bindings + Delivery]
|
||||
F --> H
|
||||
G --> H
|
||||
H --> I[Log Storage<br/>+ Topic Index]
|
||||
|
||||
style A fill:#2F69B3,stroke:#000000,stroke-width:2px,color:#ffffff
|
||||
style H fill:#F9A32A,stroke:#000000,stroke-width:2px,color:#000000
|
||||
style I fill:#2F69B3,stroke:#000000,stroke-width:2px,color:#ffffff
|
||||
`;
|
||||
|
||||
export function ArchitectureSection() {
|
||||
return (
|
||||
<section
|
||||
id="architecture"
|
||||
className="py-20 border-b-2 border-theme bg-theme-alt"
|
||||
>
|
||||
<div className="container mx-auto px-6">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-12">
|
||||
<span className="border-l-4 border-(--flux-orange) pl-4">
|
||||
ARCHITECTURE
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<MermaidDiagram chart={architectureDiagram} />
|
||||
</div>
|
||||
|
||||
<div className="mt-12 max-w-3xl mx-auto brutalist-border bg-theme p-6">
|
||||
<h3 className="font-bold mono text-lg mb-4">KEY COMPONENTS</h3>
|
||||
<ul className="space-y-3 text-theme-muted text-base">
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-blue) mr-2 font-bold">▸</span>
|
||||
<span>
|
||||
<strong>Transport Layer:</strong> Multi-protocol servers (MQTT,
|
||||
AMQP 1.0, AMQP 0.9.1, CoAP, HTTP, WebSocket)
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-blue) mr-2 font-bold">▸</span>
|
||||
<span>
|
||||
<strong>Protocol Brokers:</strong> FSM-based protocol handlers
|
||||
with zero-copy parsing
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-blue) mr-2 font-bold">▸</span>
|
||||
<span>
|
||||
<strong>Queue Manager:</strong> Durable queue bindings with
|
||||
FIFO, priority, and topic-based delivery
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-blue) mr-2 font-bold">▸</span>
|
||||
<span>
|
||||
<strong>Storage:</strong> For message persistence and topic
|
||||
indexing
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Network, Database, Server, Zap, Shield, Code2 } from "lucide-react";
|
||||
|
||||
export function FeaturesSection() {
|
||||
return (
|
||||
<section id="features" className="py-20 border-b-2 border-(--flux-border)">
|
||||
<div className="container mx-auto px-6">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-12">
|
||||
<span className="border-l-4 border-(--flux-orange) pl-4">
|
||||
FEATURES
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Feature 1 */}
|
||||
<div className="brutalist-card p-6 accent-line pl-8">
|
||||
<Network
|
||||
className="mb-4 text-(--flux-blue)"
|
||||
size={32}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<h3 className="text-xl font-bold mb-3 mono">
|
||||
Multi-Protocol Support
|
||||
</h3>
|
||||
<p className="text-theme-muted leading-relaxed text-base">
|
||||
Full MQTT 3.1.1 and 5.0 over TCP and WebSocket, plus HTTP-MQTT and
|
||||
CoAP bridges. All protocols share the same broker core — messages
|
||||
flow seamlessly across transports.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 2 */}
|
||||
<div className="brutalist-card p-6 accent-line pl-8">
|
||||
<Database
|
||||
className="mb-4 text-(--flux-orange)"
|
||||
size={32}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<h3 className="text-xl font-bold mb-3 mono">Durable Queues</h3>
|
||||
<p className="text-theme-muted leading-relaxed text-base">
|
||||
Persistent message queues with consumer groups, ack/nack/reject
|
||||
semantics, dead-letter queues, and Kafka-style retention.
|
||||
Raft-based replication with automatic failover.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 3 */}
|
||||
<div className="brutalist-card p-6 accent-line pl-8">
|
||||
<Server
|
||||
className="mb-4 text-(--flux-blue)"
|
||||
size={32}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<h3 className="text-xl font-bold mb-3 mono">
|
||||
Clustering & High Availability
|
||||
</h3>
|
||||
<p className="text-theme-muted leading-relaxed text-base">
|
||||
Embedded etcd for coordination, gRPC-based inter-broker
|
||||
communication with mTLS, automatic session ownership, and graceful
|
||||
shutdown with session transfer. No external dependencies.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 4 */}
|
||||
<div className="brutalist-card p-6 accent-line pl-8">
|
||||
<Zap
|
||||
className="mb-4 text-(--flux-orange)"
|
||||
size={32}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<h3 className="text-xl font-bold mb-3 mono">
|
||||
Performance Optimized
|
||||
</h3>
|
||||
<p className="text-theme-muted leading-relaxed text-base">
|
||||
Zero-copy packet parsing, object pooling, efficient trie-based
|
||||
topic matching, and direct instrumentation. 300K-500K msg/s per
|
||||
node with sub-10ms latency.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 5 */}
|
||||
<div className="brutalist-card p-6 accent-line pl-8">
|
||||
<Shield
|
||||
className="mb-4 text-(--flux-blue)"
|
||||
size={32}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<h3 className="text-xl font-bold mb-3 mono">Security</h3>
|
||||
<p className="text-theme-muted leading-relaxed text-base">
|
||||
TLS/mTLS for client connections, mTLS for inter-broker gRPC,
|
||||
DTLS/mDTLS for CoAP, WebSocket origin validation, and
|
||||
per-IP/per-client rate limiting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature 6 */}
|
||||
<div className="brutalist-card p-6 accent-line pl-8">
|
||||
<Code2
|
||||
className="mb-4 text-(--flux-orange)"
|
||||
size={32}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<h3 className="text-xl font-bold mb-3 mono">
|
||||
Open-Source & Extensible
|
||||
</h3>
|
||||
<p className="text-theme-muted leading-relaxed text-base">
|
||||
Licensed under Apache 2.0 with a clean layered architecture
|
||||
(Transport → Protocol → Domain). Pluggable storage backends,
|
||||
protocol-agnostic domain logic, and easy extensibility.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export function FooterSection() {
|
||||
return (
|
||||
<footer className="py-12 bg-theme-alt">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="grid md:grid-cols-4 gap-8">
|
||||
{/* About */}
|
||||
<div>
|
||||
<h3 className="font-bold mb-4 text-lg">ABOUT</h3>
|
||||
<p className="text-sm leading-relaxed">
|
||||
FluxMQ is developed by Abstract Machines, an IoT infrastructure
|
||||
and security company located in Paris.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Products */}
|
||||
<div>
|
||||
<h3 className="font-bold mb-4 text-lg">PRODUCTS</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>
|
||||
<a
|
||||
href="https://magistrala.absmach.eu"
|
||||
className="hover:text-(--flux-orange)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Magistrala
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://absmach.eu/supermq/"
|
||||
className="hover:text-(--flux-orange)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
SuperMQ
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://absmach.eu/propeller/"
|
||||
className="hover:text-(--flux-orange)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Propeller
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Resources */}
|
||||
<div>
|
||||
<h3 className="font-bold mb-4 text-lg">RESOURCES</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>
|
||||
<Link href="/docs" className="hover:text-(--flux-orange)">
|
||||
Documentation
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/absmach/fluxmq"
|
||||
className="hover:text-(--flux-orange)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://absmach.eu/blog/"
|
||||
className="hover:text-(--flux-orange)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div>
|
||||
<h3 className="font-bold mb-4 text-lg">CONTACT</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>
|
||||
<a
|
||||
href="mailto:info@absmach.eu"
|
||||
className="hover:text-(--flux-orange)"
|
||||
>
|
||||
info@absmach.eu
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/absmach"
|
||||
className="hover:text-(--flux-orange)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://twitter.com/absmach"
|
||||
className="hover:text-(--flux-orange)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Twitter
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.linkedin.com/company/abstract-machines"
|
||||
className="hover:text-(--flux-orange)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
LinkedIn
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-theme mt-8 pt-8 text-center text-sm opacity-70">
|
||||
<p>© 2026 Abstract Machines. Licensed under Apache 2.0.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import Link from "next/link";
|
||||
import { NodeNetwork } from "@/components/node-network";
|
||||
|
||||
export function HeroSection() {
|
||||
return (
|
||||
<section className="relative border-b-2 border-(--flux-border) py-20 md:py-20 h-[90vh]">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
{/* Left side - Content */}
|
||||
<div className="max-w-2xl">
|
||||
{/* Title with technical specs overlay */}
|
||||
<div className="mb-14">
|
||||
<h1
|
||||
className="text-6xl md:text-8xl font-bold mb-4 animate-fade-in"
|
||||
style={{ lineHeight: "1.1" }}
|
||||
>
|
||||
<span className="text-(--flux-blue)">Flux</span>
|
||||
<span className="text-(--flux-orange)">MQ</span>
|
||||
</h1>
|
||||
<div className="mono text-base md:text-xl text-theme-muted mb-6 border-l-4 border-(--flux-orange) pl-4 animate-slide-up">
|
||||
<div>open-source | high-throughput | low-latency | scalable</div>
|
||||
<div>persistent storage | replication | MQTT v3/v5 | AMQP</div>
|
||||
<div>security | extensibility</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xl! md:text-2xl! mb-14 max-w-3xl leading-relaxed animate-fade-in">
|
||||
A high-performance, open source, multi-protocol message broker
|
||||
written in Go for scalable IoT and event-driven architectures.
|
||||
Single binary. No external dependencies.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-4 animate-slide-up">
|
||||
<Link
|
||||
href="https://github.com/absmach/fluxmq"
|
||||
className="brutalist-border px-6 py-3 font-bold hover:bg-(--flux-blue) hover:text-white transition-colors inline-block"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
VIEW ON GITHUB →
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs"
|
||||
className="brutalist-border px-6 py-3 font-bold hover:bg-(--flux-orange) hover:text-white transition-colors inline-block"
|
||||
>
|
||||
READ DOCUMENTATION
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Node Network Animation */}
|
||||
<div className="hidden md:flex items-center justify-center h-[80vh]">
|
||||
<NodeNetwork />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
export function NewsletterSection() {
|
||||
return (
|
||||
<section className="py-20 border-b-2 border-theme bg-theme-alt">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-center">
|
||||
SUBSCRIBE TO NEWSLETTER
|
||||
</h2>
|
||||
<p className="text-center text-theme-muted mb-8">
|
||||
Stay updated with the latest FluxMQ news, updates and announcements.
|
||||
</p>
|
||||
|
||||
<form
|
||||
action="https://absmach.us11.list-manage.com/subscribe/post?u=70b43c7181d005024187bfb31&id=0a319b6b63&f_id=002611e1f0"
|
||||
method="post"
|
||||
target="_blank"
|
||||
className="max-w-md mx-auto"
|
||||
>
|
||||
<div className="flex gap-0">
|
||||
<input
|
||||
type="email"
|
||||
name="EMAIL"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
className="flex-1 brutalist-border border-r-0 px-4 py-3 focus:outline-none focus:border-(--flux-orange)"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="brutalist-border bg-(--flux-orange) hover:bg-(--flux-blue) text-white px-6 py-3 font-bold transition-colors"
|
||||
>
|
||||
SUBSCRIBE
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" name="tags" value="8115259" />
|
||||
<div
|
||||
style={{ position: "absolute", left: "-5000px" }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="b_70b43c7181d005024187bfb31_0a319b6b63"
|
||||
tabIndex={-1}
|
||||
value=""
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-theme-muted mt-4 text-center">
|
||||
By subscribing, you agree to our{" "}
|
||||
<a
|
||||
href="https://absmach.eu/privacy/"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href="https://absmach.eu/terms/"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Terms of Service
|
||||
</a>
|
||||
. You can unsubscribe at any time.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
export function PerformanceSection() {
|
||||
return (
|
||||
<section
|
||||
id="performance"
|
||||
className="py-20 border-b-2 border-theme bg-theme-alt"
|
||||
>
|
||||
<div className="container mx-auto px-6">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-12">
|
||||
<span className="border-l-4 border-(--flux-orange) pl-4">
|
||||
PERFORMANCE
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="max-w-4xl">
|
||||
<table className="metrics-table w-full mono text-sm md:text-base">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>METRIC</th>
|
||||
<th>VALUE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white">
|
||||
<tr>
|
||||
<td>Concurrent Connections</td>
|
||||
<td className="font-bold">500K+ per node</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Message Throughput</td>
|
||||
<td className="font-bold">300K-500K msg/s per node</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Latency (local)</td>
|
||||
<td className="font-bold"><10ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Latency (cross-node)</td>
|
||||
<td className="font-bold">~5ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Session Takeover</td>
|
||||
<td className="font-bold"><100ms</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="mt-8 brutalist-border bg-theme p-6">
|
||||
<h3 className="font-bold mb-4 mono text-lg">CLUSTER SCALING</h3>
|
||||
<ul className="space-y-2 mono text-sm">
|
||||
<li className="border-l-4 border-(--flux-blue) pl-4">
|
||||
<strong>3-node cluster:</strong> 1-2M msg/s
|
||||
</li>
|
||||
<li className="border-l-4 border-(--flux-blue) pl-4">
|
||||
<strong>5-node cluster:</strong> 2-4M msg/s
|
||||
</li>
|
||||
<li className="border-l-4 border-(--flux-blue) pl-4">
|
||||
<strong>Scaling:</strong> Linear with topic sharding
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import Link from "next/link";
|
||||
import { CodeBlock } from "@/components/code-block";
|
||||
import { BookOpen, Github } from "lucide-react";
|
||||
|
||||
export function QuickStartSection() {
|
||||
return (
|
||||
<section
|
||||
id="quick-start"
|
||||
className="py-20 border-b-2 border-(--flux-border)"
|
||||
>
|
||||
<div className="container mx-auto px-6">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-12">
|
||||
<span className="border-l-4 border-(--flux-orange) pl-4">
|
||||
QUICK START
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="max-w-4xl space-y-8">
|
||||
{/* Docker Compose */}
|
||||
<div>
|
||||
<h3 className="mono font-bold mb-4 text-xl">1. RUN WITH DOCKER</h3>
|
||||
<CodeBlock
|
||||
code={`git clone https://github.com/absmach/fluxmq.git
|
||||
cd fluxmq
|
||||
docker compose -f docker/compose.yaml up -d`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Test */}
|
||||
<div>
|
||||
<h3 className="mono font-bold mb-4 text-xl">2. TEST WITH MQTT</h3>
|
||||
<CodeBlock
|
||||
code={`# Subscribe to all topics
|
||||
mosquitto_sub -p 1883 -t "test/#" -v
|
||||
|
||||
# Publish a message
|
||||
mosquitto_pub -p 1883 -t "test/hello" -m "Hello FluxMQ"`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Local Build */}
|
||||
<div>
|
||||
<h3 className="mono font-bold mb-4 text-xl">3. OR BUILD LOCALLY</h3>
|
||||
<CodeBlock
|
||||
code={`git clone https://github.com/absmach/fluxmq.git
|
||||
cd fluxmq
|
||||
make build
|
||||
./build/fluxmq --config examples/no-cluster.yaml`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="brutalist-border bg-theme p-8 mt-12">
|
||||
<p className="font-bold mb-4 text-lg">
|
||||
Defaults (MQTT TCP: :1883, AMQP 0.9.1: :5682, Data:
|
||||
/tmp/fluxmq/data)
|
||||
</p>
|
||||
<p className="font-bold mb-4 text-lg">Next Steps:</p>
|
||||
<ul className="space-y-3">
|
||||
<li>
|
||||
<Link
|
||||
href="https://github.com/absmach/fluxmq/tree/main/examples"
|
||||
className="text-(--flux-blue) hover:underline font-bold text-lg flex items-center gap-2"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Github size={20} />
|
||||
Explore code examples on GitHub
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/docs"
|
||||
className="text-(--flux-orange) hover:underline font-bold text-lg flex items-center gap-2"
|
||||
>
|
||||
<BookOpen size={20} />
|
||||
Read the full documentation
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Activity, Cpu, Gauge } from "lucide-react";
|
||||
|
||||
export function UseCasesSection() {
|
||||
return (
|
||||
<section id="use-cases" className="py-20 border-b-2 border-(--flux-border)">
|
||||
<div className="container mx-auto px-6">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-12">
|
||||
<span className="border-l-4 border-(--flux-orange) pl-4">
|
||||
USE CASES
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{/* Event-Driven Systems */}
|
||||
<div className="brutalist-card p-6">
|
||||
<Activity
|
||||
className="mb-4 text-(--flux-orange)"
|
||||
size={36}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<div className="border-b-2 border-(--flux-border) pb-4 mb-4">
|
||||
<h3 className="text-2xl font-bold mono">Event-Driven Systems</h3>
|
||||
</div>
|
||||
<ul className="space-y-3 text-theme-muted text-base">
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-orange) mr-2 font-bold">■</span>
|
||||
<span>Decouple microservices with event streams</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-orange) mr-2 font-bold">■</span>
|
||||
<span>Reliable command & event pipelines (CQRS)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-orange) mr-2 font-bold">■</span>
|
||||
<span>Background jobs & asynchronous workflows</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-orange) mr-2 font-bold">■</span>
|
||||
<span>Real-time data processing pipelines</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* IoT & Real-Time */}
|
||||
<div className="brutalist-card p-6">
|
||||
<Cpu
|
||||
className="mb-4 text-(--flux-blue)"
|
||||
size={36}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<div className="border-b-2 border-(--flux-border) pb-4 mb-4">
|
||||
<h3 className="text-2xl font-bold mono">IoT & Real-Time</h3>
|
||||
</div>
|
||||
<ul className="space-y-3 text-theme-muted text-base">
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-orange) mr-2 font-bold">■</span>
|
||||
<span>IoT device telemetry ingestion (MQTT)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-orange) mr-2 font-bold">■</span>
|
||||
<span>Edge deployments with intermittent connectivity</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-orange) mr-2 font-bold">■</span>
|
||||
<span>Live dashboards & browser updates (WebSocket)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-orange) mr-2 font-bold">■</span>
|
||||
<span>Constrained device messaging via protocol bridges</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* High-Throughput Data Pipelines */}
|
||||
<div className="brutalist-card p-6">
|
||||
<Gauge
|
||||
className="mb-4 text-(--flux-orange)"
|
||||
size={36}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<div className="border-b-2 border-(--flux-border) pb-4 mb-4">
|
||||
<h3 className="text-2xl font-bold mono">
|
||||
High-Throughput Pipelines
|
||||
</h3>
|
||||
</div>
|
||||
<ul className="space-y-3 text-theme-muted text-base">
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-orange) mr-2 font-bold">■</span>
|
||||
<span>Stream millions of events per second</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-orange) mr-2 font-bold">■</span>
|
||||
<span>Buffer traffic bursts with durable queues</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-orange) mr-2 font-bold">■</span>
|
||||
<span>Decouple ingestion from downstream processing</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-(--flux-orange) mr-2 font-bold">■</span>
|
||||
<span>Power analytics & observability data streams</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import mermaid from "mermaid";
|
||||
|
||||
interface MermaidDiagramProps {
|
||||
chart: string;
|
||||
}
|
||||
|
||||
export function MermaidDiagram({ chart }: MermaidDiagramProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const isDark = theme === "dark";
|
||||
|
||||
// Initialize mermaid with theme-aware absolute colors
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: "base",
|
||||
themeVariables: {
|
||||
primaryColor: "#2F69B3", // Flux Blue
|
||||
primaryTextColor: "#ffffff",
|
||||
primaryBorderColor: isDark ? "#444444" : "#333333",
|
||||
lineColor: isDark ? "#cccccc" : "#666666",
|
||||
secondaryColor: "#F9A32A", // Flux Orange
|
||||
tertiaryColor: isDark ? "#1a1a1a" : "#ffffff",
|
||||
background: isDark ? "#1a1a1a" : "#f9f9f9",
|
||||
mainBkg: "#1a1a1a",
|
||||
textColor: isDark ? "#ffffff" : "#1a1a1a",
|
||||
fontSize: "14px",
|
||||
fontFamily: "'JetBrains Mono', 'Courier New', monospace",
|
||||
},
|
||||
});
|
||||
|
||||
if (ref.current) {
|
||||
// Create unique ID for this diagram
|
||||
const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Render the diagram
|
||||
mermaid.render(id, chart).then(({ svg }) => {
|
||||
if (ref.current) {
|
||||
ref.current.innerHTML = svg;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [chart, theme]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex justify-center items-center my-8 brutalist-border bg-theme p-8 overflow-x-auto"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
Node,
|
||||
Edge,
|
||||
Position,
|
||||
MarkerType,
|
||||
Handle,
|
||||
ConnectionMode,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { SmartBezierEdge } from "@tisoap/react-flow-smart-edge";
|
||||
|
||||
const blue = "#2F69B3";
|
||||
const orange = "#F9A32A";
|
||||
const green = "#28b828";
|
||||
|
||||
const nodeTypes = {
|
||||
broker: BrokerNode,
|
||||
};
|
||||
const edgeTypes = {
|
||||
smart: SmartBezierEdge,
|
||||
};
|
||||
export function NodeNetwork() {
|
||||
const nodes: Node[] = useMemo(
|
||||
() => [
|
||||
// 🔵 Protocol Inputs
|
||||
{
|
||||
id: "mqtt",
|
||||
position: { x: 0, y: 50 },
|
||||
data: { label: "MQTT" },
|
||||
sourcePosition: Position.Right,
|
||||
type: "input",
|
||||
style: protocolStyle,
|
||||
},
|
||||
{
|
||||
id: "http",
|
||||
position: { x: 0, y: 250 },
|
||||
data: { label: "HTTP" },
|
||||
sourcePosition: Position.Right,
|
||||
type: "input",
|
||||
style: protocolStyle,
|
||||
},
|
||||
{
|
||||
id: "ws",
|
||||
position: { x: 0, y: 450 },
|
||||
data: { label: "WebSocket" },
|
||||
sourcePosition: Position.Right,
|
||||
type: "input",
|
||||
style: protocolStyle,
|
||||
},
|
||||
{
|
||||
id: "amqp",
|
||||
position: { x: 0, y: 650 },
|
||||
data: { label: "AMQP" },
|
||||
sourcePosition: Position.Right,
|
||||
type: "input",
|
||||
style: protocolStyle,
|
||||
},
|
||||
|
||||
// 🟠 FluxMQ Cluster
|
||||
{
|
||||
id: "broker-a",
|
||||
position: { x: 300, y: 50 },
|
||||
data: { label: "FluxMQ Node A" },
|
||||
style: brokerStyle,
|
||||
type: "broker",
|
||||
},
|
||||
{
|
||||
id: "broker-b",
|
||||
position: { x: 300, y: 350 },
|
||||
data: { label: "FluxMQ Node B" },
|
||||
style: brokerStyle,
|
||||
type: "broker",
|
||||
},
|
||||
{
|
||||
id: "broker-c",
|
||||
position: { x: 300, y: 600 },
|
||||
data: { label: "FluxMQ Node C" },
|
||||
style: brokerStyle,
|
||||
type: "broker",
|
||||
},
|
||||
|
||||
// 🟣 Consumers
|
||||
{
|
||||
id: "analytics",
|
||||
position: { x: 600, y: 150 },
|
||||
data: { label: "Analytics" },
|
||||
targetPosition: Position.Left,
|
||||
type: "output",
|
||||
style: consumerStyle,
|
||||
},
|
||||
{
|
||||
id: "apps",
|
||||
position: { x: 600, y: 350 },
|
||||
data: { label: "Applications" },
|
||||
targetPosition: Position.Left,
|
||||
type: "output",
|
||||
style: consumerStyle,
|
||||
},
|
||||
{
|
||||
id: "services",
|
||||
position: { x: 600, y: 550 },
|
||||
data: { label: "Services" },
|
||||
targetPosition: Position.Left,
|
||||
type: "output",
|
||||
style: consumerStyle,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const edges: Edge[] = useMemo(
|
||||
() => [
|
||||
// Protocols → Cluster
|
||||
edge("mqtt", "broker-a"),
|
||||
edge("mqtt", "broker-b"),
|
||||
edge("mqtt", "broker-c"),
|
||||
|
||||
edge("http", "broker-b"),
|
||||
edge("http", "broker-c"),
|
||||
|
||||
edge("ws", "broker-a"),
|
||||
edge("ws", "broker-b"),
|
||||
edge("ws", "broker-c"),
|
||||
|
||||
edge("amqp", "broker-c"),
|
||||
|
||||
// Cluster mesh (clustering)
|
||||
clusterEdge("broker-a", "broker-b", "bottom", "top"),
|
||||
clusterEdge("broker-b", "broker-c", "bottom", "top"),
|
||||
clusterEdge("broker-c", "broker-a", "bottom", "left"),
|
||||
clusterEdge("broker-c", "broker-b", "bottom", "left"),
|
||||
clusterEdge("broker-b", "broker-a", "right", "top"),
|
||||
|
||||
// Cluster → Consumers
|
||||
consumerEdge("broker-a", "analytics"),
|
||||
consumerEdge("broker-b", "analytics"),
|
||||
|
||||
consumerEdge("broker-a", "apps"),
|
||||
consumerEdge("broker-b", "apps"),
|
||||
consumerEdge("broker-c", "apps"),
|
||||
|
||||
consumerEdge("broker-b", "services"),
|
||||
consumerEdge("broker-c", "services"),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
cursor: "default",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
fitView
|
||||
panOnDrag={false}
|
||||
zoomOnScroll={false}
|
||||
zoomOnPinch={false}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
elementsSelectable={false}
|
||||
preventScrolling={true}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
proOptions={{
|
||||
hideAttribution: true,
|
||||
}}
|
||||
>
|
||||
<Background gap={32} size={1} color="rgba(47,105,179,0.08)" />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Edge Helpers ---------- */
|
||||
|
||||
const edge = (
|
||||
source: string,
|
||||
target: string,
|
||||
sourceHandle?: string,
|
||||
targetHandle?: string,
|
||||
): Edge => ({
|
||||
id: `${source}-${target}`,
|
||||
source,
|
||||
target,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
animated: true,
|
||||
style: {
|
||||
stroke: blue,
|
||||
strokeWidth: 1.5,
|
||||
},
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: blue,
|
||||
},
|
||||
});
|
||||
|
||||
const consumerEdge = (source: string, target: string): Edge => ({
|
||||
id: `${source}-${target}`,
|
||||
source,
|
||||
target,
|
||||
animated: true,
|
||||
style: {
|
||||
stroke: green,
|
||||
strokeWidth: 1.5,
|
||||
},
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: blue,
|
||||
},
|
||||
});
|
||||
|
||||
const clusterEdge = (
|
||||
a: string,
|
||||
b: string,
|
||||
sourceHandle?: string,
|
||||
targetHandle?: string,
|
||||
): Edge => ({
|
||||
id: `${a}-${b}`,
|
||||
source: a,
|
||||
target: b,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
animated: true,
|
||||
type: "smoothstep",
|
||||
style: {
|
||||
stroke: orange,
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: "6 6",
|
||||
},
|
||||
});
|
||||
|
||||
/* ---------- Node Styles ---------- */
|
||||
|
||||
const baseBox = {
|
||||
borderRadius: 8,
|
||||
padding: "16px 20px",
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
border: `1px solid ${blue}`,
|
||||
color: blue,
|
||||
background: "rgba(47,105,179,0.05)",
|
||||
minWidth: 120,
|
||||
textAlign: "center" as const,
|
||||
};
|
||||
|
||||
const protocolStyle = {
|
||||
...baseBox,
|
||||
};
|
||||
|
||||
const consumerStyle = {
|
||||
...baseBox,
|
||||
color: green,
|
||||
border: `1px solid ${green}`,
|
||||
background: "rgba(40, 184, 40, 0.08)",
|
||||
};
|
||||
|
||||
const brokerStyle = {
|
||||
borderRadius: 12,
|
||||
padding: "20px 24px",
|
||||
fontSize: 18,
|
||||
fontWeight: 700,
|
||||
border: `1px solid ${orange}`,
|
||||
color: orange,
|
||||
background: "rgba(249,163,42,0.08)",
|
||||
boxShadow: "0 0 18px rgba(249,163,42,0.35)",
|
||||
minWidth: 160,
|
||||
textAlign: "center" as const,
|
||||
};
|
||||
|
||||
function BrokerNode() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="border p-1 rounded-md text-center w-full">
|
||||
<span className="text-(--flux-blue)">Flux</span>
|
||||
<span className="text-(--flux-orange)">MQ</span>
|
||||
</div>
|
||||
<div className="border p-1 rounded-md text-center w-full">
|
||||
Durable Queue
|
||||
</div>
|
||||
{/* Targets */}
|
||||
<Handle type="target" position={Position.Left} id="left" />
|
||||
<Handle type="target" position={Position.Top} id="top" />
|
||||
|
||||
{/* Sources */}
|
||||
<Handle type="source" position={Position.Right} id="right" />
|
||||
<Handle type="source" position={Position.Bottom} id="bottom" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
'use client';
|
||||
import SearchDialog from '@/components/search';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
export function Provider({ children }: { children: ReactNode }) {
|
||||
// @ts-expect-error - fumadocs type definitions don't match implementation for children prop
|
||||
return <RootProvider search={{ SearchDialog }}>{children}</RootProvider>;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
import {
|
||||
SearchDialog,
|
||||
SearchDialogClose,
|
||||
SearchDialogContent,
|
||||
SearchDialogHeader,
|
||||
SearchDialogIcon,
|
||||
SearchDialogInput,
|
||||
SearchDialogList,
|
||||
SearchDialogOverlay,
|
||||
type SharedProps,
|
||||
} from 'fumadocs-ui/components/dialog/search';
|
||||
import { useDocsSearch } from 'fumadocs-core/search/client';
|
||||
import { create } from '@orama/orama';
|
||||
import { useI18n } from 'fumadocs-ui/contexts/i18n';
|
||||
|
||||
function initOrama() {
|
||||
return create({
|
||||
schema: { _: 'string' },
|
||||
// https://docs.orama.com/docs/orama-js/supported-languages
|
||||
language: 'english',
|
||||
});
|
||||
}
|
||||
|
||||
export default function DefaultSearchDialog(props: SharedProps) {
|
||||
const { locale } = useI18n(); // (optional) for i18n
|
||||
const { search, setSearch, query } = useDocsSearch({
|
||||
type: 'static',
|
||||
initOrama,
|
||||
locale,
|
||||
});
|
||||
|
||||
return (
|
||||
<SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
|
||||
<SearchDialogOverlay />
|
||||
<SearchDialogContent>
|
||||
<SearchDialogHeader>
|
||||
<SearchDialogIcon />
|
||||
<SearchDialogInput />
|
||||
<SearchDialogClose />
|
||||
</SearchDialogHeader>
|
||||
<SearchDialogList items={query.data !== 'empty' ? query.data : null} />
|
||||
</SearchDialogContent>
|
||||
</SearchDialog>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: FluxMQ Architecture
|
||||
description: Comprehensive system design overview covering layered architecture, protocol adapters, domain logic, and multi-protocol support
|
||||
---
|
||||
|
||||
# FluxMQ Architecture
|
||||
|
||||
## Overview
|
||||
@@ -7,6 +12,7 @@ FluxMQ runs **independent protocol brokers** (MQTT, AMQP 1.0, AMQP 0.9.1) and us
|
||||
This document focuses on the MQTT broker internals and the shared queue layer, and calls out where AMQP fits into the system.
|
||||
|
||||
**Design Philosophy:**
|
||||
|
||||
1. **Domain-Driven Design** - Pure business logic isolated from protocol concerns
|
||||
2. **Protocol Adapters** - Stateless handlers translate packets/requests to domain operations
|
||||
3. **Multi-Protocol Support** - MQTT transports share a broker; AMQP brokers are separate; queues provide cross-protocol durability
|
||||
@@ -53,11 +59,13 @@ This document focuses on the MQTT broker internals and the shared queue layer, a
|
||||
```
|
||||
|
||||
Key Architecture Insight:
|
||||
|
||||
- MQTT transports (TCP/WS/HTTP/CoAP) share ONE MQTT broker
|
||||
- AMQP 1.0 and AMQP 0.9.1 each have their own broker and router
|
||||
- Brokers route queue-capable traffic to the shared Queue Manager
|
||||
- Pub/sub is protocol-local; queues are shared and topic-bound
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
## Layered Architecture Deep Dive
|
||||
|
||||
@@ -121,13 +129,15 @@ This layer contains all network-facing servers and protocol bridges. The MQTT tr
|
||||
3. Parses JSON request:
|
||||
```json
|
||||
{"topic": "sensor/temp", "payload": "...", "qos": 1, "retain": false}
|
||||
```
|
||||
````
|
||||
|
||||
4. **Directly calls domain layer**: `broker.Publish(msg)`
|
||||
5. Returns JSON response
|
||||
|
||||
**Key Difference**: HTTP bridge bypasses MQTT protocol layer entirely - it translates HTTP requests directly to domain operations.
|
||||
|
||||
**Use Cases**:
|
||||
|
||||
- Publish from web applications without MQTT client library
|
||||
- Serverless functions (AWS Lambda, Cloud Functions)
|
||||
- REST API integrations
|
||||
@@ -143,6 +153,7 @@ This layer contains all network-facing servers and protocol bridges. The MQTT tr
|
||||
**Key Files**: `server/coap/server.go`
|
||||
|
||||
**What it does** (when fully implemented):
|
||||
|
||||
1. CoAP server listening on UDP (default: `:5683`)
|
||||
2. Handles CoAP requests:
|
||||
- `POST /mqtt/publish/<topic>` - Publish to topic
|
||||
@@ -154,6 +165,7 @@ This layer contains all network-facing servers and protocol bridges. The MQTT tr
|
||||
**Current Status**: Stub implementation - handlers defined, awaits UDP server setup
|
||||
|
||||
**Use Cases**:
|
||||
|
||||
- Constrained IoT devices (low power, limited bandwidth)
|
||||
- Sensor networks
|
||||
- Embedded systems without full MQTT stack
|
||||
@@ -167,9 +179,11 @@ This layer contains all network-facing servers and protocol bridges. The MQTT tr
|
||||
**Responsibility**: Detect MQTT protocol version and create appropriate handler
|
||||
|
||||
**Key Files**:
|
||||
|
||||
- `broker/connection.go` - `HandleConnection()` function
|
||||
|
||||
**What it does**:
|
||||
|
||||
1. Wraps `net.Conn` in MQTT codec (`core.NewConnection`)
|
||||
2. Reads first packet (must be CONNECT)
|
||||
3. Inspects `ProtocolVersion` field:
|
||||
@@ -179,6 +193,7 @@ This layer contains all network-facing servers and protocol bridges. The MQTT tr
|
||||
5. Delegates to `handler.HandleConnect(conn, connectPacket)`
|
||||
|
||||
**Code Flow**:
|
||||
|
||||
```go
|
||||
func HandleConnection(broker *Broker, conn core.Connection) {
|
||||
pkt := conn.ReadPacket()
|
||||
@@ -204,11 +219,13 @@ func HandleConnection(broker *Broker, conn core.Connection) {
|
||||
**Responsibility**: Translate MQTT packets into domain operations
|
||||
|
||||
**Key Characteristics**:
|
||||
|
||||
- **Stateless** - No internal state, operates on session passed as parameter
|
||||
- **One per protocol version** - `V3Handler` for MQTT 3.1.1/4.0, `V5Handler` for MQTT 5.0
|
||||
- **Implements Handler interface** - Common interface for all protocol versions
|
||||
|
||||
**Handler Interface**:
|
||||
|
||||
```go
|
||||
type Handler interface {
|
||||
HandleConnect(conn core.Connection, pkt packets.ControlPacket) error
|
||||
@@ -231,6 +248,7 @@ type Handler interface {
|
||||
8. **Run session loop** - After CONNECT, enter packet read loop
|
||||
|
||||
**Example: V5Handler.HandlePublish**:
|
||||
|
||||
```go
|
||||
func (h *V5Handler) HandlePublish(s *session.Session, pkt packets.ControlPacket) error {
|
||||
start := time.Now()
|
||||
@@ -292,12 +310,14 @@ func (b *Broker) runSession(handler Handler, s *session.Session) error {
|
||||
**Responsibility**: Core MQTT business logic, completely protocol-agnostic
|
||||
|
||||
**Key Characteristics**:
|
||||
|
||||
- **Pure domain methods** - No knowledge of packets or protocols
|
||||
- **Direct instrumentation** - Logger and metrics injected via constructor
|
||||
- **Single Responsibility** - Each method does one thing
|
||||
- **Infrastructure-agnostic** - Uses interfaces for storage, routing
|
||||
|
||||
**Constructor**:
|
||||
|
||||
```go
|
||||
func NewBroker(logger *slog.Logger, stats *Stats) *Broker {
|
||||
if logger == nil {
|
||||
@@ -321,6 +341,7 @@ func NewBroker(logger *slog.Logger, stats *Stats) *Broker {
|
||||
**Domain Methods**:
|
||||
|
||||
#### CreateSession
|
||||
|
||||
Creates or retrieves a session, handles clean start logic.
|
||||
|
||||
```go
|
||||
@@ -347,6 +368,7 @@ func (b *Broker) CreateSession(clientID string, opts SessionOptions) (*session.S
|
||||
```
|
||||
|
||||
#### Publish
|
||||
|
||||
Handles retained messages and distributes to subscribers.
|
||||
|
||||
```go
|
||||
@@ -372,6 +394,7 @@ func (b *Broker) Publish(msg Message) error {
|
||||
```
|
||||
|
||||
#### Subscribe
|
||||
|
||||
Adds subscription and delivers retained messages.
|
||||
|
||||
```go
|
||||
@@ -410,6 +433,7 @@ func (b *Broker) logError(op string, err error, attrs ...any) {
|
||||
```
|
||||
|
||||
**Benefits of Direct Instrumentation**:
|
||||
|
||||
1. **Visibility** - See exactly what's logged in the source code
|
||||
2. **Performance** - No function call overhead, compiler can inline
|
||||
3. **Flexibility** - Can add context-specific attributes easily
|
||||
@@ -424,18 +448,21 @@ func (b *Broker) logError(op string, err error, attrs ...any) {
|
||||
**Components**:
|
||||
|
||||
#### Router (broker/router.go)
|
||||
|
||||
- **Trie-based topic matching** for efficient wildcard subscriptions
|
||||
- `Subscribe(clientID, filter, qos)` - Add subscription
|
||||
- `Unsubscribe(clientID, filter)` - Remove subscription
|
||||
- `Match(topic)` - Find all subscriptions matching a topic
|
||||
|
||||
#### Session Cache (session/cache.go)
|
||||
|
||||
- **In-memory map** of active sessions
|
||||
- `Get(clientID)` - Retrieve session
|
||||
- `Set(clientID, session)` - Store session
|
||||
- `Delete(clientID)` - Remove session
|
||||
|
||||
#### Storage (storage/)
|
||||
|
||||
- **Interfaces** for persistence
|
||||
- **Memory implementation** for messages, retained, wills, subscriptions
|
||||
- **Pluggable** - Can swap for Redis, PostgreSQL, etc.
|
||||
@@ -451,17 +478,20 @@ using MQTT-style topic patterns.
|
||||
**Core Components**:
|
||||
|
||||
#### Queue Manager (queue/manager.go)
|
||||
|
||||
- **Routing**: Matches a publish topic against queue bindings and appends to each match
|
||||
- **Consumer Groups**: Manages consumer membership and cursors per group
|
||||
- **Delivery**: Dispatches messages to protocol brokers via a single `DeliverFn`
|
||||
- **Cluster-aware**: Forwards publishes to nodes that host matching consumers
|
||||
|
||||
#### Log Storage Adapter (logstorage/)
|
||||
|
||||
- **Append-only log** for durable queues
|
||||
- **Topic index** to map topics → queues
|
||||
- **Config store** for queue metadata and bindings
|
||||
|
||||
**Key Insight**:
|
||||
|
||||
- Pub/sub is protocol-local.
|
||||
- Queues are shared and topic-bound across protocols.
|
||||
|
||||
@@ -691,12 +721,14 @@ Result: ONE message published via HTTP → delivered to ALL
|
||||
### 1. Why No Middleware/Decorators?
|
||||
|
||||
**Problem with middleware**:
|
||||
|
||||
- Internal method calls bypass middleware chain
|
||||
- Performance overhead from function wrapping
|
||||
- Hidden control flow
|
||||
- Complex debugging (stack traces through wrappers)
|
||||
|
||||
**Our solution**:
|
||||
|
||||
- Direct instrumentation at domain layer
|
||||
- Explicit logging/metrics calls
|
||||
- Clear, linear control flow
|
||||
@@ -705,22 +737,26 @@ Result: ONE message published via HTTP → delivered to ALL
|
||||
### 2. Why Stateless Protocol Handlers?
|
||||
|
||||
**Benefits**:
|
||||
|
||||
- No state to manage or synchronize
|
||||
- Can create new handler per connection (cheap)
|
||||
- Easy to test (just translation logic)
|
||||
- No shared mutable state
|
||||
|
||||
**Trade-off**:
|
||||
|
||||
- Session is passed as parameter to each method
|
||||
- Handler needs reference to broker
|
||||
|
||||
### 3. Why Separate V3Handler and V5Handler?
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
1. Single handler with version checks → Complex, hard to read
|
||||
2. Handler per packet type → Too granular, code duplication
|
||||
|
||||
**Our choice**: One handler per protocol version
|
||||
|
||||
- Each handler focuses on one protocol's quirks
|
||||
- Easy to understand and maintain
|
||||
- Clear separation of v3 vs v5 logic
|
||||
@@ -729,12 +765,14 @@ Result: ONE message published via HTTP → delivered to ALL
|
||||
### 4. Why Logger and Metrics in Broker Constructor?
|
||||
|
||||
**Benefits**:
|
||||
|
||||
- Dependency injection principle
|
||||
- Testable (can inject mock logger/metrics)
|
||||
- Explicit dependencies
|
||||
- No global state
|
||||
|
||||
**Usage**:
|
||||
|
||||
```go
|
||||
// Production
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
@@ -750,6 +788,7 @@ b := broker.NewBroker(nil, nil) // Uses defaults
|
||||
### Unit Tests
|
||||
|
||||
**Domain Layer** (broker/broker.go):
|
||||
|
||||
```go
|
||||
func TestBroker_Publish(t *testing.T) {
|
||||
b := NewBroker(nil, nil) // Default logger/metrics
|
||||
@@ -762,6 +801,7 @@ func TestBroker_Publish(t *testing.T) {
|
||||
```
|
||||
|
||||
**Protocol Handlers** (broker/v3_handler_test.go):
|
||||
|
||||
```go
|
||||
func TestV3Handler_HandlePublish(t *testing.T) {
|
||||
b := NewBroker(nil, nil)
|
||||
@@ -807,21 +847,25 @@ func TestE2E_PublishSubscribe(t *testing.T) {
|
||||
## Performance Characteristics
|
||||
|
||||
### Zero Indirection
|
||||
|
||||
- Direct function calls, no decorators
|
||||
- Compiler can inline logging checks
|
||||
- No allocations for middleware chains
|
||||
|
||||
### Efficient Data Structures
|
||||
|
||||
- **Trie-based router**: O(m) topic matching, where m = topic depth
|
||||
- **Session cache**: O(1) lookup by client ID
|
||||
- **Object pools**: Packet and buffer reuse
|
||||
|
||||
### Concurrency
|
||||
|
||||
- **Fine-grained locking**: Only lock session map, not entire broker
|
||||
- **Non-blocking I/O**: Each connection runs in own goroutine
|
||||
- **No global locks**: Router and storage use internal synchronization
|
||||
|
||||
### Memory Efficiency
|
||||
|
||||
- **Zero-copy packet parsing** where possible
|
||||
- **Shared topic strings** in subscriptions
|
||||
- **Bounded message queues** prevent memory exhaustion
|
||||
@@ -831,6 +875,7 @@ func TestE2E_PublishSubscribe(t *testing.T) {
|
||||
### New MQTT Version (e.g., v6)
|
||||
|
||||
1. Create `broker/v6_handler.go`:
|
||||
|
||||
```go
|
||||
type V6Handler struct {
|
||||
broker *Broker
|
||||
@@ -841,6 +886,7 @@ func (h *V6Handler) HandleConnect(...) { ... }
|
||||
```
|
||||
|
||||
2. Update `broker/connection.go`:
|
||||
|
||||
```go
|
||||
if v6Connect, ok := pkt.(*v6.Connect); ok {
|
||||
handler := NewV6Handler(broker)
|
||||
@@ -902,6 +948,7 @@ broker.HandleConnection(broker, wsConnection)
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- HTTP bridge: Direct domain calls, bypasses protocol layer
|
||||
- WebSocket: Implements `core.Connection`, reuses all MQTT logic
|
||||
- CoAP: Similar to HTTP, direct `broker.Publish()` calls
|
||||
@@ -930,11 +977,13 @@ b.messages = NewRedisStore(redisClient)
|
||||
### Logging Levels
|
||||
|
||||
**Production**: `info` level
|
||||
|
||||
- Connection events
|
||||
- Errors
|
||||
- Performance metrics
|
||||
|
||||
**Debug**: `debug` level
|
||||
|
||||
- Every packet
|
||||
- Domain operations
|
||||
- Performance timings
|
||||
@@ -942,6 +991,7 @@ b.messages = NewRedisStore(redisClient)
|
||||
### Metrics Collection
|
||||
|
||||
The `Stats` type exposes:
|
||||
|
||||
- Connection counts (current, total, disconnects)
|
||||
- Message rates (received, sent, publish)
|
||||
- Byte counts (received, sent)
|
||||
@@ -964,6 +1014,7 @@ b := broker.NewBroker(logger, stats)
|
||||
## Conclusion
|
||||
|
||||
This architecture achieves:
|
||||
|
||||
- ✅ **Clean separation** between transport, protocol, and domain
|
||||
- ✅ **Multi-protocol support** - MQTT transports share a broker; AMQP brokers are independent
|
||||
- ✅ **Cross-protocol durability** via the shared queue manager
|
||||
@@ -972,6 +1023,7 @@ This architecture achieves:
|
||||
- ✅ **Maintainability** with clear, single-responsibility components
|
||||
|
||||
The key insights:
|
||||
|
||||
1. **Separate what changes (protocols) from what stays stable (domain logic)**
|
||||
2. **Protocol handlers are adapters** that translate between wire formats and domain models
|
||||
3. **MQTT transports share one broker** - messages flow across MQTT/HTTP/CoAP
|
||||
@@ -987,12 +1039,14 @@ This design makes adding new protocols trivial while keeping the core broker sim
|
||||
The broker includes a comprehensive webhook system for asynchronous event notifications, enabling integrations with external services.
|
||||
|
||||
**Key Features**:
|
||||
|
||||
- **Non-blocking** worker pool architecture (zero broker performance impact)
|
||||
- **Circuit breaker** and retry with exponential backoff
|
||||
- **Flexible filtering** by event type and MQTT topic patterns
|
||||
- **Protocol-agnostic** design (HTTP implemented, gRPC planned)
|
||||
|
||||
**Event Types**:
|
||||
|
||||
- Connection events (`client.connected`, `client.disconnected`, `client.session_takeover`)
|
||||
- Message events (`message.published`, `message.delivered`, `message.retained`)
|
||||
- Subscription events (`subscription.created`, `subscription.removed`)
|
||||
@@ -1009,6 +1063,7 @@ For complete webhook documentation including architecture details, configuration
|
||||
The broker supports distributed clustering for high availability and load distribution.
|
||||
|
||||
**Key Features**:
|
||||
|
||||
- **Embedded everything** - Single binary with embedded etcd, gRPC, and BadgerDB
|
||||
- **Session takeover** - Seamless client migration between nodes
|
||||
- **Strong consistency** - etcd Raft for session ownership and subscriptions
|
||||
@@ -1016,6 +1071,7 @@ The broker supports distributed clustering for high availability and load distri
|
||||
- **Linear scalability** - Add nodes to increase capacity
|
||||
|
||||
**Components**:
|
||||
|
||||
- **etcd** (embedded) - Distributed coordination and metadata
|
||||
- **gRPC** - Inter-broker message routing
|
||||
- **BadgerDB** (local) - Per-node session and message persistence
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Broker & Message Routing
|
||||
description: Internal MQTT broker architecture, session management, message routing mechanisms, topic matching, QoS handling, and cluster integration
|
||||
---
|
||||
|
||||
# Broker & Message Routing (MQTT)
|
||||
|
||||
This document explains the MQTT broker's internal architecture, message routing mechanisms, session management, and how clustering integrates with the core broker logic. AMQP 1.0 and AMQP 0.9.1 have their own brokers and routers; the shared queue manager is the binding layer between protocols.
|
||||
@@ -106,6 +111,7 @@ func NewBroker(
|
||||
```
|
||||
|
||||
**Dependency Injection**:
|
||||
|
||||
- `store`: Pluggable storage (memory, BadgerDB, future: PostgreSQL)
|
||||
- `clust`: Cluster coordination (nil, NoopCluster, or EtcdCluster)
|
||||
- `logger`: Structured logging (slog)
|
||||
@@ -216,6 +222,7 @@ func (b *Broker) CreateSession(clientID string, opts SessionOptions) (*session.S
|
||||
```
|
||||
|
||||
**Key Steps**:
|
||||
|
||||
1. Check cache for existing session
|
||||
2. Handle clean_start flag (destroy or resume)
|
||||
3. Create new session with inflight tracker and queue
|
||||
@@ -303,13 +310,13 @@ func (b *Broker) handleDisconnect(s *session.Session, graceful bool) {
|
||||
|
||||
**Persistent vs Clean Sessions**:
|
||||
|
||||
| Aspect | Clean Session | Persistent Session |
|
||||
|--------|---------------|-------------------|
|
||||
| On Connect | Destroy old | Resume old |
|
||||
| Subscriptions | Lost | Restored |
|
||||
| Offline Queue | Lost | Restored |
|
||||
| Inflight | Lost | Restored |
|
||||
| On Disconnect | Destroyed | Saved to storage |
|
||||
| Aspect | Clean Session | Persistent Session |
|
||||
| ----------------- | ------------------- | ---------------------- |
|
||||
| On Connect | Destroy old | Resume old |
|
||||
| Subscriptions | Lost | Restored |
|
||||
| Offline Queue | Lost | Restored |
|
||||
| Inflight | Lost | Restored |
|
||||
| On Disconnect | Destroyed | Saved to storage |
|
||||
| Cluster Ownership | Released on destroy | Released on disconnect |
|
||||
|
||||
## Message Routing
|
||||
@@ -406,10 +413,12 @@ func (b *Broker) distribute(topic string, payload []byte, qos byte, retain bool,
|
||||
```
|
||||
|
||||
**Two-Phase Delivery**:
|
||||
|
||||
1. **Local Phase**: Match topic against local router, deliver immediately
|
||||
2. **Cluster Phase**: Query etcd for remote subscribers, send via gRPC
|
||||
|
||||
**Why Not Broadcast?**
|
||||
|
||||
- Broadcasting to all nodes is wasteful
|
||||
- Most topics have few subscribers
|
||||
- Targeted delivery scales better
|
||||
@@ -616,9 +625,10 @@ func (r *Router) matchRecursive(node *trieNode, levels []string, depth int, resu
|
||||
```
|
||||
|
||||
**Complexity**:
|
||||
|
||||
- Subscribe: O(L) where L = levels in filter
|
||||
- Match: O(L * B) where B = branching factor (usually small)
|
||||
- Space: O(N * L) where N = number of subscriptions
|
||||
- Match: O(L \* B) where B = branching factor (usually small)
|
||||
- Space: O(N \* L) where N = number of subscriptions
|
||||
|
||||
**Example Matching**:
|
||||
|
||||
@@ -640,11 +650,11 @@ Result: [sensor/living/temp, sensor/+/temp, sensor/#, #]
|
||||
|
||||
### QoS Levels
|
||||
|
||||
| QoS | Guarantee | Flow |
|
||||
|-----|-----------|------|
|
||||
| 0 | At most once | PUBLISH → (done) |
|
||||
| 1 | At least once | PUBLISH → PUBACK |
|
||||
| 2 | Exactly once | PUBLISH → PUBREC → PUBREL → PUBCOMP |
|
||||
| QoS | Guarantee | Flow |
|
||||
| --- | ------------- | ----------------------------------- |
|
||||
| 0 | At most once | PUBLISH → (done) |
|
||||
| 1 | At least once | PUBLISH → PUBACK |
|
||||
| 2 | Exactly once | PUBLISH → PUBREC → PUBREL → PUBCOMP |
|
||||
|
||||
### QoS 0 (Fire and Forget)
|
||||
|
||||
@@ -659,6 +669,7 @@ func (b *Broker) DeliverToSession(s *session.Session, msg Message) (uint16, erro
|
||||
```
|
||||
|
||||
**Characteristics**:
|
||||
|
||||
- No acknowledgment
|
||||
- No retransmission
|
||||
- No persistence
|
||||
@@ -839,6 +850,7 @@ func (b *Broker) DeliverToClient(ctx context.Context, clientID, topic string, pa
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
|
||||
1. Remote node calls `transport.SendPublish(nodeID, clientID, ...)`
|
||||
2. gRPC client sends `RoutePublish` RPC
|
||||
3. Local transport receives RPC, calls `HandlePublish()`
|
||||
@@ -879,26 +891,31 @@ func (b *Broker) destroySessionLocked(s *session.Session) error {
|
||||
The broker architecture provides:
|
||||
|
||||
**Clean Separation**:
|
||||
|
||||
- Protocol handlers translate packets
|
||||
- Broker contains pure domain logic
|
||||
- Storage and cluster are pluggable
|
||||
|
||||
**Efficient Routing**:
|
||||
|
||||
- Trie-based topic matching: O(L)
|
||||
- Local delivery: immediate
|
||||
- Remote delivery: targeted via etcd query
|
||||
|
||||
**QoS Guarantees**:
|
||||
|
||||
- QoS 0: Fire and forget
|
||||
- QoS 1: At least once, with retransmission
|
||||
- QoS 2: Exactly once, with state machine
|
||||
|
||||
**Cluster Integration**:
|
||||
|
||||
- Session ownership tracked in etcd
|
||||
- Subscriptions visible cluster-wide
|
||||
- Messages routed directly via gRPC
|
||||
- Seamless local/remote delivery
|
||||
|
||||
For more details, see:
|
||||
|
||||
- [Clustering](clustering.md) - Distributed broker design, etcd, gRPC, BadgerDB
|
||||
- [Configuration](configuration.md) - Setup and tuning
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Client Libraries
|
||||
description: Pure Go client libraries for MQTT 3.1.1/5.0 and AMQP 0.9.1 with durable queue support, auto-reconnect, and comprehensive features
|
||||
---
|
||||
|
||||
# Client Libraries
|
||||
|
||||
Pure Go client libraries for MQTT 3.1.1/5.0 and AMQP 0.9.1 with durable queue support.
|
||||
@@ -43,23 +48,23 @@ func main() {
|
||||
})
|
||||
|
||||
c := client.New(opts)
|
||||
|
||||
|
||||
// Connect
|
||||
if err := c.Connect(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer c.Disconnect()
|
||||
|
||||
|
||||
// Subscribe
|
||||
if err := c.SubscribeSingle("sensors/#", 1); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
// Publish
|
||||
if err := c.Publish("sensors/temp", []byte("22.5"), 1, false); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
// Keep running
|
||||
select {}
|
||||
}
|
||||
@@ -226,11 +231,13 @@ Reject/DLQ wiring in the broker is pending.
|
||||
MQTT v3 can publish and subscribe to queue topics, but acknowledgments require MQTT v5 user properties.
|
||||
|
||||
**When to use queues instead of regular pub/sub:**
|
||||
|
||||
- You need at-least-once processing with explicit acknowledgments
|
||||
- Multiple consumers should share the workload (consumer groups)
|
||||
- Failed messages need retry logic or dead-letter handling
|
||||
|
||||
**Key concepts:**
|
||||
|
||||
- **Queue**: Persistent message buffer with ordered delivery per queue (single log)
|
||||
- **Consumer Group**: Multiple consumers share messages from the same queue
|
||||
- **Acknowledgment**: Confirm success (Ack), request redelivery (Nack), or reject permanently (Reject)
|
||||
@@ -259,7 +266,7 @@ err := c.SubscribeToQueue("orders", "order-processors", func(msg *client.QueueMe
|
||||
log.Printf("Message ID: %s", msg.MessageID)
|
||||
log.Printf("Group: %s", msg.GroupID)
|
||||
log.Printf("Offset: %d", msg.Offset)
|
||||
|
||||
|
||||
// Process message...
|
||||
if processedOK {
|
||||
msg.Ack() // Message removed from queue
|
||||
@@ -287,6 +294,7 @@ c.AckWithGroup("orders", "msg-12345", "processors")
|
||||
c.NackWithGroup("orders", "msg-12345", "processors")
|
||||
c.RejectWithGroup("orders", "msg-12345", "processors")
|
||||
```
|
||||
|
||||
Note: MQTT queue acknowledgments require MQTT v5 and the broker expects
|
||||
`message-id` and `group-id` user properties on ack messages. `QueueMessage.Ack()`
|
||||
sends both when they are present on incoming messages.
|
||||
@@ -322,7 +330,7 @@ func main() {
|
||||
// Subscribe to order queue with consumer group
|
||||
err := c.SubscribeToQueue("orders", "processors", func(msg *client.QueueMessage) {
|
||||
log.Printf("Order received: %s", msg.Payload)
|
||||
|
||||
|
||||
// Simulate processing
|
||||
if processOrder(msg.Payload) {
|
||||
if err := msg.Ack(); err != nil {
|
||||
@@ -412,18 +420,18 @@ opts.Will = &client.WillMessage{
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Error | Cause |
|
||||
| -------------------- | -------------------------------------- |
|
||||
| `ErrNotConnected` | Operation attempted while disconnected |
|
||||
| `ErrNoServers` | No broker addresses configured |
|
||||
| `ErrEmptyClientID` | ClientID not set |
|
||||
| `ErrInvalidProtocol` | Protocol version must be 4 or 5 |
|
||||
| `ErrInvalidQoS` | QoS must be 0, 1, or 2 |
|
||||
| `ErrInvalidTopic` | Empty or invalid topic string |
|
||||
| `ErrInvalidMessage` | Message is nil or invalid |
|
||||
| `ErrMaxInflight` | Too many pending messages |
|
||||
| `ErrQueueAckRequiresV5` | Queue acks require MQTT v5 user properties |
|
||||
| `ErrQueueAckMissingGroup` | `group-id` missing for queue ack |
|
||||
| Error | Cause |
|
||||
| ------------------------- | ------------------------------------------ |
|
||||
| `ErrNotConnected` | Operation attempted while disconnected |
|
||||
| `ErrNoServers` | No broker addresses configured |
|
||||
| `ErrEmptyClientID` | ClientID not set |
|
||||
| `ErrInvalidProtocol` | Protocol version must be 4 or 5 |
|
||||
| `ErrInvalidQoS` | QoS must be 0, 1, or 2 |
|
||||
| `ErrInvalidTopic` | Empty or invalid topic string |
|
||||
| `ErrInvalidMessage` | Message is nil or invalid |
|
||||
| `ErrMaxInflight` | Too many pending messages |
|
||||
| `ErrQueueAckRequiresV5` | Queue acks require MQTT v5 user properties |
|
||||
| `ErrQueueAckMissingGroup` | `group-id` missing for queue ack |
|
||||
|
||||
### Handling Connection Errors
|
||||
|
||||
@@ -452,6 +460,7 @@ opts.SetStore(store)
|
||||
```
|
||||
|
||||
Built-in stores:
|
||||
|
||||
- **MemoryStore** (default): In-memory, lost on restart
|
||||
|
||||
You can implement the `MessageStore` interface to persist QoS 1/2 in-flight data.
|
||||
@@ -571,6 +580,7 @@ if err := c.PublishToStream("events", []byte("hello"), nil); err != nil {
|
||||
```
|
||||
|
||||
Stream deliveries include:
|
||||
|
||||
- `x-stream-offset`
|
||||
- `x-stream-timestamp`
|
||||
- `x-work-acked` / `x-work-committed-offset`
|
||||
@@ -606,6 +616,7 @@ _ = c.CommitOffset("events", "my-group", lastProcessedOffset)
|
||||
Use the same consumer group name in both calls.
|
||||
|
||||
With manual commit:
|
||||
|
||||
- Messages are delivered but the committed offset doesn't advance automatically
|
||||
- On reconnect, delivery resumes from the last committed offset
|
||||
- Use `CommitOffset()` to advance the committed position
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Clustering
|
||||
description: Distributed broker clustering with embedded etcd, gRPC transport, session takeover, and high availability architecture
|
||||
---
|
||||
|
||||
# Clustering
|
||||
|
||||
This document provides a comprehensive overview of the MQTT broker's clustering capabilities, explaining how multiple broker nodes work together to provide a distributed, highly available message broker.
|
||||
@@ -26,6 +31,7 @@ The MQTT broker implements a **shared-nothing architecture with distributed coor
|
||||
- Routes messages to remote subscribers via gRPC
|
||||
|
||||
This design provides:
|
||||
|
||||
- **High availability**: Clients can connect to any node
|
||||
- **Load distribution**: Sessions spread across nodes
|
||||
- **No single point of failure**: All components are embedded
|
||||
@@ -34,6 +40,7 @@ This design provides:
|
||||
### Key Insight
|
||||
|
||||
Unlike traditional message brokers that use a shared database or message queue, this broker uses a **hybrid approach**:
|
||||
|
||||
- **Strong consistency** (via etcd Raft): Session ownership, subscriptions
|
||||
- **Direct communication** (via gRPC): Message routing between nodes
|
||||
- **Local storage** (via BadgerDB): Session state, offline messages
|
||||
@@ -41,9 +48,11 @@ Unlike traditional message brokers that use a shared database or message queue,
|
||||
## Design Goals
|
||||
|
||||
### 1. Embedded Everything
|
||||
|
||||
**Goal**: Deploy as a single Go binary with no external dependencies.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
- Embedded etcd server (not etcd client to external cluster)
|
||||
- Embedded gRPC server for inter-broker communication
|
||||
- Embedded BadgerDB for local persistence
|
||||
@@ -51,9 +60,11 @@ Unlike traditional message brokers that use a shared database or message queue,
|
||||
**Benefit**: Simplified operations, consistent deployment model
|
||||
|
||||
### 2. Automatic Coordination
|
||||
|
||||
**Goal**: Nodes automatically discover and coordinate with each other.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
- etcd handles leader election for background tasks
|
||||
- Automatic session ownership tracking
|
||||
- Cluster-wide subscription visibility
|
||||
@@ -61,9 +72,11 @@ Unlike traditional message brokers that use a shared database or message queue,
|
||||
**Benefit**: No manual intervention for node failures or additions
|
||||
|
||||
### 3. Protocol Compliance
|
||||
|
||||
**Goal**: Full MQTT semantics in clustered mode.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
- Session takeover across nodes
|
||||
- QoS guarantees maintained
|
||||
- Retained messages visible cluster-wide
|
||||
@@ -72,9 +85,11 @@ Unlike traditional message brokers that use a shared database or message queue,
|
||||
**Benefit**: Clients see identical behavior regardless of cluster size
|
||||
|
||||
### 4. Linear Scalability
|
||||
|
||||
**Goal**: Adding nodes increases capacity proportionally.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
- Sessions distributed across nodes
|
||||
- Messages routed directly (no broadcast storms)
|
||||
- Local topic routing (trie per node)
|
||||
@@ -124,18 +139,21 @@ Unlike traditional message brokers that use a shared database or message queue,
|
||||
### Component Responsibilities
|
||||
|
||||
#### 1. Broker Core
|
||||
|
||||
- Session lifecycle management
|
||||
- Local message routing (trie-based)
|
||||
- Protocol handling (MQTT v3/v5)
|
||||
- Instrumentation (logging, metrics)
|
||||
|
||||
**Cluster Integration**:
|
||||
|
||||
- Registers session ownership on connect
|
||||
- Adds subscriptions to cluster on subscribe
|
||||
- Routes publishes to remote nodes
|
||||
- Implements `SessionManager` interface for session takeover callbacks
|
||||
|
||||
**SessionManager Interface**:
|
||||
|
||||
```go
|
||||
type SessionManager interface {
|
||||
// GetSessionStateAndClose captures session state and closes it
|
||||
@@ -151,28 +169,33 @@ type SessionManager interface {
|
||||
The broker implements this interface, allowing the cluster layer to request session state transfer during takeover operations.
|
||||
|
||||
#### 2. etcd (Embedded)
|
||||
|
||||
- **Raft consensus** for strong consistency
|
||||
- **Key-value store** for cluster metadata
|
||||
- **Leader election** for singleton tasks
|
||||
- **Watch API** for change notifications
|
||||
|
||||
**Stored Data**:
|
||||
|
||||
- Session ownership: `clientID → nodeID`
|
||||
- Subscriptions: `clientID:filter → {qos, options}`
|
||||
- Retained messages: `topic → message`
|
||||
- Will messages: `clientID → will`
|
||||
|
||||
#### 3. gRPC Transport
|
||||
|
||||
- **Bidirectional connections** between all nodes
|
||||
- **Protobuf serialization** for efficiency
|
||||
- **Request/response** semantics for routing
|
||||
|
||||
**RPC Methods**:
|
||||
|
||||
- `RoutePublish(clientID, topic, payload, qos, ...)`: Forward PUBLISH to remote subscriber
|
||||
- `TakeoverSession(clientID, fromNode, toNode)`: Migrate session between nodes with full state transfer
|
||||
|
||||
**Session State Transfer**:
|
||||
The `TakeoverSession` RPC returns a complete `SessionState` protobuf containing:
|
||||
|
||||
- Inflight messages (QoS 1/2 pending acknowledgments)
|
||||
- Offline queue (messages queued while disconnected)
|
||||
- Subscriptions with QoS levels
|
||||
@@ -180,11 +203,13 @@ The `TakeoverSession` RPC returns a complete `SessionState` protobuf containing:
|
||||
- Session expiry interval
|
||||
|
||||
#### 4. BadgerDB (Local)
|
||||
|
||||
- **LSM tree storage** for high write throughput
|
||||
- **Embedded** - no server process
|
||||
- **Per-node** - each node has its own database
|
||||
|
||||
**Stored Data**:
|
||||
|
||||
- Session state (local only)
|
||||
- Inflight messages (QoS 1/2)
|
||||
- Offline message queue
|
||||
@@ -214,11 +239,13 @@ etcd, err := embed.StartEtcd(eCfg)
|
||||
#### Raft Consensus
|
||||
|
||||
etcd uses the Raft consensus algorithm for:
|
||||
|
||||
- **Leader election**: One node becomes leader
|
||||
- **Log replication**: All nodes agree on operation order
|
||||
- **Strong consistency**: Reads guaranteed to see latest writes
|
||||
|
||||
**Raft Roles**:
|
||||
|
||||
- **Leader**: Handles all writes, replicates to followers
|
||||
- **Follower**: Replicates log, votes in elections
|
||||
- **Candidate**: Follower requesting votes during election
|
||||
@@ -241,19 +268,21 @@ Client → Leader → Replicate to Majority → Commit → Respond
|
||||
#### Configuration
|
||||
|
||||
**Bootstrap Node** (`node1.yaml`):
|
||||
|
||||
```yaml
|
||||
cluster:
|
||||
enabled: true
|
||||
node_id: "node1"
|
||||
etcd:
|
||||
data_dir: "/tmp/fluxmq/node1/etcd"
|
||||
bind_addr: "127.0.0.1:2380" # Raft peer communication
|
||||
client_addr: "127.0.0.1:2379" # Client API
|
||||
bind_addr: "127.0.0.1:2380" # Raft peer communication
|
||||
client_addr: "127.0.0.1:2379" # Client API
|
||||
initial_cluster: "node1=http://127.0.0.1:2380,node2=http://127.0.0.1:2480,node3=http://127.0.0.1:2580"
|
||||
bootstrap: true # cluster_state = "new"
|
||||
bootstrap: true # cluster_state = "new"
|
||||
```
|
||||
|
||||
**Port Mapping**:
|
||||
|
||||
- `bind_addr`: Raft protocol (node-to-node)
|
||||
- `client_addr`: etcd API (used by broker locally)
|
||||
|
||||
@@ -264,6 +293,7 @@ cluster:
|
||||
etcd stores data as key-value pairs with optional TTL (lease).
|
||||
|
||||
**Session Ownership**:
|
||||
|
||||
```
|
||||
Key: /mqtt/sessions/{clientID}
|
||||
Value: {"node_id": "node2", "lease_id": "7587869134..."}
|
||||
@@ -271,18 +301,21 @@ TTL: 30 seconds (auto-renewed while connected)
|
||||
```
|
||||
|
||||
When a client connects:
|
||||
|
||||
1. Node attempts `AcquireSession(clientID, nodeID)`
|
||||
2. etcd transaction: put if not exists
|
||||
3. Auto-renewing lease maintains ownership
|
||||
4. On disconnect, ownership released
|
||||
|
||||
**Subscriptions**:
|
||||
|
||||
```
|
||||
Key: /mqtt/subscriptions/{clientID}/{filter}
|
||||
Value: {"qos": 1, "no_local": false, ...}
|
||||
```
|
||||
|
||||
**Why Both Local and etcd?**
|
||||
|
||||
- **Local router**: Fast O(log n) matching for local subscribers
|
||||
- **etcd**: Cluster-wide visibility for routing to remote nodes
|
||||
|
||||
@@ -305,15 +338,18 @@ if err == nil {
|
||||
#### Performance Considerations
|
||||
|
||||
**Reads**:
|
||||
|
||||
- Linearizable reads: Contact leader (slower, guaranteed latest)
|
||||
- Serializable reads: Local replica (faster, may be stale)
|
||||
|
||||
**Writes**:
|
||||
|
||||
- All writes go through leader
|
||||
- Require quorum (majority) to commit
|
||||
- Latency: ~1-5ms in LAN, ~10-100ms WAN
|
||||
|
||||
**Best Practices**:
|
||||
|
||||
- Batch small writes when possible
|
||||
- Use transactions for atomic operations
|
||||
- Keep values small (<1KB recommended)
|
||||
@@ -332,6 +368,7 @@ gRPC provides the inter-broker communication layer for routing messages between
|
||||
#### Service Definition
|
||||
|
||||
`cluster/broker.proto`:
|
||||
|
||||
```protobuf
|
||||
syntax = "proto3";
|
||||
|
||||
@@ -361,6 +398,7 @@ message PublishResponse {
|
||||
#### Connection Management
|
||||
|
||||
**Peer Discovery**:
|
||||
|
||||
```yaml
|
||||
cluster:
|
||||
transport:
|
||||
@@ -371,6 +409,7 @@ cluster:
|
||||
```
|
||||
|
||||
**Connection Topology**:
|
||||
|
||||
```
|
||||
3-node cluster:
|
||||
|
||||
@@ -386,10 +425,12 @@ Fully connected mesh: N*(N-1)/2 connections
|
||||
#### Performance Considerations
|
||||
|
||||
**Latency**:
|
||||
|
||||
- LAN: ~1-2ms per RPC
|
||||
- WAN: 50-200ms depending on distance
|
||||
|
||||
**Throughput**:
|
||||
|
||||
- HTTP/2 multiplexing: Multiple RPCs per connection
|
||||
- Protobuf serialization: ~10x faster than JSON
|
||||
- No TLS (for now): Lower CPU overhead
|
||||
@@ -399,6 +440,7 @@ Fully connected mesh: N*(N-1)/2 connections
|
||||
#### Overview
|
||||
|
||||
BadgerDB is an embedded key-value store designed for high performance:
|
||||
|
||||
- **LSM tree** architecture (like RocksDB, LevelDB)
|
||||
- **Embedded**: No server process, library only
|
||||
- **Written in Go**: No CGO, pure Go
|
||||
@@ -423,6 +465,7 @@ Read Path:
|
||||
```
|
||||
|
||||
**LSM Tree Levels**:
|
||||
|
||||
- **L0**: Recently flushed from MemTable
|
||||
- **L1-Ln**: Compacted and merged SSTables
|
||||
- **Compaction**: Merge and sort SSTables to reduce read amplification
|
||||
@@ -432,6 +475,7 @@ Read Path:
|
||||
BadgerDB is a **pure key-value store** - no tables, no indexes, just `[]byte → []byte`.
|
||||
|
||||
**Session Store**:
|
||||
|
||||
```
|
||||
Key: session:{clientID}
|
||||
Value: JSON{
|
||||
@@ -444,12 +488,14 @@ Value: JSON{
|
||||
```
|
||||
|
||||
**Message Store**:
|
||||
|
||||
```
|
||||
Inflight: {clientID}/inflight/{packetID} → Message
|
||||
Queue: {clientID}/queue/{sequence} → Message
|
||||
```
|
||||
|
||||
**Subscription Store**:
|
||||
|
||||
```
|
||||
Key: sub:{clientID}:{filter}
|
||||
Value: JSON{qos: 1, no_local: false, ...}
|
||||
@@ -470,6 +516,7 @@ for range ticker.C {
|
||||
```
|
||||
|
||||
**Why GC?**
|
||||
|
||||
- Values stored separately in value log
|
||||
- Deleted/updated values leave garbage
|
||||
- GC compacts and reclaims space
|
||||
@@ -477,11 +524,13 @@ for range ticker.C {
|
||||
#### Performance Characteristics
|
||||
|
||||
**Write Performance**:
|
||||
|
||||
- Sequential writes to WAL: ~100K writes/sec
|
||||
- MemTable flush: Batched, amortized cost
|
||||
- Async mode (SyncWrites=false): Even faster, less durable
|
||||
|
||||
**Read Performance**:
|
||||
|
||||
- MemTable hit: ~1-2μs
|
||||
- SSTable hit: ~10-100μs (depending on level)
|
||||
- Bloom filters reduce unnecessary reads
|
||||
@@ -499,6 +548,7 @@ for range ticker.C {
|
||||
### Metadata (Strong Consistency via etcd)
|
||||
|
||||
**Session Ownership**
|
||||
|
||||
```
|
||||
Key: /mqtt/sessions/{clientID}
|
||||
Value: {"node_id": "node2", "lease_id": "7587869134..."}
|
||||
@@ -506,6 +556,7 @@ TTL: 30 seconds (auto-renewed while connected)
|
||||
```
|
||||
|
||||
**Subscriptions**
|
||||
|
||||
```
|
||||
Key: /mqtt/subscriptions/{clientID}/{filter}
|
||||
Value: {"qos": 1, "no_local": false, ...}
|
||||
@@ -561,6 +612,7 @@ Only the **elected leader** processes pending wills to avoid duplicates.
|
||||
### Scenario: Cross-Node Publish
|
||||
|
||||
**Setup**:
|
||||
|
||||
- Client A connected to Node1, subscribed to `sensor/#`
|
||||
- Client B connected to Node2, subscribed to `sensor/+/temp`
|
||||
- Client C connected to Node3, publishes to `sensor/living/temp`
|
||||
@@ -616,6 +668,7 @@ Only the **elected leader** processes pending wills to avoid duplicates.
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- Message never touches etcd (subscriptions cached locally)
|
||||
- No broadcast - targeted delivery only
|
||||
- Each node independently delivers to its local clients
|
||||
@@ -624,6 +677,7 @@ Only the **elected leader** processes pending wills to avoid duplicates.
|
||||
### Scenario: Session Takeover
|
||||
|
||||
**Setup**:
|
||||
|
||||
- Client D connected to Node1 with `clean_start=false`
|
||||
- Client D reconnects to Node2 (same clientID)
|
||||
|
||||
@@ -680,6 +734,7 @@ Only the **elected leader** processes pending wills to avoid duplicates.
|
||||
**Implementation Status**: ✅ **FULLY IMPLEMENTED**
|
||||
|
||||
The session takeover protocol is complete:
|
||||
|
||||
- ✅ gRPC `TakeoverSession` RPC with `SessionState` transfer
|
||||
- ✅ `broker.GetSessionStateAndClose()` captures full session state
|
||||
- ✅ State includes inflight, queue, subscriptions, will message
|
||||
@@ -694,17 +749,20 @@ The session takeover protocol is complete:
|
||||
**Scenario**: Node2 crashes while clients are connected.
|
||||
|
||||
**Impact**:
|
||||
|
||||
- Clients on Node2 lose connection
|
||||
- Session ownership leases expire (30s TTL)
|
||||
- Other nodes unaffected
|
||||
|
||||
**Recovery**:
|
||||
|
||||
1. Clients reconnect to Node1 or Node3
|
||||
2. Session ownership acquired by new node
|
||||
3. Subscriptions re-established
|
||||
4. Offline messages lost (if not persisted)
|
||||
|
||||
**Mitigation**:
|
||||
|
||||
- Client auto-reconnect with exponential backoff
|
||||
- Load balancer distributes connections
|
||||
- Future: Session state replication
|
||||
@@ -714,26 +772,31 @@ The session takeover protocol is complete:
|
||||
**Scenario**: Network partition splits etcd cluster.
|
||||
|
||||
**Impact**:
|
||||
|
||||
- Minority partition: Nodes can't acquire sessions (read-only)
|
||||
- Majority partition: Continues normally
|
||||
|
||||
**Recovery**:
|
||||
|
||||
- Partition heals automatically
|
||||
- etcd re-synchronizes
|
||||
- Minority nodes resume operations
|
||||
|
||||
**Design Choice**: CAP theorem - chose **CP** (Consistency + Partition tolerance)
|
||||
|
||||
- Availability sacrificed in minority partition
|
||||
- Prevents split-brain scenarios
|
||||
|
||||
### Message Loss
|
||||
|
||||
**Scenarios**:
|
||||
|
||||
1. **QoS 0**: Best effort - may lose on any failure
|
||||
2. **QoS 1**: Persisted to BadgerDB, safe unless disk failure
|
||||
3. **QoS 2**: Two-phase commit, safe unless both nodes fail
|
||||
|
||||
**Guarantees**:
|
||||
|
||||
- Messages in transit during node failure: lost
|
||||
- Messages in offline queue: persisted to BadgerDB
|
||||
- Inflight messages: persisted, redelivered on reconnect
|
||||
@@ -743,6 +806,7 @@ The session takeover protocol is complete:
|
||||
**Scenario**: Node receives SIGTERM (e.g., during deployment).
|
||||
|
||||
**Process**:
|
||||
|
||||
1. **Drain Phase**: Stop accepting new connections, wait for active sessions to disconnect
|
||||
- Configurable drain timeout (default: 30s)
|
||||
- Sessions can gracefully close
|
||||
@@ -753,18 +817,21 @@ The session takeover protocol is complete:
|
||||
- Idempotent shutdown (safe to call multiple times)
|
||||
|
||||
**Impact**:
|
||||
|
||||
- Zero session loss in cluster mode
|
||||
- Connected clients experience brief reconnection
|
||||
- Persistent sessions preserved across shutdown
|
||||
- Offline messages retained in BadgerDB
|
||||
|
||||
**Configuration**:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
shutdown_timeout: 30s # Drain period
|
||||
shutdown_timeout: 30s # Drain period
|
||||
```
|
||||
|
||||
**Recovery**:
|
||||
|
||||
- Clients automatically reconnect to other nodes
|
||||
- Session ownership acquired by new node
|
||||
- Subscriptions and offline messages restored
|
||||
@@ -774,11 +841,13 @@ server:
|
||||
### vs. Shared Database
|
||||
|
||||
**Traditional Approach**:
|
||||
|
||||
- All nodes connect to PostgreSQL/MySQL
|
||||
- Sessions, subscriptions, messages in DB
|
||||
- Lock-based coordination
|
||||
|
||||
**Our Approach**:
|
||||
|
||||
- No shared database
|
||||
- etcd for coordination only
|
||||
- Local storage for session state
|
||||
@@ -795,11 +864,13 @@ server:
|
||||
### vs. Message Queue (Kafka/NATS)
|
||||
|
||||
**Traditional Approach**:
|
||||
|
||||
- Brokers publish to message queue
|
||||
- All brokers subscribe to queue
|
||||
- Queue handles message routing
|
||||
|
||||
**Our Approach**:
|
||||
|
||||
- Direct gRPC between brokers
|
||||
- No intermediate queue
|
||||
- Targeted delivery only
|
||||
@@ -816,11 +887,13 @@ server:
|
||||
### vs. Gossip Protocol (Consul/Memberlist)
|
||||
|
||||
**Traditional Approach**:
|
||||
|
||||
- Eventual consistency
|
||||
- Broadcast state updates
|
||||
- No leader
|
||||
|
||||
**Our Approach**:
|
||||
|
||||
- Strong consistency (etcd Raft)
|
||||
- Direct targeted communication
|
||||
- Leader election
|
||||
@@ -839,27 +912,32 @@ server:
|
||||
The clustering architecture balances several concerns:
|
||||
|
||||
**Embedded First**: All components run in single process
|
||||
|
||||
- etcd server (not client)
|
||||
- gRPC server
|
||||
- BadgerDB storage
|
||||
|
||||
**Hybrid Consistency**:
|
||||
|
||||
- Strong (etcd): Session ownership, subscriptions
|
||||
- Direct (gRPC): Message routing
|
||||
- Local (BadgerDB): Session state
|
||||
|
||||
**MQTT Compliance**:
|
||||
|
||||
- Session takeover semantics
|
||||
- QoS guarantees
|
||||
- Retained message delivery
|
||||
- Will message processing
|
||||
|
||||
**Scalability**:
|
||||
|
||||
- Linear capacity scaling
|
||||
- No shared resources
|
||||
- Targeted message routing
|
||||
|
||||
**Operational Simplicity**:
|
||||
|
||||
- Single binary deployment
|
||||
- Automatic coordination
|
||||
- No external dependencies
|
||||
@@ -867,6 +945,7 @@ The clustering architecture balances several concerns:
|
||||
---
|
||||
|
||||
For related documentation, see:
|
||||
|
||||
- [Configuration Guide](configuration.md) - Cluster setup and tuning
|
||||
- [Broker & Routing](broker.md) - Message routing internals
|
||||
- [Architecture Overview](architecture.md) - Overall broker architecture
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Competitive Analysis
|
||||
description: Comprehensive comparison of FluxMQ against industry solutions including EMQX, HiveMQ, Mosquitto, NATS, RabbitMQ, and Kafka
|
||||
---
|
||||
|
||||
# Competitive Analysis: Absmach MQTT vs Industry Solutions
|
||||
|
||||
**Last Updated:** 2026-02-03
|
||||
@@ -8,40 +13,40 @@ This document provides a comprehensive comparison of the Absmach MQTT broker aga
|
||||
|
||||
## Feature Matrix
|
||||
|
||||
| Feature | Absmach MQTT | EMQX | HiveMQ | Mosquitto | NATS | RabbitMQ | Kafka |
|
||||
| ------------------------ | ---------------- | ----------- | ------ | ---------- | ------------- | ---------- | --------------- |
|
||||
| Feature | Absmach MQTT | EMQX | HiveMQ | Mosquitto | NATS | RabbitMQ | Kafka |
|
||||
| ------------------------ | ----------------------------------- | ------------ | ------ | ----------- | -------------- | ----------- | ---------------- |
|
||||
| **Protocol Support** |
|
||||
| MQTT 3.1/3.1.1 | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ (plugin) | ❌ |
|
||||
| MQTT 5.0 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| WebSocket | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| CoAP | ✅ | ✅ (plugin) | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| HTTP Bridge | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| MQTT 3.1/3.1.1 | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ (plugin) | ❌ |
|
||||
| MQTT 5.0 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| WebSocket | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| CoAP | ✅ | ✅ (plugin) | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| HTTP Bridge | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| **Clustering** |
|
||||
| Native Clustering | ✅ (etcd) | ✅ (Mria) | ✅ | ❌ | ✅ | ✅ | ✅ (ZK/KRaft) |
|
||||
| No External Dependencies | ✅ | ❌ | ✅ | N/A | ✅ | ❌ | ❌ (until KRaft) |
|
||||
| Session Migration | ✅ | ✅ | ✅ | N/A | ✅ | ❌ | N/A |
|
||||
| Native Clustering | ✅ (etcd) | ✅ (Mria) | ✅ | ❌ | ✅ | ✅ | ✅ (ZK/KRaft) |
|
||||
| No External Dependencies | ✅ | ❌ | ✅ | N/A | ✅ | ❌ | ❌ (until KRaft) |
|
||||
| Session Migration | ✅ | ✅ | ✅ | N/A | ✅ | ❌ | N/A |
|
||||
| **Persistence** |
|
||||
| Message Persistence | ✅ (BadgerDB) | ✅ (RocksDB) | ✅ | ✅ (SQLite) | ✅ (JetStream) | ✅ | ✅ |
|
||||
| Queue Replication | ⚠️ Append-only Raft (WIP) | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| Retention Policies | ⚠️ Committed-offset truncation only | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| Message Persistence | ✅ (BadgerDB) | ✅ (RocksDB) | ✅ | ✅ (SQLite) | ✅ (JetStream) | ✅ | ✅ |
|
||||
| Queue Replication | ⚠️ Append-only Raft (WIP) | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| Retention Policies | ⚠️ Committed-offset truncation only | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| **Performance** |
|
||||
| Zero-Copy | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ |
|
||||
| Buffer Pooling | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ |
|
||||
| Connections/Node | ~500K | ~5M | ~2M | ~100K | ~10M | ~1M | N/A |
|
||||
| Zero-Copy | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ |
|
||||
| Buffer Pooling | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ |
|
||||
| Connections/Node | ~500K | ~5M | ~2M | ~100K | ~10M | ~1M | N/A |
|
||||
| **Security** |
|
||||
| TLS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| mTLS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Auth Plugins | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Rate Limiting | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| RBAC | ⚠️ Interface only | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| TLS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| mTLS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Auth Plugins | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Rate Limiting | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| RBAC | ⚠️ Interface only | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Observability** |
|
||||
| Prometheus Metrics | ❌ (OTLP only) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Distributed Tracing | ⚠️ Stub only | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| Dashboard | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |
|
||||
| Prometheus Metrics | ❌ (OTLP only) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Distributed Tracing | ⚠️ Stub only | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
|
||||
| Dashboard | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ |
|
||||
| **Operations** |
|
||||
| Hot Config Reload | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| REST API | ⚠️ Basic | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Rolling Upgrades | ⚠️ Manual | ✅ | ✅ | N/A | ✅ | ✅ | ✅ |
|
||||
| Hot Config Reload | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| REST API | ⚠️ Basic | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Rolling Upgrades | ⚠️ Manual | ✅ | ✅ | N/A | ✅ | ✅ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
@@ -131,9 +136,9 @@ This document provides a comprehensive comparison of the Absmach MQTT broker aga
|
||||
| License | Apache 2.0 | Apache 2.0 (Enterprise: Commercial) |
|
||||
| Clustering | Embedded etcd | Mria (Mnesia extension) |
|
||||
| Max Connections | ~500K/node | ~5M/node |
|
||||
| Rule Engine | ❌ | ✅ Full SQL-like rules |
|
||||
| Rule Engine | ❌ | ✅ Full SQL-like rules |
|
||||
| Data Integration | Webhooks only | 40+ connectors |
|
||||
| Dashboard | ❌ | ✅ Full-featured |
|
||||
| Dashboard | ❌ | ✅ Full-featured |
|
||||
|
||||
**When to choose Absmach MQTT:** Simpler deployment, Go ecosystem, no external dependencies needed.
|
||||
|
||||
@@ -155,13 +160,13 @@ This document provides a comprehensive comparison of the Absmach MQTT broker aga
|
||||
|
||||
### vs Mosquitto
|
||||
|
||||
| Aspect | Absmach MQTT | Mosquitto |
|
||||
| ----------- | ----------------- | ------------- |
|
||||
| Language | Go | C |
|
||||
| Clustering | ✅ Native | ❌ Bridge only |
|
||||
| Persistence | BadgerDB | SQLite/Files |
|
||||
| Performance | Higher throughput | Lower latency |
|
||||
| Memory | Higher baseline | Very low |
|
||||
| Aspect | Absmach MQTT | Mosquitto |
|
||||
| ----------- | ----------------- | -------------- |
|
||||
| Language | Go | C |
|
||||
| Clustering | ✅ Native | ❌ Bridge only |
|
||||
| Persistence | BadgerDB | SQLite/Files |
|
||||
| Performance | Higher throughput | Lower latency |
|
||||
| Memory | Higher baseline | Very low |
|
||||
|
||||
**When to choose Absmach MQTT:** Clustering required, higher throughput needs.
|
||||
|
||||
@@ -195,13 +200,13 @@ This document provides a comprehensive comparison of the Absmach MQTT broker aga
|
||||
|
||||
### vs Kafka
|
||||
|
||||
| Aspect | Absmach MQTT | Kafka |
|
||||
| ------------- | ---------------- | --------------- |
|
||||
| Protocol | MQTT | Kafka Protocol |
|
||||
| Message Model | Pub/Sub + Queues | Log-based |
|
||||
| Ordering | Per-queue (single log) | Per-partition |
|
||||
| Aspect | Absmach MQTT | Kafka |
|
||||
| ------------- | ----------------------------------------------- | --------------- |
|
||||
| Protocol | MQTT | Kafka Protocol |
|
||||
| Message Model | Pub/Sub + Queues | Log-based |
|
||||
| Ordering | Per-queue (single log) | Per-partition |
|
||||
| Retention | Committed-offset truncation (time/size planned) | Log compaction |
|
||||
| Use Case | IoT/Real-time | Event streaming |
|
||||
| Use Case | IoT/Real-time | Event streaming |
|
||||
|
||||
**When to choose Absmach MQTT:** IoT devices, bidirectional communication, MQTT protocol.
|
||||
|
||||
@@ -211,14 +216,14 @@ This document provides a comprehensive comparison of the Absmach MQTT broker aga
|
||||
|
||||
## Production Readiness Assessment
|
||||
|
||||
| Aspect | Rating | Notes |
|
||||
| ------------------- | ------ | ---------------------------------------- |
|
||||
| Code Quality | ⭐⭐⭐⭐ | Clean architecture, low tech debt |
|
||||
| Protocol Compliance | ⭐⭐⭐ | Good MQTT 5.0, minor gaps |
|
||||
| Performance | ⭐⭐⭐⭐ | Excellent optimizations |
|
||||
| Security | ⭐⭐⭐ | Core fixes complete, auth plugins needed |
|
||||
| Scalability | ⭐⭐⭐ | Good to 5M clients, ceiling beyond |
|
||||
| Observability | ⭐⭐⭐ | Metrics good, tracing incomplete |
|
||||
| Aspect | Rating | Notes |
|
||||
| ------------------- | -------- | ---------------------------------------- |
|
||||
| Code Quality | ⭐⭐⭐⭐ | Clean architecture, low tech debt |
|
||||
| Protocol Compliance | ⭐⭐⭐ | Good MQTT 5.0, minor gaps |
|
||||
| Performance | ⭐⭐⭐⭐ | Excellent optimizations |
|
||||
| Security | ⭐⭐⭐ | Core fixes complete, auth plugins needed |
|
||||
| Scalability | ⭐⭐⭐ | Good to 5M clients, ceiling beyond |
|
||||
| Observability | ⭐⭐⭐ | Metrics good, tracing incomplete |
|
||||
| Operations | ⭐⭐ | Missing hot reload, rate limiting |
|
||||
|
||||
### Verdict: Conditionally Production Ready
|
||||
@@ -234,20 +239,24 @@ This document provides a comprehensive comparison of the Absmach MQTT broker aga
|
||||
See [docs/roadmap.md](roadmap.md) for the full implementation plan.
|
||||
|
||||
### Priority 1: Critical (Phase 0.1)
|
||||
|
||||
- ✅ Secure inter-broker TLS communication
|
||||
- ✅ WebSocket origin validation
|
||||
- Secure default ACL (deny-all)
|
||||
|
||||
### Priority 2: High (Phase 0.2)
|
||||
|
||||
- Rate limiting (connections, messages, subscriptions)
|
||||
|
||||
### Priority 3: Medium (Phase 0.3-0.4)
|
||||
|
||||
- Distributed tracing instrumentation
|
||||
- Prometheus metrics endpoint
|
||||
- MaxQoS enforcement
|
||||
- Complete shared subscriptions
|
||||
|
||||
### Priority 4: Enhancement (Phase 0.5-0.6)
|
||||
|
||||
- Management dashboard
|
||||
- Hot configuration reload
|
||||
- Graceful shutdown improvements
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Configuration Guide
|
||||
description: Comprehensive configuration reference for server transports, broker settings, clustering, storage, security, and operational features
|
||||
---
|
||||
|
||||
# Configuration Guide
|
||||
|
||||
This document provides a comprehensive guide to configuring the MQTT broker for single-node and clustered deployments.
|
||||
@@ -261,6 +266,7 @@ DTLS documentation (`dtls.CipherSuites()`).
|
||||
### TCP Configuration
|
||||
|
||||
**server.tcp.<mode>.addr**:
|
||||
|
||||
- Format: `"host:port"` or `":port"`
|
||||
- Examples:
|
||||
- `":1883"` - Listen on all interfaces, port 1883
|
||||
@@ -268,11 +274,13 @@ DTLS documentation (`dtls.CipherSuites()`).
|
||||
- `"192.168.1.10:1883"` - Listen on specific IP
|
||||
|
||||
**server.tcp.<mode>.max_connections**:
|
||||
|
||||
- Maximum concurrent TCP connections
|
||||
- Default: 10000
|
||||
- Adjust based on available file descriptors (`ulimit -n`)
|
||||
|
||||
**server.tcp.<mode>.read_timeout / server.tcp.<mode>.write_timeout**:
|
||||
|
||||
- Duration format: `"60s"`, `"5m"`, `"1h"`
|
||||
- Prevents hung connections
|
||||
- Should be longer than maximum expected keep-alive
|
||||
@@ -291,12 +299,14 @@ server:
|
||||
```
|
||||
|
||||
**Client Connection**:
|
||||
|
||||
```javascript
|
||||
// Browser client
|
||||
const client = mqtt.connect('ws://localhost:8083/mqtt');
|
||||
const client = mqtt.connect("ws://localhost:8083/mqtt");
|
||||
```
|
||||
|
||||
**Origin Validation**:
|
||||
|
||||
- `allowed_origins` empty: allow all origins (development mode, warns in logs)
|
||||
- `allowed_origins` set: only listed origins allowed
|
||||
- `"*"`: explicit wildcard for all origins
|
||||
@@ -312,6 +322,7 @@ server:
|
||||
```
|
||||
|
||||
**Publishing via HTTP**:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/publish \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -332,6 +343,7 @@ server:
|
||||
```
|
||||
|
||||
Endpoints:
|
||||
|
||||
- `GET /health` liveness
|
||||
- `GET /ready` readiness
|
||||
- `GET /cluster/status` cluster status (single node returns `cluster_mode=false`)
|
||||
@@ -353,6 +365,7 @@ server:
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `metrics_enabled` controls provider initialization.
|
||||
- `otel_metrics_enabled` and `otel_traces_enabled` control what is emitted.
|
||||
- `otel_trace_sample_rate` must be between 0.0 and 1.0.
|
||||
@@ -373,11 +386,11 @@ Controls broker behavior and message handling.
|
||||
|
||||
```yaml
|
||||
broker:
|
||||
max_message_size: 1048576 # 1MB max payload size
|
||||
max_retained_messages: 10000 # Max retained messages
|
||||
retry_interval: "20s" # QoS retry interval
|
||||
max_retries: 0 # 0 = infinite retries
|
||||
max_qos: 2 # Max supported QoS (0-2)
|
||||
max_message_size: 1048576 # 1MB max payload size
|
||||
max_retained_messages: 10000 # Max retained messages
|
||||
retry_interval: "20s" # QoS retry interval
|
||||
max_retries: 0 # 0 = infinite retries
|
||||
max_qos: 2 # Max supported QoS (0-2)
|
||||
```
|
||||
|
||||
### max_message_size
|
||||
@@ -385,11 +398,13 @@ broker:
|
||||
Maximum MQTT payload size in bytes.
|
||||
|
||||
**Considerations**:
|
||||
|
||||
- Larger values use more memory
|
||||
- MQTT default is unlimited, but this protects against abuse
|
||||
- Should match client expectations
|
||||
|
||||
**Examples**:
|
||||
|
||||
- `1048576` (1MB) - IoT sensors
|
||||
- `10485760` (10MB) - File transfers
|
||||
- `104857600` (100MB) - Large data transfers
|
||||
@@ -399,11 +414,13 @@ Maximum MQTT payload size in bytes.
|
||||
Maximum number of retained messages stored.
|
||||
|
||||
**Behavior**:
|
||||
|
||||
- When limit reached, oldest retained messages evicted
|
||||
- 0 = unlimited (not recommended)
|
||||
- Applies per-node in clustered mode
|
||||
|
||||
**Recommendations**:
|
||||
|
||||
- Small deployments: 1000-10000
|
||||
- Large deployments: 100000+
|
||||
|
||||
@@ -414,6 +431,7 @@ Maximum number of retained messages stored.
|
||||
How often to retry unacknowledged QoS 1/2 messages.
|
||||
|
||||
**Considerations**:
|
||||
|
||||
- Too short: Network congestion
|
||||
- Too long: Delayed delivery
|
||||
- Typical: 10-30 seconds
|
||||
@@ -425,6 +443,7 @@ How often to retry unacknowledged QoS 1/2 messages.
|
||||
Maximum retry attempts before giving up.
|
||||
|
||||
**Values**:
|
||||
|
||||
- `0`: Infinite retries (recommended for reliability)
|
||||
- `N`: Give up after N attempts
|
||||
|
||||
@@ -441,11 +460,11 @@ Controls session lifecycle and queuing.
|
||||
|
||||
```yaml
|
||||
session:
|
||||
max_sessions: 10000 # Max concurrent sessions
|
||||
default_expiry_interval: 300 # 5 minutes default expiry
|
||||
max_offline_queue_size: 1000 # Max queued messages per session
|
||||
max_inflight_messages: 100 # Max inflight QoS 1/2 per session
|
||||
offline_queue_policy: "evict" # "evict" or "reject"
|
||||
max_sessions: 10000 # Max concurrent sessions
|
||||
default_expiry_interval: 300 # 5 minutes default expiry
|
||||
max_offline_queue_size: 1000 # Max queued messages per session
|
||||
max_inflight_messages: 100 # Max inflight QoS 1/2 per session
|
||||
offline_queue_policy: "evict" # "evict" or "reject"
|
||||
```
|
||||
|
||||
### max_sessions
|
||||
@@ -453,12 +472,14 @@ session:
|
||||
Maximum concurrent sessions the broker will manage.
|
||||
|
||||
**Calculation**:
|
||||
|
||||
```
|
||||
Memory per session ≈ 1-10KB (depending on activity)
|
||||
Max sessions = Available Memory / Memory per session
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
|
||||
- 1GB RAM → ~100,000 - 1,000,000 sessions
|
||||
- 4GB RAM → ~500,000 - 4,000,000 sessions
|
||||
|
||||
@@ -467,6 +488,7 @@ Max sessions = Available Memory / Memory per session
|
||||
Default session expiry for clients that don't specify.
|
||||
|
||||
**Values** (in seconds):
|
||||
|
||||
- `0`: Session expires immediately on disconnect
|
||||
- `300`: 5 minutes (good default)
|
||||
- `3600`: 1 hour
|
||||
@@ -479,11 +501,13 @@ Default session expiry for clients that don't specify.
|
||||
Maximum messages queued for disconnected client.
|
||||
|
||||
**Behavior**:
|
||||
|
||||
- When limit reached, behavior is controlled by `offline_queue_policy`
|
||||
- Prevents memory exhaustion from offline clients
|
||||
- Only applies to QoS > 0 messages
|
||||
|
||||
**Recommendations**:
|
||||
|
||||
- IoT sensors: 100-1000
|
||||
- Mobile apps: 1000-10000
|
||||
- Critical systems: 10000+
|
||||
@@ -493,10 +517,12 @@ Maximum messages queued for disconnected client.
|
||||
Maximum unacknowledged QoS 1/2 messages per session.
|
||||
|
||||
**MQTT Spec**:
|
||||
|
||||
- MQTT 3.1.1: Fixed at 65535
|
||||
- MQTT 5.0: Client specifies "Receive Maximum"
|
||||
|
||||
**Typical Values**:
|
||||
|
||||
- Conservative: 100
|
||||
- Standard: 1000
|
||||
- Aggressive: 10000
|
||||
@@ -506,6 +532,7 @@ Maximum unacknowledged QoS 1/2 messages per session.
|
||||
Controls what happens when the offline queue is full.
|
||||
|
||||
**Values**:
|
||||
|
||||
- `evict`: Drop oldest messages and enqueue new ones
|
||||
- `reject`: Reject new messages while preserving existing queue
|
||||
|
||||
@@ -523,6 +550,7 @@ queue_manager:
|
||||
How often stream consumer groups auto-commit offsets when auto-commit is enabled.
|
||||
|
||||
Notes:
|
||||
|
||||
- Default: `5s` (Kafka-like).
|
||||
- `0s` commits on every delivery batch (lowest latency, more write pressure).
|
||||
|
||||
@@ -541,9 +569,9 @@ queues:
|
||||
- name: "orders"
|
||||
topics:
|
||||
- "orders/#"
|
||||
- "$queue/orders/#" # allow explicit queue publish
|
||||
- "$queue/orders/#" # allow explicit queue publish
|
||||
reserved: false
|
||||
type: "stream" # classic|stream (optional)
|
||||
type: "stream" # classic|stream (optional)
|
||||
primary_group: "workers"
|
||||
retention:
|
||||
max_age: "168h"
|
||||
@@ -581,6 +609,7 @@ Marks a queue as system-reserved (cannot be deleted via management APIs).
|
||||
### type
|
||||
|
||||
Queue behavior mode:
|
||||
|
||||
- `classic` (default): work-queue semantics (acks drive committed offset)
|
||||
- `stream`: append-only log semantics with cursor-based consumption
|
||||
|
||||
@@ -591,6 +620,7 @@ Consumer group name used to compute delivery status for stream consumers.
|
||||
### retention
|
||||
|
||||
Retention policy for stream-style access:
|
||||
|
||||
- `max_age`: time-based retention window
|
||||
- `max_length_bytes`: size-based retention window
|
||||
- `max_length_messages`: message-count window
|
||||
@@ -610,8 +640,8 @@ Selects the storage backend for persistence.
|
||||
|
||||
```yaml
|
||||
storage:
|
||||
type: "badger" # "memory" or "badger"
|
||||
badger_dir: "/tmp/fluxmq/data" # BadgerDB directory
|
||||
type: "badger" # "memory" or "badger"
|
||||
badger_dir: "/tmp/fluxmq/data" # BadgerDB directory
|
||||
```
|
||||
|
||||
### Memory Storage
|
||||
@@ -622,12 +652,14 @@ storage:
|
||||
```
|
||||
|
||||
**Characteristics**:
|
||||
|
||||
- All data in RAM
|
||||
- Fastest performance
|
||||
- No persistence
|
||||
- Lost on restart
|
||||
|
||||
**Use Cases**:
|
||||
|
||||
- Development/testing
|
||||
- Ephemeral deployments
|
||||
- Cache-only scenarios
|
||||
@@ -641,12 +673,14 @@ storage:
|
||||
```
|
||||
|
||||
**Characteristics**:
|
||||
|
||||
- LSM tree embedded database
|
||||
- Persists to disk
|
||||
- Fast writes, good reads
|
||||
- Automatic compaction
|
||||
|
||||
**Directory Structure**:
|
||||
|
||||
```
|
||||
/var/lib/mqtt/data/
|
||||
├── 000000.vlog # Value log
|
||||
@@ -656,10 +690,12 @@ storage:
|
||||
```
|
||||
|
||||
**Disk Space**:
|
||||
|
||||
- Compaction ratio: ~1.5-3x raw data
|
||||
- Reserve 2-5x expected data size
|
||||
|
||||
**Permissions**:
|
||||
|
||||
```bash
|
||||
mkdir -p /var/lib/mqtt/data
|
||||
chown mqtt:mqtt /var/lib/mqtt/data
|
||||
@@ -672,20 +708,20 @@ Enables distributed broker clustering.
|
||||
|
||||
```yaml
|
||||
cluster:
|
||||
enabled: true # Enable clustering
|
||||
node_id: "node1" # Unique node identifier
|
||||
enabled: true # Enable clustering
|
||||
node_id: "node1" # Unique node identifier
|
||||
|
||||
etcd:
|
||||
data_dir: "/tmp/fluxmq/node1/etcd" # etcd data directory
|
||||
bind_addr: "127.0.0.1:2380" # etcd peer address
|
||||
client_addr: "127.0.0.1:2379" # etcd client address
|
||||
data_dir: "/tmp/fluxmq/node1/etcd" # etcd data directory
|
||||
bind_addr: "127.0.0.1:2380" # etcd peer address
|
||||
client_addr: "127.0.0.1:2379" # etcd client address
|
||||
initial_cluster: "node1=http://127.0.0.1:2380,node2=http://127.0.0.1:2480,node3=http://127.0.0.1:2580"
|
||||
bootstrap: true # Bootstrap new cluster
|
||||
bootstrap: true # Bootstrap new cluster
|
||||
hybrid_retained_size_threshold: 1024 # Bytes; smaller retained messages replicated via etcd
|
||||
|
||||
transport:
|
||||
bind_addr: "127.0.0.1:7948" # gRPC listen address
|
||||
peers: # Peer node addresses
|
||||
bind_addr: "127.0.0.1:7948" # gRPC listen address
|
||||
peers: # Peer node addresses
|
||||
node2: "127.0.0.1:7949"
|
||||
node3: "127.0.0.1:7950"
|
||||
tls_enabled: false
|
||||
@@ -715,6 +751,7 @@ cluster:
|
||||
Enable or disable clustering.
|
||||
|
||||
**Values**:
|
||||
|
||||
- `false`: Single-node mode
|
||||
- `true`: Cluster mode (default in `config.Default()`)
|
||||
|
||||
@@ -723,11 +760,13 @@ Enable or disable clustering.
|
||||
Unique identifier for this broker node.
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- Must be unique across all nodes
|
||||
- Used in etcd cluster configuration
|
||||
- Cannot be changed after initialization
|
||||
|
||||
**Naming Convention**:
|
||||
|
||||
- `node1`, `node2`, `node3`, ...
|
||||
- `broker-us-east-1a`, `broker-us-east-1b`, ...
|
||||
- `mqtt-01`, `mqtt-02`, ...
|
||||
@@ -739,11 +778,13 @@ Unique identifier for this broker node.
|
||||
Directory for etcd's persistent data.
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- Must be writable
|
||||
- Should be on fast disk (SSD recommended)
|
||||
- Backed up regularly
|
||||
|
||||
**Size Estimation**:
|
||||
|
||||
```
|
||||
Subscriptions: ~100 bytes each
|
||||
Sessions: ~200 bytes each
|
||||
@@ -766,6 +807,7 @@ For 10,000 sessions, 50,000 subscriptions:
|
||||
| node3 | 127.0.0.1:2580 | 127.0.0.1:2579 |
|
||||
|
||||
**Production** (different hosts):
|
||||
|
||||
```yaml
|
||||
# node1 (192.168.1.10)
|
||||
bind_addr: "192.168.1.10:2380"
|
||||
@@ -783,11 +825,13 @@ Comma-separated list of all nodes in initial cluster.
|
||||
**Format**: `name=peer_url,name=peer_url,...`
|
||||
|
||||
**Example**:
|
||||
|
||||
```yaml
|
||||
initial_cluster: "node1=http://192.168.1.10:2380,node2=http://192.168.1.11:2380,node3=http://192.168.1.12:2380"
|
||||
```
|
||||
|
||||
**Important**:
|
||||
|
||||
- All nodes must have identical `initial_cluster` value
|
||||
- Use HTTP (HTTPS not yet supported)
|
||||
- URLs must match `bind_addr` values
|
||||
@@ -797,10 +841,12 @@ initial_cluster: "node1=http://192.168.1.10:2380,node2=http://192.168.1.11:2380,
|
||||
Whether this node is part of initial cluster formation.
|
||||
|
||||
**Values**:
|
||||
|
||||
- `true`: Node participates in new cluster formation
|
||||
- `false`: Node joins existing cluster (dynamic membership)
|
||||
|
||||
**Initial Cluster** (all nodes start together):
|
||||
|
||||
```yaml
|
||||
# All nodes set bootstrap: true
|
||||
node1: bootstrap: true
|
||||
@@ -809,6 +855,7 @@ node3: bootstrap: true
|
||||
```
|
||||
|
||||
**Add Node to Running Cluster** (advanced):
|
||||
|
||||
```yaml
|
||||
# Existing nodes: bootstrap: true
|
||||
# New node: bootstrap: false
|
||||
@@ -820,6 +867,7 @@ node3: bootstrap: true
|
||||
#### hybrid_retained_size_threshold
|
||||
|
||||
Threshold in bytes for hybrid retained storage:
|
||||
|
||||
- Messages smaller than this are replicated via etcd.
|
||||
- Larger messages are stored on the owner node and fetched on-demand.
|
||||
|
||||
@@ -832,6 +880,7 @@ gRPC server listen address for inter-broker communication.
|
||||
**Format**: `"host:port"`
|
||||
|
||||
**Examples**:
|
||||
|
||||
```yaml
|
||||
# Local testing
|
||||
bind_addr: "127.0.0.1:7948"
|
||||
@@ -846,6 +895,7 @@ bind_addr: "192.168.1.10:7948" # Listen on specific IP
|
||||
Map of peer node IDs to their transport addresses.
|
||||
|
||||
**Format**:
|
||||
|
||||
```yaml
|
||||
peers:
|
||||
nodeID: "host:port"
|
||||
@@ -853,6 +903,7 @@ peers:
|
||||
```
|
||||
|
||||
**Example**:
|
||||
|
||||
```yaml
|
||||
# node1 configuration
|
||||
transport:
|
||||
@@ -870,6 +921,7 @@ transport:
|
||||
```
|
||||
|
||||
**Production** (different hosts):
|
||||
|
||||
```yaml
|
||||
# node1 (192.168.1.10)
|
||||
transport:
|
||||
@@ -905,7 +957,7 @@ raft:
|
||||
sync_mode: true
|
||||
min_in_sync_replicas: 2
|
||||
ack_timeout: "5s"
|
||||
write_policy: "forward" # local | reject | forward
|
||||
write_policy: "forward" # local | reject | forward
|
||||
distribution_mode: "replicate" # forward | replicate
|
||||
bind_addr: "127.0.0.1:7100"
|
||||
data_dir: "/tmp/fluxmq/raft"
|
||||
@@ -919,6 +971,7 @@ raft:
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `write_policy` controls how non-leader nodes handle queue writes when Raft is enabled.
|
||||
`local` keeps current behavior, `reject` returns a leader error, `forward` proxies to the leader.
|
||||
- `distribution_mode` controls how publishes reach consumers across nodes.
|
||||
@@ -933,20 +986,21 @@ ratelimit:
|
||||
enabled: false
|
||||
connection:
|
||||
enabled: true
|
||||
rate: 1.6667 # connections per second per IP (100/min)
|
||||
rate: 1.6667 # connections per second per IP (100/min)
|
||||
burst: 20
|
||||
cleanup_interval: "5m"
|
||||
message:
|
||||
enabled: true
|
||||
rate: 1000 # messages per second per client
|
||||
rate: 1000 # messages per second per client
|
||||
burst: 100
|
||||
subscribe:
|
||||
enabled: true
|
||||
rate: 100 # subscriptions per second per client
|
||||
rate: 100 # subscriptions per second per client
|
||||
burst: 10
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- Connection limits are enforced before MQTT handshake.
|
||||
- Message limits return MQTT 5 `QuotaExceeded` for QoS > 0 and drop QoS 0.
|
||||
- Subscribe limits return MQTT 5 `SubAckQuotaExceeded` or MQTT 3 `SubAckFailure`.
|
||||
@@ -987,6 +1041,7 @@ webhook:
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `include_payload` is accepted by config but payload inclusion is not yet wired in the notifier.
|
||||
- Supported endpoint `type` is `"http"` only.
|
||||
|
||||
@@ -996,8 +1051,8 @@ Controls logging output.
|
||||
|
||||
```yaml
|
||||
log:
|
||||
level: "info" # debug, info, warn, error
|
||||
format: "text" # text, json
|
||||
level: "info" # debug, info, warn, error
|
||||
format: "text" # text, json
|
||||
```
|
||||
|
||||
### level
|
||||
@@ -1005,12 +1060,14 @@ log:
|
||||
Log verbosity.
|
||||
|
||||
**Levels** (from most to least verbose):
|
||||
|
||||
- `debug`: Everything (connection details, packet traces)
|
||||
- `info`: Normal operations (connects, subscribes, publishes)
|
||||
- `warn`: Warnings (retries, non-critical errors)
|
||||
- `error`: Errors only
|
||||
|
||||
**Recommendations**:
|
||||
|
||||
- Development: `debug`
|
||||
- Production: `info` or `warn`
|
||||
- High-volume: `error`
|
||||
@@ -1020,17 +1077,25 @@ Log verbosity.
|
||||
Log output format.
|
||||
|
||||
**Options**:
|
||||
|
||||
- `text`: Human-readable
|
||||
|
||||
```
|
||||
time=2025-12-17T18:00:00Z level=INFO msg=v5_connect client_id=client1
|
||||
```
|
||||
|
||||
- `json`: Machine-parseable
|
||||
```json
|
||||
{"time":"2025-12-17T18:00:00Z","level":"INFO","msg":"v5_connect","client_id":"client1"}
|
||||
{
|
||||
"time": "2025-12-17T18:00:00Z",
|
||||
"level": "INFO",
|
||||
"msg": "v5_connect",
|
||||
"client_id": "client1"
|
||||
}
|
||||
```
|
||||
|
||||
**Recommendations**:
|
||||
|
||||
- Development: `text`
|
||||
- Production with log aggregation: `json`
|
||||
|
||||
@@ -1145,6 +1210,7 @@ log:
|
||||
### 3-Node Cluster (Local)
|
||||
|
||||
**Node 1**:
|
||||
|
||||
```yaml
|
||||
# examples/node1.yaml
|
||||
server:
|
||||
@@ -1195,6 +1261,7 @@ log:
|
||||
```
|
||||
|
||||
**Node 2**: Same as Node 1, with:
|
||||
|
||||
- `tcp.plain.addr: ":1884"`
|
||||
- `node_id: "node2"`
|
||||
- `badger_dir: "/tmp/fluxmq/node2/data"`
|
||||
@@ -1209,6 +1276,7 @@ log:
|
||||
### Production 3-Node Cluster
|
||||
|
||||
**Node 1** (192.168.1.10):
|
||||
|
||||
```yaml
|
||||
server:
|
||||
tcp:
|
||||
@@ -1263,6 +1331,7 @@ log:
|
||||
### Security
|
||||
|
||||
1. **Bind addresses**:
|
||||
|
||||
```yaml
|
||||
# Development: localhost only
|
||||
server:
|
||||
@@ -1290,17 +1359,19 @@ log:
|
||||
### Performance
|
||||
|
||||
1. **Connection limits**:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
tcp:
|
||||
plain:
|
||||
max_connections: 50000 # Based on ulimit
|
||||
max_connections: 50000 # Based on ulimit
|
||||
|
||||
session:
|
||||
max_sessions: 50000
|
||||
```
|
||||
|
||||
System:
|
||||
|
||||
```bash
|
||||
# Increase file descriptor limit
|
||||
ulimit -n 100000
|
||||
@@ -1311,11 +1382,12 @@ log:
|
||||
```
|
||||
|
||||
2. **Timeouts**:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
tcp:
|
||||
plain:
|
||||
read_timeout: "120s" # 2x max keep-alive
|
||||
read_timeout: "120s" # 2x max keep-alive
|
||||
write_timeout: "120s"
|
||||
```
|
||||
|
||||
@@ -1323,7 +1395,7 @@ log:
|
||||
```yaml
|
||||
storage:
|
||||
type: "badger"
|
||||
badger_dir: "/mnt/ssd/mqtt/data" # SSD for best performance
|
||||
badger_dir: "/mnt/ssd/mqtt/data" # SSD for best performance
|
||||
```
|
||||
|
||||
### High Availability
|
||||
@@ -1334,16 +1406,18 @@ log:
|
||||
- 7 nodes: Tolerates 3 failures
|
||||
|
||||
2. **Data directories**: Use separate disks for etcd and BadgerDB
|
||||
|
||||
```yaml
|
||||
storage:
|
||||
badger_dir: "/mnt/data/mqtt/badger"
|
||||
|
||||
cluster:
|
||||
etcd:
|
||||
data_dir: "/mnt/ssd/mqtt/etcd" # SSD for etcd
|
||||
data_dir: "/mnt/ssd/mqtt/etcd" # SSD for etcd
|
||||
```
|
||||
|
||||
3. **Backups**: Backup etcd data and BadgerDB regularly
|
||||
|
||||
```bash
|
||||
# etcd backup (via etcdctl)
|
||||
etcdctl snapshot save /backup/mqtt-etcd-$(date +%Y%m%d).db
|
||||
@@ -1355,10 +1429,11 @@ log:
|
||||
### Monitoring
|
||||
|
||||
1. **Logging**:
|
||||
|
||||
```yaml
|
||||
log:
|
||||
level: "info"
|
||||
format: "json" # For log aggregation
|
||||
format: "json" # For log aggregation
|
||||
```
|
||||
|
||||
2. **Metrics** (future):
|
||||
@@ -1370,20 +1445,24 @@ log:
|
||||
### Resource Planning
|
||||
|
||||
**CPU**:
|
||||
|
||||
- Single node: 2-4 cores
|
||||
- Cluster node: 4-8 cores (etcd + broker + gRPC)
|
||||
|
||||
**Memory**:
|
||||
|
||||
- Base: 512MB - 1GB
|
||||
- Per 1000 sessions: ~10-100MB
|
||||
- Per 10000 retained: ~10-100MB
|
||||
|
||||
**Disk**:
|
||||
|
||||
- etcd: 10-100GB (slow growth)
|
||||
- BadgerDB: Based on message volume
|
||||
- I/O: SSD recommended for production
|
||||
|
||||
**Network**:
|
||||
|
||||
- Client connections: Based on concurrent clients
|
||||
- Cluster: Low latency preferred (<10ms)
|
||||
- Bandwidth: Based on message throughput
|
||||
@@ -1400,6 +1479,7 @@ Key configuration areas:
|
||||
**Log**: Logging output
|
||||
|
||||
For more details, see:
|
||||
|
||||
- [Clustering](clustering.md) - Distributed broker design, etcd, gRPC, BadgerDB
|
||||
- [Broker & Message Routing](broker.md) - Message routing internals
|
||||
- [Architecture](architecture.md) - Detailed system design
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: FluxMQ Documentation
|
||||
description: Welcome to FluxMQ - a high-performance, multi-protocol message broker with MQTT, WebSocket, HTTP, and CoAP support
|
||||
---
|
||||
|
||||
# FluxMQ
|
||||
|
||||
A high-performance, multi-protocol message broker written in Go designed for scalability, extensibility, and protocol diversity. Supports MQTT 3.1.1 and 5.0 over TCP and WebSocket, plus HTTP-MQTT and CoAP bridges for IoT integration.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-Protocol Support** - MQTT 3.1.1/5.0, WebSocket, HTTP-MQTT Bridge, CoAP Bridge
|
||||
- **High Performance** - 300K-500K msg/s per node with zero-copy packet parsing
|
||||
- **Clustering** - Embedded etcd for distributed coordination, no external dependencies
|
||||
- **Durable Queues** - Consumer groups with ack/nack/reject semantics
|
||||
- **Security** - TLS/mTLS, rate limiting, authentication plugins
|
||||
- **Observability** - OpenTelemetry metrics, structured logging, webhook notifications
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Build & Run
|
||||
|
||||
```bash
|
||||
# Clone and build
|
||||
git clone https://github.com/absmach/fluxmq.git
|
||||
cd fluxmq
|
||||
make build
|
||||
|
||||
# Run single node
|
||||
./build/fluxmq
|
||||
|
||||
# Run with configuration
|
||||
./build/fluxmq --config config.yaml
|
||||
```
|
||||
|
||||
### Test with MQTT Client
|
||||
|
||||
```bash
|
||||
# Subscribe
|
||||
mosquitto_sub -p 1883 -t "test/#" -v
|
||||
|
||||
# Publish
|
||||
mosquitto_pub -p 1883 -t "test/hello" -m "Hello FluxMQ"
|
||||
```
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```yaml
|
||||
server:
|
||||
tcp:
|
||||
plain:
|
||||
addr: ":1883"
|
||||
websocket:
|
||||
plain:
|
||||
addr: ":8083"
|
||||
path: "/mqtt"
|
||||
http:
|
||||
plain:
|
||||
addr: ":8080"
|
||||
|
||||
broker:
|
||||
max_message_size: 1048576
|
||||
max_retained_messages: 10000
|
||||
|
||||
storage:
|
||||
type: badger
|
||||
badger_dir: "./data"
|
||||
|
||||
log:
|
||||
level: info
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
FluxMQ uses a clean layered architecture:
|
||||
|
||||
- **Transport Layer** - TCP, WebSocket, HTTP, CoAP servers
|
||||
- **Protocol Layer** - MQTT 3.1.1/5.0 packet handling
|
||||
- **Domain Layer** - Protocol-agnostic broker logic
|
||||
- **Storage Layer** - BadgerDB for persistence, etcd for clustering
|
||||
|
||||
## Documentation
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="Architecture"
|
||||
href="/docs/architecture"
|
||||
description="System design and component overview"
|
||||
/>
|
||||
<Card
|
||||
title="Configuration"
|
||||
href="/docs/configuration"
|
||||
description="Complete configuration reference"
|
||||
/>
|
||||
<Card
|
||||
title="Clustering"
|
||||
href="/docs/clustering"
|
||||
description="Distributed broker setup"
|
||||
/>
|
||||
<Card
|
||||
title="Client Libraries"
|
||||
href="/docs/client"
|
||||
description="Go MQTT and AMQP clients"
|
||||
/>
|
||||
<Card
|
||||
title="Durable Queues"
|
||||
href="/docs/queue"
|
||||
description="Queue system with consumer groups"
|
||||
/>
|
||||
<Card
|
||||
title="Webhooks"
|
||||
href="/docs/webhooks"
|
||||
description="Event notification system"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
| -------------------------- | ------------------------ |
|
||||
| **Concurrent Connections** | 500K+ per node |
|
||||
| **Message Throughput** | 300K-500K msg/s per node |
|
||||
| **Latency (local)** | \<10ms |
|
||||
| **Session Takeover** | \<100ms |
|
||||
|
||||
## Use Cases
|
||||
|
||||
**Event-Driven Architectures**
|
||||
|
||||
- Event backbone for microservices
|
||||
- CQRS systems with durable queues
|
||||
- Real-time event processing
|
||||
|
||||
**IoT & Real-Time Systems**
|
||||
|
||||
- Device communication via MQTT
|
||||
- Browser clients via WebSocket
|
||||
- Edge computing deployments
|
||||
|
||||
**High-Availability Systems**
|
||||
|
||||
- Clustered deployments (3-5 nodes)
|
||||
- Geographic distribution
|
||||
- Linear scaling
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **GitHub**: [github.com/absmach/fluxmq](https://github.com/absmach/fluxmq)
|
||||
- **Issues**: Report bugs and request features
|
||||
- **Discussions**: Community support and questions
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Durable Queues
|
||||
description: Shared queue system for MQTT and AMQP with consumer groups, acknowledgments, stream queues, and append-only log storage
|
||||
---
|
||||
|
||||
# Durable Queues (MQTT + AMQP)
|
||||
|
||||
> **Status**: Implemented (single-log queues, consumer groups; DLQ wiring pending)
|
||||
@@ -29,6 +34,7 @@ This document describes the durable queue system shared by the MQTT, AMQP 1.0, a
|
||||
## Overview
|
||||
|
||||
Queues provide durable, at-least-once delivery across protocols:
|
||||
|
||||
- Persistent append-only storage per queue
|
||||
- Consumer groups for load-balanced delivery
|
||||
- Acknowledgments (`$ack`, `$nack`, `$reject`)
|
||||
@@ -38,6 +44,7 @@ Queues provide durable, at-least-once delivery across protocols:
|
||||
- Stream queues (RabbitMQ-compatible) for event-log consumption with cursor offsets
|
||||
|
||||
Queues are integrated with MQTT and AMQP:
|
||||
|
||||
- MQTT uses `$queue/<name>/...` topics
|
||||
- AMQP brokers map queue operations to the same queue manager
|
||||
|
||||
@@ -88,21 +95,24 @@ Stream queues use RabbitMQ-compatible queue names and arguments:
|
||||
The `$queue/<name>` prefix remains supported for legacy queue clients.
|
||||
|
||||
**Acknowledgment requirements**:
|
||||
|
||||
- `message-id` and `group-id` must be provided in message properties.
|
||||
- For MQTT, these are MQTT v5 User Properties.
|
||||
- For MQTT v3, acknowledgments are not supported (no properties).
|
||||
|
||||
**Delivery properties**:
|
||||
Queue deliveries include properties:
|
||||
|
||||
- `message-id`
|
||||
- `group-id`
|
||||
- `queue`
|
||||
- `offset`
|
||||
|
||||
Stream deliveries also include:
|
||||
|
||||
- `x-stream-offset`
|
||||
- `x-stream-timestamp`
|
||||
- `x-work-acked` (true when this message is below the primary work group's committed offset; may lag by the auto-commit interval)
|
||||
- `x-work-acked` (true when this message is below the primary work group's committed offset; may lag by the auto-commit interval)
|
||||
- `x-work-committed-offset`
|
||||
- `x-work-group`
|
||||
|
||||
@@ -148,6 +158,7 @@ removed only when retention policies allow it, and only up to the safe
|
||||
truncation point for queue-mode consumers.
|
||||
|
||||
`x-stream-offset` supports:
|
||||
|
||||
- `first`
|
||||
- `last`
|
||||
- `next`
|
||||
@@ -155,6 +166,7 @@ truncation point for queue-mode consumers.
|
||||
- `timestamp=<unix-seconds|unix-millis>`
|
||||
|
||||
FluxMQ extensions for stream consumers:
|
||||
|
||||
- `x-work-acked` and `x-work-committed-offset` to report delivery status for the
|
||||
configured primary work group.
|
||||
- `x-work-group` to identify the group used for status.
|
||||
@@ -170,9 +182,9 @@ used only for delivery status reporting; it does not affect routing.
|
||||
|
||||
Consumer groups operate in one of two modes:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `queue` | Work queue: messages removed on ack, PEL tracking, redelivery on timeout |
|
||||
| Mode | Behavior |
|
||||
| -------- | ------------------------------------------------------------------------- |
|
||||
| `queue` | Work queue: messages removed on ack, PEL tracking, redelivery on timeout |
|
||||
| `stream` | Log consumption: messages stay in log, cursor-based, optional auto-commit |
|
||||
|
||||
**Mode is immutable.** Once a group is created with a mode, all consumers must use the same mode.
|
||||
@@ -182,6 +194,7 @@ Consumer groups operate in one of two modes:
|
||||
If a consumer joins a group with a different mode, the broker returns `ErrGroupModeMismatch`.
|
||||
|
||||
**Resolution:**
|
||||
|
||||
- Use a different consumer group name
|
||||
- Ensure all consumers use consistent subscription options
|
||||
- Delete and recreate the group (all consumers must disconnect first)
|
||||
@@ -209,6 +222,7 @@ _ = client.CommitOffset("events", "my-group", lastProcessedOffset)
|
||||
Use the same consumer group name in both calls.
|
||||
|
||||
With manual commit:
|
||||
|
||||
- Messages are delivered but committed offset doesn't advance automatically
|
||||
- On reconnect, delivery resumes from last committed offset
|
||||
- Use `CommitOffset()` to advance the committed position
|
||||
@@ -230,6 +244,7 @@ Queues are backed by an **append-only log** with segments and sparse indexes.
|
||||
Each queue has a single log (no partitions).
|
||||
|
||||
**Key properties**:
|
||||
|
||||
- Sequential writes for high throughput
|
||||
- Offset-based reads
|
||||
- Sparse offset and time indexes (`.idx`, `.tix`)
|
||||
@@ -262,6 +277,7 @@ The queue manager truncates logs to a **safe offset** that respects:
|
||||
- The queue’s retention policy (time/size/message-count)
|
||||
|
||||
This means:
|
||||
|
||||
- Queue-mode consumers never lose unacked data.
|
||||
- Stream consumers do not block truncation.
|
||||
- Retention keeps data available for event-log readers as configured.
|
||||
@@ -273,6 +289,7 @@ This means:
|
||||
### Package Structure
|
||||
|
||||
**Core packages**:
|
||||
|
||||
```
|
||||
queue/ - Queue manager + delivery loops
|
||||
queue/consumer/ - Consumer groups, PEL, metrics
|
||||
@@ -283,6 +300,7 @@ queue/raft/ - Optional Raft replication layer
|
||||
```
|
||||
|
||||
**Broker integration**:
|
||||
|
||||
```
|
||||
mqtt/broker/publish.go - Routes $queue/* topics and acks
|
||||
mqtt/broker/subscribe.go - Queue subscriptions + consumer groups
|
||||
@@ -332,6 +350,7 @@ queues:
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- If no queues are configured, a default reserved queue `mqtt` is created with topic `$queue/#`.
|
||||
- Auto-created queues are **ephemeral** and expire after the last consumer disconnects.
|
||||
- `queue_manager.auto_commit_interval` controls how often stream offsets are auto-committed.
|
||||
@@ -345,6 +364,7 @@ Notes:
|
||||
## Performance & Scalability
|
||||
|
||||
Current scaling model:
|
||||
|
||||
- Scale horizontally with more broker nodes.
|
||||
- Use multiple queues to spread load.
|
||||
- Increase consumer counts per group to improve parallelism.
|
||||
@@ -357,6 +377,7 @@ partitioned queues and per-queue retention policies.
|
||||
## Current Progress
|
||||
|
||||
Implemented:
|
||||
|
||||
- Append-only log storage with segment indexing
|
||||
- Consumer groups and PEL-based redelivery
|
||||
- Ack/Nack/Reject via `$ack`, `$nack`, `$reject`
|
||||
@@ -364,6 +385,7 @@ Implemented:
|
||||
- Optional Raft layer for queue storage (leader append only)
|
||||
|
||||
Raft notes:
|
||||
|
||||
- Only leader appends go through Raft.
|
||||
- Non-leader write behavior is configurable via `cluster.raft.write_policy` (`local`, `reject`, `forward`).
|
||||
- Ack/Nack/Reject and consumer state are not replicated via Raft yet.
|
||||
@@ -374,6 +396,7 @@ Raft notes:
|
||||
## Missing Features & Next Steps
|
||||
|
||||
Planned or in-progress:
|
||||
|
||||
- Queue partitioning and ordering modes
|
||||
- Time-based and size-based retention policies
|
||||
- Enforce queue limits (max depth, max message size) and message TTL expiry
|
||||
@@ -448,9 +471,11 @@ removes it from the pending list; automatic DLQ routing is planned.
|
||||
## Troubleshooting
|
||||
|
||||
**Queue acks fail with "message-id required"**:
|
||||
|
||||
- Ensure you are sending `message-id` and `group-id` as properties.
|
||||
- MQTT v5 is required for properties.
|
||||
|
||||
**Messages not delivered**:
|
||||
|
||||
- Confirm the queue is configured and topic bindings match.
|
||||
- Verify consumers are subscribed with the correct group.
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Development Roadmap
|
||||
description: Production-ready MQTT broker development roadmap covering queue architecture, security hardening, performance optimization, and clustering
|
||||
---
|
||||
|
||||
# MQTT Broker Development Roadmap
|
||||
|
||||
**Last Updated:** 2026-02-03
|
||||
@@ -41,6 +46,7 @@ truncation only). The detailed completion checklist previously listed here was
|
||||
removed to avoid stale status. See `docs/queue.md` for current implementation notes.
|
||||
|
||||
**Completed (2026-01-16):**
|
||||
|
||||
- ✅ **Rate Limiting - COMPLETE:**
|
||||
- ✅ Per-IP connection rate limiting (TCP, WebSocket)
|
||||
- ✅ Per-client message rate limiting (V3/V5 handlers)
|
||||
@@ -62,6 +68,7 @@ Remaining production hardening tasks:
|
||||
3. **Prometheus Endpoint (P2)** - Native `/metrics` endpoint
|
||||
|
||||
**Deferred: Phase 2.5 Observability & Migration**
|
||||
|
||||
- Will be implemented after Phase 0
|
||||
- Metrics, health checks, and migration tooling
|
||||
- Can be done in parallel with production usage
|
||||
@@ -83,12 +90,14 @@ Core log-based queue behavior exists in-tree; remaining work includes retention
|
||||
### Motivation
|
||||
|
||||
**Current Model Problems:**
|
||||
|
||||
- Each `$queue/a/b/c` is a separate queue with independent storage
|
||||
- Wildcard subscriptions (`$queue/tasks/+`) don't work - would create literal queue named `$queue/tasks/+`
|
||||
- Delete-on-ack requires random I/O and causes fragmentation
|
||||
- Partition assignment is sticky - slow consumer blocks partition
|
||||
|
||||
**New Model Benefits:**
|
||||
|
||||
- Wildcard queue subscriptions work naturally (single log, filter at read time)
|
||||
- Sequential append-only writes (high throughput)
|
||||
- Work stealing enables self-healing (claim from slow consumer's PEL)
|
||||
@@ -129,14 +138,14 @@ Consumer Group "workers" for $queue/tasks/#
|
||||
|
||||
#### Key Differences from Current Model
|
||||
|
||||
| Aspect | Current | New (Log-Based) |
|
||||
|--------|---------|-----------------|
|
||||
| Storage | Delete on ack | Append-only, truncate by retention |
|
||||
| Consumer state | Partition assignment (sticky) | Cursor + PEL per partition |
|
||||
| Delivery | Push to assigned consumer | Consumer claims from any partition |
|
||||
| Ack | Delete message | Advance committed offset, remove from PEL |
|
||||
| Failure handling | Redeliver after timeout | Work stealing (claim from other's PEL) |
|
||||
| Wildcards | Not supported | Supported via routing key filtering |
|
||||
| Aspect | Current | New (Log-Based) |
|
||||
| ---------------- | ----------------------------- | ----------------------------------------- |
|
||||
| Storage | Delete on ack | Append-only, truncate by retention |
|
||||
| Consumer state | Partition assignment (sticky) | Cursor + PEL per partition |
|
||||
| Delivery | Push to assigned consumer | Consumer claims from any partition |
|
||||
| Ack | Delete message | Advance committed offset, remove from PEL |
|
||||
| Failure handling | Redeliver after timeout | Work stealing (claim from other's PEL) |
|
||||
| Wildcards | Not supported | Supported via routing key filtering |
|
||||
|
||||
---
|
||||
|
||||
@@ -152,13 +161,13 @@ Publish to $queue/tasks/us/images → queue="$queue/tasks", routing_key="us/imag
|
||||
|
||||
**Subscription Patterns:**
|
||||
|
||||
| Pattern | Behavior |
|
||||
|---------|----------|
|
||||
| `$queue/tasks` | All messages (no filter) |
|
||||
| `$queue/tasks/#` | All messages (explicit wildcard) |
|
||||
| `$queue/tasks/+` | All messages (single-level wildcard) |
|
||||
| `$queue/tasks/images` | Filter: routing_key == "images" |
|
||||
| `$queue/tasks/+/png` | Filter: routing_key matches "*/png" |
|
||||
| Pattern | Behavior |
|
||||
| --------------------- | ------------------------------------ |
|
||||
| `$queue/tasks` | All messages (no filter) |
|
||||
| `$queue/tasks/#` | All messages (explicit wildcard) |
|
||||
| `$queue/tasks/+` | All messages (single-level wildcard) |
|
||||
| `$queue/tasks/images` | Filter: routing_key == "images" |
|
||||
| `$queue/tasks/+/png` | Filter: routing_key matches "\*/png" |
|
||||
|
||||
**Consumer Groups per Subscription Pattern:**
|
||||
|
||||
@@ -336,6 +345,7 @@ func (state *PartitionState) advanceCommitted() {
|
||||
**Goal:** Append-only log storage with offset-based access
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- [ ] Define `LogStore` interface (append, read by offset, truncate)
|
||||
- [ ] Implement memory-based log store (ring buffer with offset tracking)
|
||||
- [ ] Implement BadgerDB-based log store (offset as key prefix)
|
||||
@@ -370,6 +380,7 @@ type LogStore interface {
|
||||
**Goal:** Cursor-based consumer groups with PEL tracking
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- [ ] Define new `ConsumerGroup` struct with cursor and PEL per partition
|
||||
- [ ] Implement pattern-based subscription matching
|
||||
- [ ] Add routing key filter compilation (wildcard patterns)
|
||||
@@ -384,6 +395,7 @@ type LogStore interface {
|
||||
**Goal:** Automatic work redistribution from slow/dead consumers
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- [ ] Add visibility timeout configuration
|
||||
- [ ] Implement PEL scanning for timed-out entries
|
||||
- [ ] Implement claim transfer between consumers
|
||||
@@ -396,6 +408,7 @@ type LogStore interface {
|
||||
**Goal:** Support `$queue/tasks/+` and `$queue/tasks/#` patterns
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- [ ] Parse queue subscription patterns (extract queue name + filter)
|
||||
- [ ] Create consumer group per unique (queue, pattern) combination
|
||||
- [ ] Implement routing key extraction from publish topic
|
||||
@@ -408,6 +421,7 @@ type LogStore interface {
|
||||
**Goal:** Integrate new model with message delivery system
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- [ ] Update delivery workers to use claim-based model
|
||||
- [ ] Implement batch claiming for efficiency
|
||||
- [ ] Update ack/nack/reject handlers for new model
|
||||
@@ -420,6 +434,7 @@ type LogStore interface {
|
||||
**Goal:** Migrate existing queues and validate new model
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- [ ] Create migration path for existing queue data
|
||||
- [ ] Update all queue unit tests for new model
|
||||
- [ ] Add cursor/PEL persistence tests
|
||||
@@ -433,14 +448,14 @@ type LogStore interface {
|
||||
|
||||
### Success Criteria
|
||||
|
||||
| Criteria | Target |
|
||||
|----------|--------|
|
||||
| Criteria | Target |
|
||||
| --------------------------- | ----------------------------------------------------------------- |
|
||||
| Wildcard subscriptions work | `$queue/tasks/+` receives from `$queue/tasks/a`, `$queue/tasks/b` |
|
||||
| Work stealing functional | Pending messages claimed after visibility timeout |
|
||||
| No message loss | All messages delivered at least once |
|
||||
| Throughput improvement | ≥ current throughput (sequential I/O benefit) |
|
||||
| Recovery time | Cursor reload < 1 second |
|
||||
| PEL accuracy | No orphaned entries, no duplicate delivery |
|
||||
| Work stealing functional | Pending messages claimed after visibility timeout |
|
||||
| No message loss | All messages delivered at least once |
|
||||
| Throughput improvement | ≥ current throughput (sequential I/O benefit) |
|
||||
| Recovery time | Cursor reload < 1 second |
|
||||
| PEL accuracy | No orphaned entries, no duplicate delivery |
|
||||
|
||||
---
|
||||
|
||||
@@ -448,24 +463,24 @@ type LogStore interface {
|
||||
|
||||
```yaml
|
||||
queue:
|
||||
model: log # "log" (new) or "legacy" (current, deprecated)
|
||||
model: log # "log" (new) or "legacy" (current, deprecated)
|
||||
|
||||
# Log storage settings
|
||||
log:
|
||||
segment_size: 1073741824 # 1GB per segment file
|
||||
index_interval: 4096 # Index every N messages
|
||||
segment_size: 1073741824 # 1GB per segment file
|
||||
index_interval: 4096 # Index every N messages
|
||||
|
||||
# Consumer group settings
|
||||
consumer:
|
||||
visibility_timeout: 30s # Time before message eligible for stealing
|
||||
max_delivery_count: 5 # Max deliveries before DLQ
|
||||
cursor_checkpoint_interval: 1s # How often to persist cursors
|
||||
visibility_timeout: 30s # Time before message eligible for stealing
|
||||
max_delivery_count: 5 # Max deliveries before DLQ
|
||||
cursor_checkpoint_interval: 1s # How often to persist cursors
|
||||
|
||||
# Work stealing settings
|
||||
stealing:
|
||||
enabled: true
|
||||
scan_interval: 5s # How often to scan for stealable work
|
||||
batch_size: 10 # Max messages to steal per scan
|
||||
scan_interval: 5s # How often to scan for stealable work
|
||||
batch_size: 10 # Max messages to steal per scan
|
||||
|
||||
# Retention (unchanged from Phase 2.4)
|
||||
retention:
|
||||
@@ -477,26 +492,26 @@ queue:
|
||||
|
||||
### Risks and Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Breaking change for existing queues | Provide migration tool, support legacy mode |
|
||||
| PEL memory growth | Limit PEL size, oldest entries go to DLQ |
|
||||
| Work stealing contention | Randomize steal targets, backoff on conflict |
|
||||
| Complex Raft integration | Reuse existing FSM patterns, incremental changes |
|
||||
| Risk | Mitigation |
|
||||
| ----------------------------------- | ------------------------------------------------ |
|
||||
| Breaking change for existing queues | Provide migration tool, support legacy mode |
|
||||
| PEL memory growth | Limit PEL size, oldest entries go to DLQ |
|
||||
| Work stealing contention | Randomize steal targets, backoff on conflict |
|
||||
| Complex Raft integration | Reuse existing FSM patterns, incremental changes |
|
||||
|
||||
---
|
||||
|
||||
### Estimated Effort
|
||||
|
||||
| Sub-phase | Effort |
|
||||
|-----------|--------|
|
||||
| Q.1: Storage Layer | 1-2 weeks |
|
||||
| Q.2: Consumer Groups | 1-2 weeks |
|
||||
| Q.3: Work Stealing | 1 week |
|
||||
| Q.4: Wildcards | 1 week |
|
||||
| Q.5: Delivery Integration | 1-2 weeks |
|
||||
| Q.6: Migration & Testing | 1-2 weeks |
|
||||
| **Total** | **6-10 weeks** |
|
||||
| Sub-phase | Effort |
|
||||
| ------------------------- | -------------- |
|
||||
| Q.1: Storage Layer | 1-2 weeks |
|
||||
| Q.2: Consumer Groups | 1-2 weeks |
|
||||
| Q.3: Work Stealing | 1 week |
|
||||
| Q.4: Wildcards | 1 week |
|
||||
| Q.5: Delivery Integration | 1-2 weeks |
|
||||
| Q.6: Migration & Testing | 1-2 weeks |
|
||||
| **Total** | **6-10 weeks** |
|
||||
|
||||
---
|
||||
|
||||
@@ -514,12 +529,14 @@ This phase was identified through comprehensive code audit comparing against NAT
|
||||
**Priority:** P0 - Block production deployment
|
||||
|
||||
**0.1.1 Secure Inter-Broker Communication** ✅ COMPLETE
|
||||
|
||||
- **File:** `cluster/transport.go`
|
||||
- **Issue:** Uses `insecure.NewCredentials()` - cluster traffic is unencrypted
|
||||
- **Risk:** Man-in-the-middle attacks, data interception between nodes
|
||||
- **Fix:** Implemented mTLS for gRPC connections
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```yaml
|
||||
cluster:
|
||||
transport:
|
||||
@@ -530,6 +547,7 @@ cluster:
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- [x] Add cluster TLS configuration options (`config/config.go`)
|
||||
- [x] Load certificates for inter-broker mTLS auth (`cluster/transport.go`)
|
||||
- [x] Server uses `tls.RequireAndVerifyClientCert` for mutual TLS
|
||||
@@ -539,12 +557,14 @@ cluster:
|
||||
- [ ] Add cluster TLS validation tests (future enhancement)
|
||||
|
||||
**0.1.2 WebSocket Origin Validation** ✅ COMPLETE
|
||||
|
||||
- **File:** `server/websocket/server.go`
|
||||
- **Issue:** `CheckOrigin` always returns `true` - accepts all origins
|
||||
- **Risk:** Cross-Site WebSocket Hijacking (CSWSH), CSRF attacks
|
||||
- **Fix:** Implemented configurable origin allowlist
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```yaml
|
||||
server:
|
||||
websocket:
|
||||
@@ -552,10 +572,11 @@ server:
|
||||
allowed_origins:
|
||||
- "https://example.com"
|
||||
- "https://app.example.com"
|
||||
- "*.example.com" # Wildcard subdomain support
|
||||
- "*.example.com" # Wildcard subdomain support
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- [x] Add `websocket.*.allowed_origins` configuration option (`config/config.go`)
|
||||
- [x] Implement origin validation logic (`server/websocket/server.go`)
|
||||
- [x] Support exact match origins
|
||||
@@ -565,6 +586,7 @@ server:
|
||||
- [ ] Add WebSocket security tests (future enhancement)
|
||||
|
||||
**0.1.3 Secure Default ACL**
|
||||
|
||||
- **File:** `broker/auth.go`
|
||||
- **Issue:** Default allows all when no authorizer configured
|
||||
- **Risk:** Unauthorized access to all topics and operations
|
||||
@@ -582,6 +604,7 @@ server:
|
||||
**Status:** Completed 2026-01-16
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- ✅ `ratelimit/ratelimit.go` - IPRateLimiter, ClientRateLimiter, Manager (~280 lines)
|
||||
- ✅ Per-IP connection rate limiting in TCP server (`server/tcp/server.go`)
|
||||
- ✅ Per-IP connection rate limiting in WebSocket server (`server/websocket/server.go`)
|
||||
@@ -592,25 +615,27 @@ server:
|
||||
- ✅ 12 unit tests (`ratelimit/ratelimit_test.go`)
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```yaml
|
||||
ratelimit:
|
||||
enabled: true
|
||||
connection:
|
||||
enabled: true
|
||||
rate: 1.67 # ~100 connections per minute per IP
|
||||
rate: 1.67 # ~100 connections per minute per IP
|
||||
burst: 20
|
||||
cleanup_interval: 5m
|
||||
message:
|
||||
enabled: true
|
||||
rate: 1000 # messages per second per client
|
||||
rate: 1000 # messages per second per client
|
||||
burst: 100
|
||||
subscribe:
|
||||
enabled: true
|
||||
rate: 100 # subscriptions per second per client
|
||||
rate: 100 # subscriptions per second per client
|
||||
burst: 10
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- Connection rate limiting: Closes connection immediately if exceeded (before MQTT handshake)
|
||||
- Message rate limiting: Returns MQTT 5.0 `QuotaExceeded` (0x97) for QoS > 0, silently drops QoS 0
|
||||
- Subscribe rate limiting: Returns `SubAckQuotaExceeded` for V5, `SubAckFailure` for V3
|
||||
@@ -622,6 +647,7 @@ ratelimit:
|
||||
**Priority:** P2 - Required for production operations
|
||||
|
||||
**0.3.1 Distributed Tracing Instrumentation**
|
||||
|
||||
- **File:** `server/otel/tracer.go`
|
||||
- **Issue:** Tracer created but never used in message paths
|
||||
- **Risk:** No visibility into message flow, debugging blind spots
|
||||
@@ -632,6 +658,7 @@ ratelimit:
|
||||
- [ ] Add span attributes (client_id, topic, qos, etc.)
|
||||
|
||||
**0.3.2 Prometheus Metrics Endpoint**
|
||||
|
||||
- **Issue:** Only OTLP export, no native Prometheus format
|
||||
- **Risk:** Incompatible with most monitoring stacks
|
||||
- [ ] Add `/metrics` endpoint with Prometheus format
|
||||
@@ -645,10 +672,12 @@ ratelimit:
|
||||
**Priority:** P2 - Required for MQTT spec compliance
|
||||
|
||||
**0.4.1 MaxQoS Enforcement** ✅ COMPLETE
|
||||
|
||||
- **MQTT 5.0 Spec 3.2.2.1.4:** Server MUST announce MaxQoS in CONNACK
|
||||
- **MQTT 5.0 Spec 3.3.2-4:** Server MUST downgrade inbound publish QoS
|
||||
|
||||
**Implementation (2026-01-14):**
|
||||
|
||||
- ✅ Added `MaxQoS` field to Broker struct with getter/setter (`broker/broker.go`)
|
||||
- ✅ Added `max_qos` config option (`config/config.go`, default: 2)
|
||||
- ✅ CONNACK now includes MaxQoS property (`broker/v5_handler.go`)
|
||||
@@ -656,12 +685,14 @@ ratelimit:
|
||||
- ✅ 6 unit tests covering config, setter, and downgrade behavior (`broker/maxqos_test.go`)
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```yaml
|
||||
broker:
|
||||
max_qos: 2 # Maximum QoS level (0, 1, or 2)
|
||||
max_qos: 2 # Maximum QoS level (0, 1, or 2)
|
||||
```
|
||||
|
||||
**0.4.2 Shared Subscriptions** ✅ COMPLETE
|
||||
|
||||
- **Status:** Fully implemented with comprehensive tests
|
||||
- **Files:**
|
||||
- `topics/shared.go` - `$share/{group}/topic` parsing
|
||||
@@ -686,6 +717,7 @@ broker:
|
||||
**Goal:** Modern web UI for broker management and monitoring
|
||||
|
||||
**Dashboard Features:**
|
||||
|
||||
- **Overview Page**
|
||||
- Connection count (current, peak, 24h graph)
|
||||
- Message throughput (publish/subscribe rates)
|
||||
@@ -722,6 +754,7 @@ broker:
|
||||
- Rate limit configuration
|
||||
|
||||
**Technical Stack:**
|
||||
|
||||
- Backend: REST API in Go (extend existing `/api/` routes)
|
||||
- Frontend: React + TypeScript + Tailwind CSS
|
||||
- Charts: Recharts or Chart.js
|
||||
@@ -729,6 +762,7 @@ broker:
|
||||
- Build: Embedded in binary via `go:embed`
|
||||
|
||||
**Implementation Tasks:**
|
||||
|
||||
- [ ] Create `dashboard/` directory for frontend code
|
||||
- [ ] Implement REST API endpoints for management operations
|
||||
- [ ] Build React frontend with modern UI
|
||||
@@ -742,6 +776,7 @@ broker:
|
||||
- [ ] Document dashboard usage
|
||||
|
||||
**API Endpoints:**
|
||||
|
||||
```
|
||||
GET /api/v1/overview - System overview metrics
|
||||
GET /api/v1/clients - List connected clients
|
||||
@@ -768,6 +803,7 @@ WS /api/v1/metrics/stream - Real-time metrics stream
|
||||
**Goal:** Enable configuration changes without broker restart
|
||||
|
||||
**Reloadable Configuration:**
|
||||
|
||||
- TLS certificates (rotation without connection drops)
|
||||
- Log level (debug/info/warn/error)
|
||||
- Rate limits (connections, messages, subscriptions)
|
||||
@@ -776,12 +812,14 @@ WS /api/v1/metrics/stream - Real-time metrics stream
|
||||
- Session expiry defaults
|
||||
|
||||
**Non-Reloadable (Requires Restart):**
|
||||
|
||||
- Listen addresses (TCP, WebSocket, CoAP, HTTP)
|
||||
- Storage backend type
|
||||
- Cluster node ID and etcd configuration
|
||||
- Maximum message size
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```go
|
||||
// Signal handler for SIGHUP
|
||||
func (b *Broker) setupSignalHandler() {
|
||||
@@ -801,6 +839,7 @@ func (b *Broker) setupSignalHandler() {
|
||||
```
|
||||
|
||||
**Tasks:**
|
||||
|
||||
- [ ] Add `ReloadConfig()` method to Broker
|
||||
- [ ] Implement SIGHUP handler in main.go
|
||||
- [ ] Add TLS certificate watcher for auto-rotation
|
||||
@@ -811,6 +850,7 @@ func (b *Broker) setupSignalHandler() {
|
||||
- [ ] Document which settings are hot-reloadable
|
||||
|
||||
**0.6.2 Graceful Shutdown**
|
||||
|
||||
- [ ] Drain connections before shutdown
|
||||
- [ ] Wait for inflight messages to complete
|
||||
- [ ] Transfer sessions to other nodes (clustered mode)
|
||||
@@ -820,22 +860,23 @@ func (b *Broker) setupSignalHandler() {
|
||||
|
||||
### Phase 0 Success Criteria
|
||||
|
||||
| Task | Priority | Status |
|
||||
|------|----------|--------|
|
||||
| Inter-broker TLS | P0 Critical | ✅ Complete |
|
||||
| WebSocket origin validation | P0 Critical | ✅ Complete |
|
||||
| Secure default ACL | P0 Critical | 📋 Planned |
|
||||
| Rate limiting | P1 High | ✅ Complete |
|
||||
| CoAP with DTLS/mDTLS | P1 High | ✅ Complete |
|
||||
| Distributed tracing | P2 Medium | 📋 Planned |
|
||||
| Prometheus endpoint | P2 Medium | 📋 Planned |
|
||||
| MaxQoS enforcement | P2 Medium | ✅ Complete |
|
||||
| Shared subscriptions | P2 Medium | ✅ Complete |
|
||||
| Management dashboard | P3 Enhancement | 📋 Planned |
|
||||
| Hot config reload | P3 Enhancement | 📋 Planned |
|
||||
| Graceful shutdown | P3 Enhancement | 📋 Planned |
|
||||
| Task | Priority | Status |
|
||||
| --------------------------- | -------------- | ----------- |
|
||||
| Inter-broker TLS | P0 Critical | ✅ Complete |
|
||||
| WebSocket origin validation | P0 Critical | ✅ Complete |
|
||||
| Secure default ACL | P0 Critical | 📋 Planned |
|
||||
| Rate limiting | P1 High | ✅ Complete |
|
||||
| CoAP with DTLS/mDTLS | P1 High | ✅ Complete |
|
||||
| Distributed tracing | P2 Medium | 📋 Planned |
|
||||
| Prometheus endpoint | P2 Medium | 📋 Planned |
|
||||
| MaxQoS enforcement | P2 Medium | ✅ Complete |
|
||||
| Shared subscriptions | P2 Medium | ✅ Complete |
|
||||
| Management dashboard | P3 Enhancement | 📋 Planned |
|
||||
| Hot config reload | P3 Enhancement | 📋 Planned |
|
||||
| Graceful shutdown | P3 Enhancement | 📋 Planned |
|
||||
|
||||
**Blocking Production:**
|
||||
|
||||
- P0 tasks MUST be complete before production deployment
|
||||
- P1 tasks SHOULD be complete before production deployment
|
||||
- P2/P3 tasks can be deployed incrementally
|
||||
@@ -848,11 +889,11 @@ func (b *Broker) setupSignalHandler() {
|
||||
|
||||
### Summary
|
||||
|
||||
| Metric | Before | After | Improvement |
|
||||
|--------|--------|-------|-------------|
|
||||
| Latency | 220 μs/op | 67 μs/op | 3.27x faster |
|
||||
| Memory | 424 KB/op | 177 B/op | 2,456x less |
|
||||
| Throughput | 4.6K msg/s | 14.9K msg/s | 3.24x more |
|
||||
| Metric | Before | After | Improvement |
|
||||
| ---------- | ---------- | ----------- | ------------ |
|
||||
| Latency | 220 μs/op | 67 μs/op | 3.27x faster |
|
||||
| Memory | 424 KB/op | 177 B/op | 2,456x less |
|
||||
| Throughput | 4.6K msg/s | 14.9K msg/s | 3.24x more |
|
||||
|
||||
**Key optimizations:** Object pooling for messages/packets, router match pooling, zero-copy buffers.
|
||||
|
||||
@@ -909,6 +950,7 @@ cluster:
|
||||
**Status:** Planned to start after Phase 2 completion
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Queue Raft benchmarks and failover tests in tree
|
||||
- Queue observability metrics in place
|
||||
|
||||
@@ -993,38 +1035,38 @@ While the current `queue/storage/log` implementation is durable and feature-rich
|
||||
|
||||
### 3.5.1: Implement Segment Write Buffer
|
||||
|
||||
* **Goal:** Reduce `write` syscalls to improve I/O efficiency.
|
||||
* **Tasks:**
|
||||
* [ ] Activate and utilize the `writeBuf` in the `Segment` struct.
|
||||
* [ ] Modify `Segment.Append` to write to the in-memory buffer.
|
||||
* [ ] Flush the buffer to disk when it's full or when `Sync` is called.
|
||||
* [ ] Add benchmarks to validate throughput improvement.
|
||||
- **Goal:** Reduce `write` syscalls to improve I/O efficiency.
|
||||
- **Tasks:**
|
||||
- [ ] Activate and utilize the `writeBuf` in the `Segment` struct.
|
||||
- [ ] Modify `Segment.Append` to write to the in-memory buffer.
|
||||
- [ ] Flush the buffer to disk when it's full or when `Sync` is called.
|
||||
- [ ] Add benchmarks to validate throughput improvement.
|
||||
|
||||
### 3.5.2: Improve `Store` Lock Granularity
|
||||
|
||||
* **Goal:** Eliminate the global lock on the main `Store` to allow concurrent operations on different queues.
|
||||
* **Tasks:**
|
||||
* [ ] Analyze trade-offs between `sync.Map` and a custom sharded map with per-shard locks.
|
||||
* [ ] Replace the single `RWMutex` in `store.go` with a sharded map (`[256]struct { sync.RWMutex; ... }`).
|
||||
* [ ] Update all access to `store.queues` and `store.consumers` to use the sharded lock mechanism.
|
||||
* [ ] Add concurrency tests to verify thread safety and measure contention reduction.
|
||||
- **Goal:** Eliminate the global lock on the main `Store` to allow concurrent operations on different queues.
|
||||
- **Tasks:**
|
||||
- [ ] Analyze trade-offs between `sync.Map` and a custom sharded map with per-shard locks.
|
||||
- [ ] Replace the single `RWMutex` in `store.go` with a sharded map (`[256]struct { sync.RWMutex; ... }`).
|
||||
- [ ] Update all access to `store.queues` and `store.consumers` to use the sharded lock mechanism.
|
||||
- [ ] Add concurrency tests to verify thread safety and measure contention reduction.
|
||||
|
||||
### 3.5.3: Decouple Appends from Disk I/O
|
||||
|
||||
* **Goal:** Drastically reduce append latency by making disk writes asynchronous from the client's perspective.
|
||||
* **Tasks:**
|
||||
* [ ] Introduce a buffered channel for append requests in `SegmentManager`.
|
||||
* [ ] Create a dedicated writer goroutine that reads from the channel and performs batched writes to the segment file.
|
||||
* [ ] Modify the public `Append` method to be a lightweight, non-blocking send to the channel.
|
||||
* [ ] Update latency benchmarks to measure p99 append latency reduction.
|
||||
- **Goal:** Drastically reduce append latency by making disk writes asynchronous from the client's perspective.
|
||||
- **Tasks:**
|
||||
- [ ] Introduce a buffered channel for append requests in `SegmentManager`.
|
||||
- [ ] Create a dedicated writer goroutine that reads from the channel and performs batched writes to the segment file.
|
||||
- [ ] Modify the public `Append` method to be a lightweight, non-blocking send to the channel.
|
||||
- [ ] Update latency benchmarks to measure p99 append latency reduction.
|
||||
|
||||
### 3.5.4: Implement Zero-Copy Writes
|
||||
|
||||
* **Goal:** Reduce memory allocations and GC pressure during high-throughput writes.
|
||||
* **Tasks:**
|
||||
* [ ] Introduce a `sync.Pool` for byte buffers used in batch encoding.
|
||||
* [ ] Modify `batch.Encode()` and `segment.Append()` to use pooled buffers.
|
||||
* [ ] Profile memory allocations under load to confirm reduction.
|
||||
- **Goal:** Reduce memory allocations and GC pressure during high-throughput writes.
|
||||
- **Tasks:**
|
||||
- [ ] Introduce a `sync.Pool` for byte buffers used in batch encoding.
|
||||
- [ ] Modify `batch.Encode()` and `segment.Append()` to use pooled buffers.
|
||||
- [ ] Profile memory allocations under load to confirm reduction.
|
||||
|
||||
---
|
||||
|
||||
@@ -1052,6 +1094,7 @@ Custom Raft implementation would replace etcd with a purpose-built coordination
|
||||
### Architecture
|
||||
|
||||
Use **hashicorp/raft** library + **BadgerDB** storage:
|
||||
|
||||
- Raft consensus library (battle-tested, used in Consul/Nomad/Vault)
|
||||
- BadgerDB for Raft log and snapshots
|
||||
- Custom FSM (Finite State Machine) for MQTT operations
|
||||
@@ -1060,12 +1103,14 @@ Use **hashicorp/raft** library + **BadgerDB** storage:
|
||||
### When to Consider Custom Raft
|
||||
|
||||
**DO NOT implement unless:**
|
||||
|
||||
- Single cluster exceeds 5-10M clients
|
||||
- etcd becomes proven bottleneck (profiling data required)
|
||||
- Write throughput consistently exceeds 5K writes/sec
|
||||
- Storage requirements exceed 8GB coordinated state
|
||||
|
||||
**Better alternatives first:**
|
||||
|
||||
- Geographic sharding (multiple 3-5 node clusters)
|
||||
- Topic-based sharding (95% local routing)
|
||||
- Hybrid storage optimization (already implemented)
|
||||
@@ -1088,6 +1133,7 @@ Use **hashicorp/raft** library + **BadgerDB** storage:
|
||||
### Cluster Design (3-5 Nodes Recommended)
|
||||
|
||||
**Components:**
|
||||
|
||||
- **Embedded etcd** - Distributed coordination (3-5 member cluster)
|
||||
- **BadgerDB** - Local persistent storage (500GB-1TB per node)
|
||||
- **gRPC Transport** - Inter-broker communication
|
||||
@@ -1095,6 +1141,7 @@ Use **hashicorp/raft** library + **BadgerDB** storage:
|
||||
- **Zero-Copy Buffers** - Reference-counted payload sharing
|
||||
|
||||
**Capacity (5-Node Cluster):**
|
||||
|
||||
- Concurrent connections: 250K-500K clients
|
||||
- Message throughput: 2-4M msgs/sec (with topic sharding)
|
||||
- Retained messages: 5M-10M messages
|
||||
@@ -1114,23 +1161,27 @@ Use **hashicorp/raft** library + **BadgerDB** storage:
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Target: >85% code coverage
|
||||
- All core packages (broker, router, queue, storage)
|
||||
- Fast execution (<1 minute total)
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Multi-node cluster scenarios
|
||||
- Session persistence and recovery
|
||||
- Cross-node messaging (all QoS levels)
|
||||
- Failure injection and recovery
|
||||
|
||||
### Performance Tests
|
||||
|
||||
- Benchmark suite: `benchmarks/`
|
||||
- CPU/memory profiling
|
||||
- Regression testing in CI
|
||||
- Load testing for capacity planning
|
||||
|
||||
### E2E Tests
|
||||
|
||||
- Real MQTT client integration
|
||||
- Network partition scenarios
|
||||
- Chaos engineering (node failures, network issues)
|
||||
@@ -1143,11 +1194,13 @@ Use **hashicorp/raft** library + **BadgerDB** storage:
|
||||
### Recommended Configuration
|
||||
|
||||
**3-Node Cluster (Most Common):**
|
||||
|
||||
- Connections: 150K-300K clients
|
||||
- Throughput: 1-2M msgs/sec (with topic sharding)
|
||||
- Cost: $1,200-1,500/month (cloud)
|
||||
|
||||
**5-Node Cluster (High Scale):**
|
||||
|
||||
- Connections: 250K-500K clients
|
||||
- Throughput: 2-4M msgs/sec (with topic sharding)
|
||||
- Cost: $2,000-2,500/month (cloud)
|
||||
@@ -1169,17 +1222,20 @@ Use **hashicorp/raft** library + **BadgerDB** storage:
|
||||
## Key Documentation
|
||||
|
||||
### Architecture
|
||||
|
||||
- [`docs/architecture.md`](architecture.md) - System design overview
|
||||
- [`docs/clustering.md`](clustering.md) - Cluster coordination
|
||||
- [`docs/queue.md`](queue.md) - Durable queue system
|
||||
- [`docs/broker.md`](broker.md) - Core broker implementation
|
||||
|
||||
### Performance & Scaling
|
||||
|
||||
- [`docs/scaling.md`](scaling.md) - Comprehensive scaling & performance guide
|
||||
- [`benchmarks/README.md`](../benchmarks/README.md) - Benchmark guide
|
||||
- [`docs/client.md`](client.md) - Go client library with queue support
|
||||
|
||||
### Operations
|
||||
|
||||
- [`docs/configuration.md`](configuration.md) - Configuration reference
|
||||
- [`docs/webhooks.md`](webhooks.md) - Event notification system
|
||||
|
||||
@@ -1188,6 +1244,7 @@ Use **hashicorp/raft** library + **BadgerDB** storage:
|
||||
## Overall Progress Summary
|
||||
|
||||
Status snapshot (2026-02-03):
|
||||
|
||||
- Phase Q: Log-based queues are in tree (consumer groups, PEL, work stealing, wildcard patterns). Remaining: retention wiring, DLQ routing, queue admin API, Raft state replication.
|
||||
- Phase 2: Raft manager exists; append-only replication on leader; consumer state not replicated; tests/benchmarks not in tree.
|
||||
- Phase 0.2 Rate limiting and CoAP DTLS/mDTLS are implemented (see sections above).
|
||||
@@ -1243,12 +1300,14 @@ When working on this roadmap:
|
||||
The queue system needs architectural redesign to support wildcard subscriptions and work stealing. This is now the top priority.
|
||||
|
||||
**Why Phase Q First:**
|
||||
|
||||
- Current model doesn't support wildcard queue subscriptions
|
||||
- Delete-on-ack model has performance limitations
|
||||
- Work stealing enables better fault tolerance
|
||||
- This is a foundational change that other features build upon
|
||||
|
||||
**Recommended Order within Phase Q:**
|
||||
|
||||
1. **Q.1: Storage Layer** - Foundation for everything else
|
||||
2. **Q.2: Consumer Groups** - Core cursor/PEL mechanics
|
||||
3. **Q.3: Work Stealing** - Fault tolerance
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Scaling & Performance
|
||||
description: Comprehensive scaling guide covering capacity analysis, performance optimizations, benchmarks, topic sharding, and architecture for 100M+ clients
|
||||
---
|
||||
|
||||
# Scaling & Performance
|
||||
|
||||
**Last Updated:** 2026-01-07
|
||||
@@ -27,11 +32,13 @@ This document consolidates all scaling, performance, and optimization informatio
|
||||
### Answer: Yes, with modifications (6 weeks effort)
|
||||
|
||||
**Current Bottleneck:**
|
||||
|
||||
- 🔴 Subscription storage exceeds etcd capacity
|
||||
- 10M clients × 10 subs = 100M subscriptions × 500B = 50GB
|
||||
- etcd limit: 8GB → Won't fit
|
||||
|
||||
**Solution: Gossip Protocol for Subscriptions**
|
||||
|
||||
- Store subscriptions in local BadgerDB (per node)
|
||||
- Propagate changes via gossip (hashicorp/memberlist)
|
||||
- Reduces etcd to 2GB (session ownership only)
|
||||
@@ -41,17 +48,17 @@ This document consolidates all scaling, performance, and optimization informatio
|
||||
|
||||
| Resource | Per Node | 20 Nodes | Status |
|
||||
| --------------- | -------- | --------------- | ------ |
|
||||
| **Connections** | 500K | 10M | ✅ OK |
|
||||
| **RAM** | 15GB | 300GB | ✅ OK |
|
||||
| **Disk** | 5GB | 100GB | ✅ OK |
|
||||
| **Throughput** | 8K msg/s | 160K-500K msg/s | ✅ OK |
|
||||
| **Connections** | 500K | 10M | ✅ OK |
|
||||
| **RAM** | 15GB | 300GB | ✅ OK |
|
||||
| **Disk** | 5GB | 100GB | ✅ OK |
|
||||
| **Throughput** | 8K msg/s | 160K-500K msg/s | ✅ OK |
|
||||
|
||||
### Architecture Comparison
|
||||
|
||||
| Approach | Effort | Scalability | Risk |
|
||||
| ----------------------- | -------- | ------------ | ---- |
|
||||
| **Keep etcd (current)** | 0 weeks | 5M clients | Low |
|
||||
| **etcd + Gossip** ⭐ | 6 weeks | 10M clients | Low |
|
||||
| **etcd + Gossip** ⭐ | 6 weeks | 10M clients | Low |
|
||||
| **Custom Raft** | 20 weeks | 50M+ clients | High |
|
||||
|
||||
⭐ = Recommended for 10M client requirement
|
||||
@@ -63,18 +70,21 @@ This document consolidates all scaling, performance, and optimization informatio
|
||||
### Deployment Tiers
|
||||
|
||||
#### Tier 1: Single Node (Development)
|
||||
|
||||
- **Hardware:** 16-32 cores, 64-128GB RAM, 1-2TB NVMe
|
||||
- **Connections:** 100K-500K (tuned)
|
||||
- **Throughput:** 300-500K msg/s
|
||||
- **Cost:** $500-1,000/month
|
||||
|
||||
#### Tier 2: 3-Node Cluster ⭐ RECOMMENDED
|
||||
|
||||
- **Hardware per node:** 8-16 cores, 32-64GB RAM, 500GB-1TB SSD
|
||||
- **Connections:** 300K-1.5M (with tuning)
|
||||
- **Throughput:** 1M-2M msg/s (with topic sharding)
|
||||
- **Cost:** $1,200-1,500/month
|
||||
|
||||
#### Tier 3: 5-Node Cluster (High Scale)
|
||||
|
||||
- **Connections:** 500K-2.5M (with tuning)
|
||||
- **Throughput:** 2M-4M msg/s (with topic sharding)
|
||||
- **Cost:** $2,000-2,500/month
|
||||
@@ -82,6 +92,7 @@ This document consolidates all scaling, performance, and optimization informatio
|
||||
### OS Tuning for High Connections
|
||||
|
||||
**Standard Tuning (100K-250K connections):**
|
||||
|
||||
```bash
|
||||
# /etc/sysctl.conf
|
||||
fs.file-max = 1000000
|
||||
@@ -93,6 +104,7 @@ ulimit -n 131072
|
||||
```
|
||||
|
||||
**Aggressive Tuning (250K-500K connections):**
|
||||
|
||||
```bash
|
||||
fs.file-max = 2097152
|
||||
fs.nr_open = 2097152
|
||||
@@ -110,11 +122,11 @@ ulimit -n 524288
|
||||
**Memory Requirements:**
|
||||
|
||||
| Connections | Memory (Conservative) | Memory (Realistic) |
|
||||
|-------------|----------------------|-------------------|
|
||||
| 100K | 800 MB | 600 MB |
|
||||
| 250K | 2 GB | 1.5 GB |
|
||||
| 500K | 4 GB | 3 GB |
|
||||
| 1M | 8 GB | 6 GB |
|
||||
| ----------- | --------------------- | ------------------ |
|
||||
| 100K | 800 MB | 600 MB |
|
||||
| 250K | 2 GB | 1.5 GB |
|
||||
| 500K | 4 GB | 3 GB |
|
||||
| 1M | 8 GB | 6 GB |
|
||||
|
||||
**Per-connection breakdown:** ~6-8KB total (TCP buffers 4KB + session state 2KB + Go runtime 0.5KB)
|
||||
|
||||
@@ -129,24 +141,28 @@ Comprehensive performance optimization achieved **3.3x performance improvement**
|
||||
#### Phase 1: Baseline & Profiling
|
||||
|
||||
**Critical discovery:** 75% of CPU spent in Garbage Collection
|
||||
|
||||
- 49.5 GB allocated in 88 seconds (560 MB/sec allocation rate)
|
||||
- 3,005 allocations per message to 1000 subscribers
|
||||
|
||||
#### Phase 2: Object Pooling (2.98x gain)
|
||||
|
||||
**Implemented pools for:**
|
||||
|
||||
1. `storage.Message` - Eliminated 26.28 GB allocations
|
||||
2. `v5.Publish` packets - Eliminated 12.89 GB allocations
|
||||
3. `v5.PublishProperties` - Eliminated 9.20 GB allocations
|
||||
4. `v3.Publish` packets - Eliminated allocations for v3 clients
|
||||
|
||||
**Results:**
|
||||
|
||||
- Latency: 220 μs → 74 μs (2.98x faster)
|
||||
- Memory: 424 KB → 8.4 KB (50.6x reduction)
|
||||
- Allocations: 3,005 → 5 (601x reduction)
|
||||
- GC time: 75% → 54%
|
||||
|
||||
**Implementation files:**
|
||||
|
||||
- `storage/pool.go` - Message pooling
|
||||
- `mqtt/packets/v5/pool.go` - v5 Publish pooling
|
||||
- `mqtt/packets/v3/pool.go` - v3 Publish pooling
|
||||
@@ -154,11 +170,13 @@ Comprehensive performance optimization achieved **3.3x performance improvement**
|
||||
#### Phase 3: Router Match Pooling (1.10x additional gain)
|
||||
|
||||
**Implemented pool for subscription slices:**
|
||||
|
||||
- Router.Match() uses pooled subscription slice
|
||||
- Pre-allocated capacity of 64 subscribers
|
||||
- Slice header reused across match operations
|
||||
|
||||
**Results:**
|
||||
|
||||
- Latency: 74 μs → 67 μs (1.10x faster)
|
||||
- Memory: 8.4 KB → 177 B (47x reduction)
|
||||
- Allocations: 5 → 4 (1 fewer)
|
||||
@@ -168,6 +186,7 @@ Comprehensive performance optimization achieved **3.3x performance improvement**
|
||||
#### Phase 4: Mutex Profiling & Analysis
|
||||
|
||||
**Findings:**
|
||||
|
||||
- Total mutex contention: 231ms over entire benchmark
|
||||
- 98.88% from test cleanup (disconnect/close), not publish path
|
||||
- Actual publish path shows minimal lock contention
|
||||
@@ -199,6 +218,7 @@ Pooled Objects:
|
||||
The zero-copy optimization using reference-counted buffers provides significant performance improvements:
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- Reference-counted buffers with pooling
|
||||
- Zero allocations for message payload sharing
|
||||
- Constant memory usage independent of subscriber count
|
||||
@@ -206,10 +226,10 @@ The zero-copy optimization using reference-counted buffers provides significant
|
||||
|
||||
**Memory Analysis:**
|
||||
|
||||
| Approach | Memory for 1000 subs × 64KB msg |
|
||||
|-----------------|--------------------------------|
|
||||
| Traditional | 64 MB per message |
|
||||
| Zero-Copy | 64 KB + 400 KB ≈ **0.5 MB** |
|
||||
| Approach | Memory for 1000 subs × 64KB msg |
|
||||
| ----------- | ------------------------------- |
|
||||
| Traditional | 64 MB per message |
|
||||
| Zero-Copy | 64 KB + 400 KB ≈ **0.5 MB** |
|
||||
|
||||
**Savings: ~128x reduction**
|
||||
|
||||
@@ -257,7 +277,7 @@ go test -bench=BenchmarkMessageCopy -benchmem ./mqtt/broker
|
||||
### Single Subscriber Performance
|
||||
|
||||
| Message Size | Time (ns/op) | Memory (B/op) | Allocs (op) |
|
||||
|--------------|--------------|---------------|-------------|
|
||||
| ------------ | ------------ | ------------- | ----------- |
|
||||
| 100 bytes | 539.1 | 600 | 8 |
|
||||
| 1 KB | 545.7 | 600 | 8 |
|
||||
| 10 KB | 654.0 | 600 | 8 |
|
||||
@@ -272,16 +292,17 @@ go test -bench=BenchmarkMessagePublish_SingleSubscriber -benchmem ./mqtt/broker
|
||||
### Multiple Subscribers Scalability
|
||||
|
||||
| Subscribers | Time (ns/op) | Time/Sub (ns) | Memory (B/op) | Allocs/op |
|
||||
|-------------|--------------|---------------|---------------|-----------|
|
||||
| ----------- | ------------ | ------------- | ------------- | --------- |
|
||||
| 1 | 568.6 | 568.6 | 600 | 8 |
|
||||
| 10 | 2,393 | 239.3 | 4,416 | 35 |
|
||||
| 100 | 21,997 | 220.0 | 42,672 | 305 |
|
||||
| 1,000 | 240,064 | 240.1 | 424,369 | 3,005 |
|
||||
|
||||
**Key insights:**
|
||||
|
||||
- Near-linear scalability: ~230-240 ns per subscriber
|
||||
- Memory per subscriber: ~424 bytes (mostly overhead, not message payload)
|
||||
- Zero-copy prevents O(N*MessageSize) memory usage
|
||||
- Zero-copy prevents O(N\*MessageSize) memory usage
|
||||
|
||||
```bash
|
||||
go test -bench=BenchmarkMessagePublish_MultipleSubscribers -benchmem ./mqtt/broker
|
||||
@@ -292,11 +313,11 @@ go test -bench=BenchmarkMessagePublish_MultipleSubscribers -benchmem ./mqtt/brok
|
||||
Publishing 256-byte messages (typical sensor data) to many subscribers:
|
||||
|
||||
| Subscribers | Time (ns/op) | Throughput (msg/s) | Memory (B/op) |
|
||||
|-------------|--------------|-------------------|---------------|
|
||||
| 10 | 2,346 | 426,000 | 4,416 |
|
||||
| 100 | 21,538 | 46,400 | 42,672 |
|
||||
| 500 | 115,021 | 8,700 | 212,272 |
|
||||
| 1,000 | 253,930 | 3,940 | 424,370 |
|
||||
| ----------- | ------------ | ------------------ | ------------- |
|
||||
| 10 | 2,346 | 426,000 | 4,416 |
|
||||
| 100 | 21,538 | 46,400 | 42,672 |
|
||||
| 500 | 115,021 | 8,700 | 212,272 |
|
||||
| 1,000 | 253,930 | 3,940 | 424,370 |
|
||||
|
||||
```bash
|
||||
go test -bench=BenchmarkMessagePublish_FanOut -benchmem ./mqtt/broker
|
||||
@@ -313,6 +334,7 @@ go test -bench=BenchmarkMessagePublish_FanOut -benchmem ./mqtt/broker
|
||||
| GetWithData 64KB | 34.2 | 0 | 0 |
|
||||
|
||||
**Key insights:**
|
||||
|
||||
- Extremely fast atomic reference counting (3.5 ns)
|
||||
- Pool provides consistent performance regardless of buffer size
|
||||
- Zero allocations when pool is warm
|
||||
@@ -325,12 +347,12 @@ go test -bench=BenchmarkRefCountedBuffer -benchmem ./mqtt
|
||||
|
||||
### Router Performance
|
||||
|
||||
| Operation | Time | Memory | Allocs |
|
||||
|---------------------|------------|----------|--------|
|
||||
| Match (10K subs) | 148 ns/op | 143 B/op | 5 |
|
||||
| Subscribe | 521 ns/op | 221 B/op | 5 |
|
||||
| Unsubscribe | 21.7 μs/op | 109 B/op | 4 |
|
||||
| Wildcard matching | 212 ns/op | 199 B/op | 6 |
|
||||
| Operation | Time | Memory | Allocs |
|
||||
| ----------------- | ---------- | -------- | ------ |
|
||||
| Match (10K subs) | 148 ns/op | 143 B/op | 5 |
|
||||
| Subscribe | 521 ns/op | 221 B/op | 5 |
|
||||
| Unsubscribe | 21.7 μs/op | 109 B/op | 4 |
|
||||
| Wildcard matching | 212 ns/op | 199 B/op | 6 |
|
||||
|
||||
📁 **Test file:** [broker/router/router_bench_test.go](broker/router/router_bench_test.go)
|
||||
|
||||
@@ -340,13 +362,13 @@ go test -bench=. -benchmem ./broker/router
|
||||
|
||||
### Stress Test Results
|
||||
|
||||
| Test | Config | Result |
|
||||
|------|--------|--------|
|
||||
| **Buffer Pool Exhaustion** | 100 concurrent goroutines, 1M ops | 8.4M ops/sec, 99.95% hit rate |
|
||||
| **High Throughput** | 10K msg/s for 10s to 100 subs | Stable, <1000 B/msg overhead |
|
||||
| **Concurrent Publishers** | 10 publishers × 10K msgs | Zero errors, linear scaling |
|
||||
| **Memory Pressure** | 64KB-1MB messages to 20 subs | <100MB total |
|
||||
| **Extreme Fan-Out** | 1K msgs to 5K subs | 5M deliveries, <100 B/delivery |
|
||||
| Test | Config | Result |
|
||||
| -------------------------- | --------------------------------- | ------------------------------ |
|
||||
| **Buffer Pool Exhaustion** | 100 concurrent goroutines, 1M ops | 8.4M ops/sec, 99.95% hit rate |
|
||||
| **High Throughput** | 10K msg/s for 10s to 100 subs | Stable, <1000 B/msg overhead |
|
||||
| **Concurrent Publishers** | 10 publishers × 10K msgs | Zero errors, linear scaling |
|
||||
| **Memory Pressure** | 64KB-1MB messages to 20 subs | <100MB total |
|
||||
| **Extreme Fan-Out** | 1K msgs to 5K subs | 5M deliveries, <100 B/delivery |
|
||||
|
||||
📁 **Test file:** [mqtt/broker/message_stress_test.go](mqtt/broker/message_stress_test.go)
|
||||
|
||||
@@ -390,12 +412,12 @@ failover tests for the queue Raft layer are not currently present in-tree.
|
||||
|
||||
### Performance Comparison
|
||||
|
||||
| Scenario | Throughput | Latency |
|
||||
|---------------------------------|----------------|---------------|
|
||||
| Single node | 500K msgs/sec | <100μs |
|
||||
| 20-node (no sharding) | 600K-1M msgs/s | 1-5ms |
|
||||
| Single shard (3 nodes) | 1-1.5M msgs/s | <100μs local |
|
||||
| 20-node (7 shards, 95% local) | **5-10M msgs/s** | <100μs local |
|
||||
| Scenario | Throughput | Latency |
|
||||
| ----------------------------- | ---------------- | ------------ |
|
||||
| Single node | 500K msgs/sec | <100μs |
|
||||
| 20-node (no sharding) | 600K-1M msgs/s | 1-5ms |
|
||||
| Single shard (3 nodes) | 1-1.5M msgs/s | <100μs local |
|
||||
| 20-node (7 shards, 95% local) | **5-10M msgs/s** | <100μs local |
|
||||
|
||||
### ROI Calculation
|
||||
|
||||
@@ -413,7 +435,7 @@ Total = 6.7 × 2.5 = 16.75x vs original baseline
|
||||
|
||||
### HAProxy Configuration
|
||||
|
||||
```haproxy
|
||||
```nginx
|
||||
global
|
||||
log /dev/log local0
|
||||
maxconn 50000
|
||||
@@ -426,16 +448,16 @@ defaults
|
||||
|
||||
frontend mqtt_frontend
|
||||
bind *:1883
|
||||
|
||||
|
||||
# Extract shard from ClientID prefix
|
||||
tcp-request inspect-delay 5s
|
||||
tcp-request content accept if { req.len gt 0 }
|
||||
|
||||
|
||||
# Route based on ClientID prefix
|
||||
use_backend mqtt_shard1 if { req.payload(0,100),lower,word(1,-) -m beg tenant1 }
|
||||
use_backend mqtt_shard2 if { req.payload(0,100),lower,word(1,-) -m beg tenant2 }
|
||||
use_backend mqtt_shard3 if { req.payload(0,100),lower,word(1,-) -m beg tenant3 }
|
||||
|
||||
|
||||
default_backend mqtt_cluster
|
||||
|
||||
backend mqtt_shard1
|
||||
@@ -456,18 +478,21 @@ backend mqtt_cluster
|
||||
### ClientID Naming Conventions
|
||||
|
||||
**Multi-Tenant SaaS:**
|
||||
|
||||
```
|
||||
Format: {tenant_id}-{app}-{device_id}
|
||||
Example: acme-corp-sensor-001
|
||||
```
|
||||
|
||||
**IoT Platform:**
|
||||
|
||||
```
|
||||
Format: {device_type}-{region}-{device_id}
|
||||
Example: sensor-us-west-001
|
||||
```
|
||||
|
||||
**Geographic Sharding:**
|
||||
|
||||
```
|
||||
Format: {region}-{service}-{id}
|
||||
Example: us-east-sensor-001
|
||||
@@ -475,9 +500,9 @@ Example: us-east-sensor-001
|
||||
|
||||
### Metrics to Monitor
|
||||
|
||||
```promql
|
||||
```yaml
|
||||
# Local routing ratio (target: >95%)
|
||||
sum(rate(mqtt_messages_delivered_locally[5m]))
|
||||
sum(rate(mqtt_messages_delivered_locally[5m]))
|
||||
/ sum(rate(mqtt_messages_total[5m]))
|
||||
|
||||
# Cross-node routing (target: <5%)
|
||||
@@ -553,16 +578,19 @@ go test -race -run=TestStress_ConcurrentPublishers ./mqtt/broker
|
||||
### Interpreting Results
|
||||
|
||||
**Benchmark output format:**
|
||||
|
||||
```
|
||||
BenchmarkName-16 iterations ns/op B/op allocs/op
|
||||
```
|
||||
|
||||
**Zero-copy indicators:**
|
||||
|
||||
1. Zero allocations in payload sharing: `0 B/op, 0 allocs/op`
|
||||
2. Memory independent of subscriber count
|
||||
3. Constant time for different message sizes
|
||||
|
||||
**CI regression checks:**
|
||||
|
||||
```bash
|
||||
# Fail if latency > 100 μs/op (current: 67 μs)
|
||||
# Fail if allocations > 10/op (current: 4)
|
||||
@@ -593,6 +621,7 @@ benchstat baseline.txt optimized.txt
|
||||
### Key Insight
|
||||
|
||||
100M clients cannot be handled as "one big cluster":
|
||||
|
||||
- Network: 200 nodes × 199 peers = 39,800 connections
|
||||
- Gossip: 200 nodes = 100x network amplification
|
||||
- etcd: Maximum 7-9 members (not 200)
|
||||
@@ -651,19 +680,21 @@ benchstat baseline.txt optimized.txt
|
||||
4. **BoltDB Backend:** B+ tree with ~10x write amplification
|
||||
|
||||
**Measured Performance (etcd v3.5, SSD):**
|
||||
|
||||
- Small writes (100 bytes): 8K writes/sec
|
||||
- Medium writes (1KB): 5K writes/sec
|
||||
- Large writes (10KB): 1K writes/sec
|
||||
|
||||
### Why Can't We Make etcd Faster?
|
||||
|
||||
| Option | Effect | Result |
|
||||
| ----------------- | --------------------- | --------------------- |
|
||||
| Option | Effect | Result |
|
||||
| ----------------- | ---------------------- | --------------------- |
|
||||
| More nodes | ❌ Doesn't help writes | Still single leader |
|
||||
| Better hardware | +50% | Still <10K writes/sec |
|
||||
| Aggressive tuning | 2-3x | ~15K writes/sec max |
|
||||
| Better hardware | +50% | Still <10K writes/sec |
|
||||
| Aggressive tuning | 2-3x | ~15K writes/sec max |
|
||||
|
||||
**Theoretical Maximum (aggressive tuning):**
|
||||
|
||||
- Batching: 100 writes/batch
|
||||
- No fsync: Skip disk writes (dangerous)
|
||||
- Result: ~50K writes/sec
|
||||
@@ -672,11 +703,13 @@ benchstat baseline.txt optimized.txt
|
||||
### Alternative Architectures
|
||||
|
||||
**1. Consistent Hashing + Gossip (Cassandra-Style)**
|
||||
|
||||
- Linear scaling, no SPOF
|
||||
- Eventual consistency (acceptable for subscriptions)
|
||||
- Effort: 3-4 months
|
||||
|
||||
**2. Kafka-Based Event Sourcing**
|
||||
|
||||
- Full audit trail, scalable
|
||||
- External dependency (Kafka cluster)
|
||||
- Effort: 4-6 months
|
||||
@@ -724,11 +757,13 @@ benchstat baseline.txt optimized.txt
|
||||
### When to Build Custom Raft
|
||||
|
||||
**Don't build if:**
|
||||
|
||||
- Target <20M clients (gossip sufficient)
|
||||
- Timeline <6 months
|
||||
- Team <3 senior engineers
|
||||
|
||||
**Consider if:**
|
||||
|
||||
- Target >50M clients
|
||||
- 12+ month timeline
|
||||
- Experienced distributed systems team
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Webhook System
|
||||
description: Comprehensive webhook system for asynchronous event notifications with circuit breaker, retry logic, and flexible filtering
|
||||
---
|
||||
|
||||
# Webhook System Documentation
|
||||
|
||||
## Overview
|
||||
@@ -5,6 +10,7 @@
|
||||
The MQTT broker provides a comprehensive webhook system for asynchronous event notifications. This enables integrations with analytics services, audit systems, monitoring platforms, and custom applications.
|
||||
|
||||
**Design Philosophy:**
|
||||
|
||||
1. **Protocol-Agnostic** - Sender interface allows HTTP, gRPC, or custom protocols
|
||||
2. **Non-Blocking** - Worker pool with buffered queue ensures zero impact on broker performance
|
||||
3. **Resilient** - Circuit breaker, exponential backoff retry, graceful degradation
|
||||
@@ -84,25 +90,26 @@ The MQTT broker provides a comprehensive webhook system for asynchronous event n
|
||||
|
||||
**All Broker Operations Emit Events:**
|
||||
|
||||
| Event Type | Trigger | Payload Fields |
|
||||
|-------------------------------|----------------------------------|-----------------------------------------------------|
|
||||
| `client.connected` | CreateSession() succeeds | clientID, protocol, cleanStart, keepAlive, addr |
|
||||
| `client.disconnected` | handleDisconnect() | clientID, reason, remoteAddr |
|
||||
| `client.session_takeover` | Cluster session migration | clientID, fromNode, toNode |
|
||||
| `message.published` | Publish() | clientID, topic, qos, retained, payloadSize, payload|
|
||||
| `message.delivered` | DeliverToSession() | clientID, topic, qos, payloadSize |
|
||||
| `message.retained` | Retained message set/cleared | topic, payloadSize, cleared |
|
||||
| `subscription.created` | subscribe() | clientID, topicFilter, qos, subscriptionID |
|
||||
| `subscription.removed` | unsubscribeInternal() | clientID, topicFilter |
|
||||
| `auth.success` | Auth check passes | clientID, remoteAddr |
|
||||
| `auth.failure` | Auth check fails | clientID, reason, remoteAddr |
|
||||
| `authz.publish_denied` | Publish authorization denied | clientID, topic, reason |
|
||||
| `authz.subscribe_denied` | Subscribe authorization denied | clientID, topicFilter, reason |
|
||||
| Event Type | Trigger | Payload Fields |
|
||||
| ------------------------- | ------------------------------ | ---------------------------------------------------- |
|
||||
| `client.connected` | CreateSession() succeeds | clientID, protocol, cleanStart, keepAlive, addr |
|
||||
| `client.disconnected` | handleDisconnect() | clientID, reason, remoteAddr |
|
||||
| `client.session_takeover` | Cluster session migration | clientID, fromNode, toNode |
|
||||
| `message.published` | Publish() | clientID, topic, qos, retained, payloadSize, payload |
|
||||
| `message.delivered` | DeliverToSession() | clientID, topic, qos, payloadSize |
|
||||
| `message.retained` | Retained message set/cleared | topic, payloadSize, cleared |
|
||||
| `subscription.created` | subscribe() | clientID, topicFilter, qos, subscriptionID |
|
||||
| `subscription.removed` | unsubscribeInternal() | clientID, topicFilter |
|
||||
| `auth.success` | Auth check passes | clientID, remoteAddr |
|
||||
| `auth.failure` | Auth check fails | clientID, reason, remoteAddr |
|
||||
| `authz.publish_denied` | Publish authorization denied | clientID, topic, reason |
|
||||
| `authz.subscribe_denied` | Subscribe authorization denied | clientID, topicFilter, reason |
|
||||
|
||||
Note: Message payloads are not currently included in webhook events. The
|
||||
`payload` field is omitted regardless of `include_payload`.
|
||||
|
||||
**Event Envelope (Common Wrapper):**
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "message.published",
|
||||
@@ -124,6 +131,7 @@ Note: Message payloads are not currently included in webhook events. The
|
||||
**Responsibility**: Protocol-agnostic worker pool with circuit breaker and retry
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- **Buffered Queue** (10k events): Non-blocking `Notify()` calls
|
||||
- **Worker Pool** (5 workers): Concurrent event processing
|
||||
- **Circuit Breaker** (per endpoint): Fail-fast when endpoints are down
|
||||
@@ -134,13 +142,14 @@ Note: Message payloads are not currently included in webhook events. The
|
||||
- **Graceful Shutdown**: Drains queue with configurable timeout (30s default)
|
||||
|
||||
**Worker Pool Configuration:**
|
||||
|
||||
```yaml
|
||||
webhook:
|
||||
enabled: true
|
||||
queue_size: 10000
|
||||
drop_policy: "oldest" # or "newest"
|
||||
drop_policy: "oldest" # or "newest"
|
||||
workers: 5
|
||||
include_payload: false # Accepted by config, payload inclusion not yet wired
|
||||
include_payload: false # Accepted by config, payload inclusion not yet wired
|
||||
shutdown_timeout: 30s
|
||||
```
|
||||
|
||||
@@ -151,15 +160,17 @@ webhook:
|
||||
**Responsibility**: Send webhooks via HTTP POST
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Simple HTTP client with configurable timeout
|
||||
- Custom headers (e.g., `Authorization: Bearer token`)
|
||||
- JSON payload serialization
|
||||
- Returns error for non-2xx status codes
|
||||
|
||||
**Interface:**
|
||||
|
||||
```go
|
||||
type Sender interface {
|
||||
Send(ctx context.Context, url string, headers map[string]string,
|
||||
Send(ctx context.Context, url string, headers map[string]string,
|
||||
payload []byte, timeout time.Duration) error
|
||||
}
|
||||
```
|
||||
@@ -167,6 +178,7 @@ type Sender interface {
|
||||
##### gRPC Sender (Future)
|
||||
|
||||
**Planned Features:**
|
||||
|
||||
- Protocol Buffer serialization
|
||||
- Streaming support for high-volume events
|
||||
- TLS mutual authentication
|
||||
@@ -175,26 +187,27 @@ type Sender interface {
|
||||
#### 4. Endpoint Configuration
|
||||
|
||||
**Per-Endpoint Settings:**
|
||||
|
||||
```yaml
|
||||
endpoints:
|
||||
- name: "analytics-service"
|
||||
type: "http"
|
||||
url: "https://analytics.example.com/mqtt/events"
|
||||
|
||||
|
||||
# Filter by event type (empty = all events)
|
||||
events:
|
||||
- "message.published"
|
||||
- "client.connected"
|
||||
|
||||
|
||||
# Filter by topic pattern (empty = all topics)
|
||||
topic_filters:
|
||||
- "sensors/#"
|
||||
- "devices/+/telemetry"
|
||||
|
||||
|
||||
# Custom headers
|
||||
headers:
|
||||
Authorization: "Bearer secret-token"
|
||||
|
||||
|
||||
# Override defaults (optional)
|
||||
timeout: 10s
|
||||
retry:
|
||||
@@ -206,11 +219,13 @@ endpoints:
|
||||
#### Circuit Breaker (sony/gobreaker)
|
||||
|
||||
**States:**
|
||||
|
||||
- **Closed**: Normal operation, all requests sent
|
||||
- **Open**: Endpoint failing, requests fail fast (no network calls)
|
||||
- **Half-Open**: Testing if endpoint recovered
|
||||
|
||||
**Thresholds:**
|
||||
|
||||
- **Failure threshold**: 5 consecutive failures → Open
|
||||
- **Reset timeout**: 60s in Open state → Half-Open
|
||||
- **Success in Half-Open** → Closed
|
||||
@@ -218,6 +233,7 @@ endpoints:
|
||||
#### Retry Strategy
|
||||
|
||||
**Exponential Backoff:**
|
||||
|
||||
```
|
||||
Attempt 1: immediate
|
||||
Attempt 2: 1s delay
|
||||
@@ -228,6 +244,7 @@ Max: 30s cap
|
||||
```
|
||||
|
||||
**Dead Letter Handling:**
|
||||
|
||||
- After max retries exhausted: log error with full event details
|
||||
- No persistent dead letter queue (events are dropped)
|
||||
- Future: Optional file-based dead letter queue for replay
|
||||
@@ -235,6 +252,7 @@ Max: 30s cap
|
||||
#### Queue Overflow
|
||||
|
||||
**Drop Policies:**
|
||||
|
||||
- **"oldest"**: Drop oldest events, keep newest (default)
|
||||
- Ensures latest broker state is always captured
|
||||
- Best for real-time monitoring
|
||||
@@ -253,7 +271,7 @@ Webhook notifications added to all domain operations:
|
||||
|
||||
func (b *Broker) CreateSession(...) (*session.Session, bool, error) {
|
||||
// ... create session logic ...
|
||||
|
||||
|
||||
// Webhook notification (non-blocking)
|
||||
if b.webhooks != nil {
|
||||
b.webhooks.Notify(ctx, events.ClientConnected{
|
||||
@@ -264,12 +282,13 @@ func (b *Broker) CreateSession(...) (*session.Session, bool, error) {
|
||||
RemoteAddr: "", // Not available at broker level
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return sess, true, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern:**
|
||||
|
||||
- Check if webhooks enabled: `if b.webhooks != nil`
|
||||
- Non-blocking call: `Notify()` returns immediately
|
||||
- No error handling: broker never blocks on webhook failures
|
||||
@@ -277,6 +296,7 @@ func (b *Broker) CreateSession(...) (*session.Session, bool, error) {
|
||||
#### Initialization (main.go)
|
||||
|
||||
**Dependency Injection Pattern:**
|
||||
|
||||
```go
|
||||
// cmd/main.go
|
||||
|
||||
@@ -312,6 +332,7 @@ broker := broker.NewBroker(store, cluster, logger, stats, notifier)
|
||||
- ✅ Circuit breaker integration
|
||||
|
||||
**Test Coverage:**
|
||||
|
||||
- HTTP Sender: 7 test cases
|
||||
- Notifier: 10 test cases
|
||||
- All tests passing ✅
|
||||
@@ -319,27 +340,32 @@ broker := broker.NewBroker(store, cluster, logger, stats, notifier)
|
||||
### Future Enhancements
|
||||
|
||||
#### 1. gRPC Sender
|
||||
|
||||
- Protocol Buffer serialization
|
||||
- Bi-directional streaming
|
||||
- Connection pooling
|
||||
- TLS mutual auth
|
||||
|
||||
#### 2. Batch Delivery
|
||||
|
||||
- Group events before sending (reduce HTTP overhead)
|
||||
- Configurable batch size and timeout
|
||||
- Example: Send 100 events or every 1s, whichever comes first
|
||||
|
||||
#### 3. Dead Letter Queue
|
||||
|
||||
- Persist failed events to disk
|
||||
- Manual replay endpoint
|
||||
- Webhook retry dashboard
|
||||
|
||||
#### 4. Webhook Management API
|
||||
|
||||
- Add/remove endpoints at runtime (no restart)
|
||||
- Test webhook endpoint (send test event)
|
||||
- View webhook stats (success/failure rates)
|
||||
|
||||
#### 5. Event Sampling
|
||||
|
||||
- For high-volume deployments, sample events
|
||||
- Example: Send 1% of `message.delivered` events
|
||||
- Configurable per event type
|
||||
@@ -348,12 +374,12 @@ broker := broker.NewBroker(store, cluster, logger, stats, notifier)
|
||||
|
||||
**Benchmark Results** (Estimated):
|
||||
|
||||
| Scenario | Overhead | Notes |
|
||||
|------------------------------------|--------------|--------------------------------|
|
||||
| Webhooks disabled | 0% | No-op, zero cost |
|
||||
| Webhooks enabled, queue not full | < 0.1% | Single channel send |
|
||||
| Webhooks enabled, queue full | < 0.5% | Drop oldest + enqueue |
|
||||
| 1M messages/sec with webhooks | ~0.2% | Workers process asynchronously |
|
||||
| Scenario | Overhead | Notes |
|
||||
| -------------------------------- | -------- | ------------------------------ |
|
||||
| Webhooks disabled | 0% | No-op, zero cost |
|
||||
| Webhooks enabled, queue not full | < 0.1% | Single channel send |
|
||||
| Webhooks enabled, queue full | < 0.5% | Drop oldest + enqueue |
|
||||
| 1M messages/sec with webhooks | ~0.2% | Workers process asynchronously |
|
||||
|
||||
**Key Insight**: Worker pool architecture ensures broker performance is unaffected even under heavy load.
|
||||
|
||||
@@ -369,6 +395,7 @@ The webhook system provides production-ready event notifications with:
|
||||
- ✅ **Easy to extend** - Clean separation: Notifier (worker pool) vs Sender (protocol)
|
||||
|
||||
**Files:**
|
||||
|
||||
- `broker/events/events.go` - Event definitions (12 types)
|
||||
- `broker/webhook/webhook.go` - Interfaces (Notifier, Sender)
|
||||
- `broker/webhook/notifier.go` - Generic worker pool + circuit breaker
|
||||
@@ -389,7 +416,7 @@ webhook:
|
||||
queue_size: 10000
|
||||
drop_policy: "oldest"
|
||||
workers: 5
|
||||
include_payload: false # Accepted by config, payload inclusion not yet wired
|
||||
include_payload: false # Accepted by config, payload inclusion not yet wired
|
||||
shutdown_timeout: 30s
|
||||
|
||||
defaults:
|
||||
@@ -444,20 +471,20 @@ Your webhook endpoint will receive HTTP POST requests with JSON payloads:
|
||||
|
||||
## Event Types
|
||||
|
||||
| Event Type | Description | Key Fields |
|
||||
|-------------------------------|----------------------------------------------|-------------------------------------------------|
|
||||
| `client.connected` | Client successfully connected | clientID, protocol, cleanStart, keepAlive |
|
||||
| `client.disconnected` | Client disconnected | clientID, reason, remoteAddr |
|
||||
| `client.session_takeover` | Session migrated between cluster nodes | clientID, fromNode, toNode |
|
||||
| `message.published` | Message published to broker | clientID, topic, qos, retained, payloadSize |
|
||||
| `message.delivered` | Message delivered to subscriber | clientID, topic, qos, payloadSize |
|
||||
| `message.retained` | Retained message set or cleared | topic, payloadSize, cleared |
|
||||
| `subscription.created` | Client subscribed to topic | clientID, topicFilter, qos |
|
||||
| `subscription.removed` | Client unsubscribed from topic | clientID, topicFilter |
|
||||
| `auth.success` | Authentication succeeded | clientID, remoteAddr |
|
||||
| `auth.failure` | Authentication failed | clientID, reason, remoteAddr |
|
||||
| `authz.publish_denied` | Publish authorization denied | clientID, topic, reason |
|
||||
| `authz.subscribe_denied` | Subscribe authorization denied | clientID, topicFilter, reason |
|
||||
| Event Type | Description | Key Fields |
|
||||
| ------------------------- | -------------------------------------- | ------------------------------------------- |
|
||||
| `client.connected` | Client successfully connected | clientID, protocol, cleanStart, keepAlive |
|
||||
| `client.disconnected` | Client disconnected | clientID, reason, remoteAddr |
|
||||
| `client.session_takeover` | Session migrated between cluster nodes | clientID, fromNode, toNode |
|
||||
| `message.published` | Message published to broker | clientID, topic, qos, retained, payloadSize |
|
||||
| `message.delivered` | Message delivered to subscriber | clientID, topic, qos, payloadSize |
|
||||
| `message.retained` | Retained message set or cleared | topic, payloadSize, cleared |
|
||||
| `subscription.created` | Client subscribed to topic | clientID, topicFilter, qos |
|
||||
| `subscription.removed` | Client unsubscribed from topic | clientID, topicFilter |
|
||||
| `auth.success` | Authentication succeeded | clientID, remoteAddr |
|
||||
| `auth.failure` | Authentication failed | clientID, reason, remoteAddr |
|
||||
| `authz.publish_denied` | Publish authorization denied | clientID, topic, reason |
|
||||
| `authz.subscribe_denied` | Subscribe authorization denied | clientID, topicFilter, reason |
|
||||
|
||||
## Filtering
|
||||
|
||||
@@ -493,6 +520,7 @@ endpoints:
|
||||
```
|
||||
|
||||
**MQTT Wildcards:**
|
||||
|
||||
- `+` - Single-level wildcard (e.g., `sensors/+/temp` matches `sensors/device1/temp`)
|
||||
- `#` - Multi-level wildcard (e.g., `sensors/#` matches `sensors/temp` and `sensors/room1/temp`)
|
||||
|
||||
@@ -502,39 +530,39 @@ Empty `topic_filters` array = all topics.
|
||||
|
||||
### Global Settings
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|----------------------|----------|-----------|-------------------------------------------------------|
|
||||
| `enabled` | bool | false | Enable/disable webhook system |
|
||||
| `queue_size` | int | 10000 | Max events in memory queue |
|
||||
| `drop_policy` | string | "oldest" | "oldest" or "newest" when queue is full |
|
||||
| `workers` | int | 5 | Number of concurrent worker goroutines |
|
||||
| `include_payload` | bool | false | Accepted by config, payload inclusion not yet wired |
|
||||
| `shutdown_timeout` | duration | 30s | Graceful shutdown timeout |
|
||||
| Field | Type | Default | Description |
|
||||
| ------------------ | -------- | -------- | --------------------------------------------------- |
|
||||
| `enabled` | bool | false | Enable/disable webhook system |
|
||||
| `queue_size` | int | 10000 | Max events in memory queue |
|
||||
| `drop_policy` | string | "oldest" | "oldest" or "newest" when queue is full |
|
||||
| `workers` | int | 5 | Number of concurrent worker goroutines |
|
||||
| `include_payload` | bool | false | Accepted by config, payload inclusion not yet wired |
|
||||
| `shutdown_timeout` | duration | 30s | Graceful shutdown timeout |
|
||||
|
||||
### Default Settings
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|----------------------------------------|----------|---------|------------------------------------------|
|
||||
| `defaults.timeout` | duration | 5s | HTTP request timeout |
|
||||
| `defaults.retry.max_attempts` | int | 3 | Maximum retry attempts |
|
||||
| `defaults.retry.initial_interval` | duration | 1s | Initial retry delay |
|
||||
| `defaults.retry.max_interval` | duration | 30s | Maximum retry delay |
|
||||
| `defaults.retry.multiplier` | float64 | 2.0 | Backoff multiplier (exponential) |
|
||||
| `defaults.circuit_breaker.failure_threshold` | int | 5 | Consecutive failures before opening |
|
||||
| `defaults.circuit_breaker.reset_timeout` | duration | 60s | Time in open state before half-open |
|
||||
| Field | Type | Default | Description |
|
||||
| -------------------------------------------- | -------- | ------- | ----------------------------------- |
|
||||
| `defaults.timeout` | duration | 5s | HTTP request timeout |
|
||||
| `defaults.retry.max_attempts` | int | 3 | Maximum retry attempts |
|
||||
| `defaults.retry.initial_interval` | duration | 1s | Initial retry delay |
|
||||
| `defaults.retry.max_interval` | duration | 30s | Maximum retry delay |
|
||||
| `defaults.retry.multiplier` | float64 | 2.0 | Backoff multiplier (exponential) |
|
||||
| `defaults.circuit_breaker.failure_threshold` | int | 5 | Consecutive failures before opening |
|
||||
| `defaults.circuit_breaker.reset_timeout` | duration | 60s | Time in open state before half-open |
|
||||
|
||||
### Endpoint Settings
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|------------------|-------------------|----------|-----------------------------------------------|
|
||||
| `name` | string | Yes | Unique endpoint identifier |
|
||||
| `type` | string | Yes | "http" (gRPC support planned) |
|
||||
| `url` | string | Yes | Webhook URL |
|
||||
| `events` | []string | No | Event type filter (empty = all) |
|
||||
| `topic_filters` | []string | No | Topic pattern filter (empty = all) |
|
||||
| `headers` | map[string]string | No | Custom HTTP headers |
|
||||
| `timeout` | duration | No | Override default timeout |
|
||||
| `retry` | RetryConfig | No | Override default retry config |
|
||||
| Field | Type | Required | Description |
|
||||
| --------------- | ----------------- | -------- | ---------------------------------- |
|
||||
| `name` | string | Yes | Unique endpoint identifier |
|
||||
| `type` | string | Yes | "http" (gRPC support planned) |
|
||||
| `url` | string | Yes | Webhook URL |
|
||||
| `events` | []string | No | Event type filter (empty = all) |
|
||||
| `topic_filters` | []string | No | Topic pattern filter (empty = all) |
|
||||
| `headers` | map[string]string | No | Custom HTTP headers |
|
||||
| `timeout` | duration | No | Override default timeout |
|
||||
| `retry` | RetryConfig | No | Override default retry config |
|
||||
|
||||
## Resilience
|
||||
|
||||
@@ -543,11 +571,13 @@ Empty `topic_filters` array = all topics.
|
||||
The circuit breaker prevents cascading failures by failing fast when an endpoint is unhealthy.
|
||||
|
||||
**States:**
|
||||
|
||||
- **Closed**: Normal operation, all requests sent
|
||||
- **Open**: Endpoint failing, requests fail immediately (no network calls)
|
||||
- **Half-Open**: Testing if endpoint recovered
|
||||
|
||||
**Thresholds:**
|
||||
|
||||
- 5 consecutive failures → Open circuit
|
||||
- 60 seconds in Open → try Half-Open
|
||||
- Success in Half-Open → Close circuit
|
||||
@@ -572,11 +602,13 @@ After max retries exhausted, the event is logged and dropped.
|
||||
When the event queue is full:
|
||||
|
||||
**Drop Policy: "oldest"** (default)
|
||||
|
||||
- Removes oldest event from queue
|
||||
- Adds new event
|
||||
- Best for: Real-time monitoring (latest state matters)
|
||||
|
||||
**Drop Policy: "newest"**
|
||||
|
||||
- Keeps existing queue
|
||||
- Drops incoming event
|
||||
- Best for: Audit logs (chronological order matters)
|
||||
@@ -585,12 +617,12 @@ When the event queue is full:
|
||||
|
||||
### Overhead
|
||||
|
||||
| Scenario | Overhead | Notes |
|
||||
|------------------------------------|-----------|-------------------------------|
|
||||
| Webhooks disabled | 0% | No-op, zero cost |
|
||||
| Webhooks enabled, queue not full | < 0.1% | Single channel send |
|
||||
| Webhooks enabled, queue full | < 0.5% | Drop oldest + enqueue |
|
||||
| 1M messages/sec with webhooks | ~0.2% | Workers process async |
|
||||
| Scenario | Overhead | Notes |
|
||||
| -------------------------------- | -------- | --------------------- |
|
||||
| Webhooks disabled | 0% | No-op, zero cost |
|
||||
| Webhooks enabled, queue not full | < 0.1% | Single channel send |
|
||||
| Webhooks enabled, queue full | < 0.5% | Drop oldest + enqueue |
|
||||
| 1M messages/sec with webhooks | ~0.2% | Workers process async |
|
||||
|
||||
**Key Insight**: Non-blocking design ensures broker performance is unaffected.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { BaseLayoutProps, LinkItemType } from "fumadocs-ui/layouts/shared";
|
||||
import Image from "next/image";
|
||||
import Logo from "@/public/logo.png";
|
||||
|
||||
export const LogoImage = () => (
|
||||
<Image
|
||||
alt="FluxMQ"
|
||||
src={Logo}
|
||||
sizes="100px"
|
||||
className="hidden w-22 in-[.uwu]:block"
|
||||
aria-label="Fumadocs"
|
||||
/>
|
||||
);
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
nav: {
|
||||
title: (
|
||||
<div
|
||||
className="text-2xl text-center font-bold animate-fade-in"
|
||||
style={{ lineHeight: "1.1" }}
|
||||
>
|
||||
<span className="text-(--flux-blue)">Flux</span>
|
||||
<span className="text-(--flux-orange)">MQ</span>
|
||||
</div>
|
||||
),
|
||||
url: "/",
|
||||
},
|
||||
links: [
|
||||
{
|
||||
text: "Features",
|
||||
url: "/#features",
|
||||
},
|
||||
{
|
||||
text: "Performance",
|
||||
url: "/#performance",
|
||||
},
|
||||
{
|
||||
text: "Use Cases",
|
||||
url: "/#use-cases",
|
||||
},
|
||||
{
|
||||
text: "Architecture",
|
||||
url: "/#architecture",
|
||||
},
|
||||
{
|
||||
text: "Quick Start",
|
||||
url: "/#quick-start",
|
||||
},
|
||||
{
|
||||
text: "Documentation",
|
||||
url: "/docs",
|
||||
},
|
||||
{
|
||||
type: "icon",
|
||||
label: "github",
|
||||
text: "Github",
|
||||
url: "https://github.com/absmach/fluxmq",
|
||||
icon: (
|
||||
<svg role="img" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
),
|
||||
external: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { docs } from 'fumadocs-mdx:collections/server';
|
||||
import { type InferPageType, loader } from 'fumadocs-core/source';
|
||||
|
||||
// See https://fumadocs.dev/docs/headless/source-api for more info
|
||||
export const source = loader({
|
||||
baseUrl: '/docs',
|
||||
source: docs.toFumadocsSource(),
|
||||
plugins: [],
|
||||
});
|
||||
|
||||
export function getPageImage(page: InferPageType<typeof source>) {
|
||||
const segments = [...page.slugs, 'image.png'];
|
||||
|
||||
return {
|
||||
segments,
|
||||
url: `/og/docs/${segments.join('/')}`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLLMText(page: InferPageType<typeof source>) {
|
||||
const processed = await page.data.getText('processed');
|
||||
|
||||
return `# ${page.data.title}
|
||||
|
||||
${processed}`;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
|
||||
export function getMDXComponents(components?: MDXComponents): MDXComponents {
|
||||
return {
|
||||
...defaultMdxComponents,
|
||||
...components,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createMDX } from "fumadocs-mdx/next";
|
||||
|
||||
const withMDX = createMDX();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
output: "export",
|
||||
reactStrictMode: true,
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default withMDX(config);
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"start": "serve out",
|
||||
"types:check": "fumadocs-mdx && next typegen && tsc --noEmit",
|
||||
"postinstall": "fumadocs-mdx",
|
||||
"lint": "biome check",
|
||||
"format": "biome format --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@orama/orama": "^3.1.18",
|
||||
"@tisoap/react-flow-smart-edge": "^4.0.1",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"fumadocs-core": "16.5.0",
|
||||
"fumadocs-mdx": "14.2.6",
|
||||
"fumadocs-ui": "16.5.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"next": "16.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.13",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.6",
|
||||
"serve": "^14.2.5",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
|
||||
}
|
||||
Generated
+5645
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,22 @@
|
||||
import { defineConfig, defineDocs, frontmatterSchema, metaSchema } from 'fumadocs-mdx/config';
|
||||
|
||||
// You can customise Zod schemas for frontmatter and `meta.json` here
|
||||
// see https://fumadocs.dev/docs/mdx/collections
|
||||
export const docs = defineDocs({
|
||||
dir: 'content/docs',
|
||||
docs: {
|
||||
schema: frontmatterSchema,
|
||||
postprocess: {
|
||||
includeProcessedMarkdown: true,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
schema: metaSchema,
|
||||
},
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
mdxOptions: {
|
||||
// MDX options
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"fumadocs-mdx:collections/*": [".source/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user