ktor-native-worker-tutorial

Part 6: Test and Demo

In this final part, we’ll explore how to set up, run, and test the complete application across different platforms.

Prerequisites

Before running the application, ensure you have:

  1. RabbitMQ Server:
    • Running locally or accessible via network
    • Default connection: localhost:5672 with guest:guest
    • You can run RabbitMQ using Docker:
      docker run -d --name rabbitmq \
        -p 5672:5672 \
        -p 15672:15672 \
        rabbitmq:3-management
      
  2. Firebase Service Account:
    • Download from Firebase Console → Project Settings → Service Accounts
    • Save as firebase-admin-sdk.json in the project root
    • Or specify custom path via SERVICE_ACCOUNT_PATH environment variable
  3. FCM Device Token:
    • Obtain from a mobile app integrated with Firebase Cloud Messaging
    • Needed to test actual notification sending

Running the Application

Running on JVM (Development)

The JVM target is ideal for development:

./gradlew jvmRun

This command:

Running on Native Platforms

For production deployment, use native executables:

macOS (ARM64):

./gradlew runDebugExecutableMacosArm64

Linux (x64):

./gradlew runDebugExecutableLinuxX64

Windows (x64):

./gradlew runDebugExecutableMingwX64

The application will:

  1. Connect to RabbitMQ
  2. Declare exchange and queue
  3. Start consuming messages from the queue
  4. Start the HTTP server on port 8080

You should see output similar to:

[main] INFO  Application - Application started
[main] INFO  Application - Responding at http://0.0.0.0:8080

Building Production Executables

To build optimized native executables:

./gradlew build

Executables will be located at:

These are standalone executables that can be deployed without a JVM.

Environment Configuration

Configure the application using environment variables:

export RABBITMQ_HOST=localhost
export RABBITMQ_PORT=5672
export RABBITMQ_USER=guest
export RABBITMQ_PASSWORD=guest
export SERVICE_ACCOUNT_PATH=/path/to/firebase-admin-sdk.json

Then run:

./gradlew jvmRun

Or for native executables:

export SERVICE_ACCOUNT_PATH=/path/to/firebase-admin-sdk.json
./build/bin/macosArm64/releaseExecutable/ktor-native-worker-tutorial.kexe

Testing the API

Using curl

Send a test notification request:

curl -X POST http://localhost:8080/api/notifications \
  -H "Content-Type: application/json" \
  -d '{
    "token": "your-fcm-device-token-here",
    "title": "Hello World",
    "body": "This is a test notification from Ktor Native!"
  }'

Expected response:

HTTP/1.1 200 OK

Using HTTPie

If you prefer HTTPie:

http POST localhost:8080/api/notifications \
  token=your-fcm-device-token-here \
  title="Hello World" \
  body="This is a test notification from Ktor Native!"

Using Postman

  1. Create a new POST request
  2. URL: http://localhost:8080/api/notifications
  3. Headers: Content-Type: application/json
  4. Body (raw JSON):
    {
      "token": "your-fcm-device-token-here",
      "title": "Hello World",
      "body": "This is a test notification from Ktor Native!"
    }
    
  5. Send the request

Understanding the Flow

When you send a request, here’s what happens:

  1. HTTP Request Received:
    • Ktor receives POST to /api/notifications
    • Content negotiation deserializes JSON to NotificationPayload
  2. Message Published:
    • Route handler creates SendNotificationEvent
    • Event serialized to JSON
    • Published to RabbitMQ exchange with routing key
    • HTTP 200 OK returned immediately
  3. Message Queued:
    • RabbitMQ routes message to the queue
    • Message persisted to disk (durable queue)
  4. Message Consumed:
    • Application’s consumer receives the message
    • SendNotificationHandler invoked
  5. Notification Sent:
    • Handler deserializes SendNotificationEvent
    • Calls NotificationService.sendNotification()
    • FCM API sends push notification to device

Monitoring RabbitMQ

If you’re running RabbitMQ with the management plugin:

  1. Open http://localhost:15672 in your browser
  2. Login with guest:guest
  3. Navigate to “Queues” tab
  4. You should see notifications_queue
  5. View message rates, consumers, and queue depth

Verifying the Setup

Check RabbitMQ Connection

After starting the application, verify RabbitMQ connection:

  1. In RabbitMQ Management UI, go to “Connections”
  2. You should see an active connection from your application

Check Exchange and Queue

  1. Go to “Exchanges” tab
  2. Verify notifications_exchange exists (type: topic)
  3. Go to “Queues” tab
  4. Verify notifications_queue exists (durable: true)
  5. Check that it’s bound to the exchange with routing key notifications.send

Check Consumer

  1. In the queue details for notifications_queue
  2. Scroll to “Consumers” section
  3. You should see 1 active consumer

Testing Error Scenarios

Invalid JSON

curl -X POST http://localhost:8080/api/notifications \
  -H "Content-Type: application/json" \
  -d '{"invalid": "data"}'

Expected: 400 Bad Request (missing required fields)

Missing Content-Type

curl -X POST http://localhost:8080/api/notifications \
  -d '{"token": "xxx", "title": "Hi", "body": "Test"}'

Expected: 415 Unsupported Media Type

Invalid FCM Token

curl -X POST http://localhost:8080/api/notifications \
  -H "Content-Type: application/json" \
  -d '{
    "token": "invalid-token",
    "title": "Test",
    "body": "This will fail"
  }'

Expected: 200 OK (request accepted), but notification sending will fail in the worker. Check application logs for FCM error.

Performance Testing

Simple Load Test with curl

for i in {1..100}; do
  curl -X POST http://localhost:8080/api/notifications \
    -H "Content-Type: application/json" \
    -d "{\"token\": \"test-$i\", \"title\": \"Test $i\", \"body\": \"Message $i\"}" &
done
wait

This sends 100 concurrent requests. Monitor:

Using Apache Bench

ab -n 1000 -c 10 -p payload.json -T application/json \
  http://localhost:8080/api/notifications

Create payload.json:

{
  "token": "test-token",
  "title": "Load Test",
  "body": "Testing"
}

Comparing JVM vs Native Performance

Startup Time

JVM:

time ./gradlew jvmRun

Native:

time ./build/bin/macosArm64/releaseExecutable/ktor-native-worker-tutorial.kexe

Native executables typically start much faster than JVM.

Memory Usage

Monitor memory usage while running:

On macOS/Linux:

# JVM
ps aux | grep java

# Native
ps aux | grep ktor-native-worker-tutorial

Native executables typically use less memory than JVM.

Request Throughput

Use Apache Bench or a similar tool to compare request handling performance between JVM and native builds.

Deployment Considerations

Docker Deployment

Create a Dockerfile for native builds:

FROM alpine:latest

# Install dependencies (if any needed by your native binary)
RUN apk add --no-cache libstdc++

# Copy the native executable
COPY build/bin/linuxX64/releaseExecutable/ktor-native-worker-tutorial.kexe /app/server

# Copy Firebase service account
COPY firebase-admin-sdk.json /app/

WORKDIR /app

# Set environment variables
ENV RABBITMQ_HOST=rabbitmq
ENV SERVICE_ACCOUNT_PATH=/app/firebase-admin-sdk.json

# Run the executable
CMD ["/app/server"]

Build and run:

docker build -t ktor-worker .
docker run -p 8080:8080 --link rabbitmq ktor-worker

Kubernetes Deployment

The native executable is perfect for Kubernetes:

Scaling Workers

You can run multiple instances of the application:

Troubleshooting

Application Won’t Start

Check RabbitMQ Connection:

# Verify RabbitMQ is running
docker ps | grep rabbitmq

# Check connectivity
telnet localhost 5672

Check Firebase Service Account:

# Verify file exists
ls -la firebase-admin-sdk.json

# Verify it's valid JSON
cat firebase-admin-sdk.json | python -m json.tool

Messages Not Being Processed

Check Consumer Status:

Check Queue Bindings:

Notifications Not Received

Verify FCM Token:

Check Application Logs:

Summary

This tutorial covered:

Complete Architecture Overview

Putting it all together:

  1. Gradle Setup: Multiplatform build with JVM and Native targets
  2. Notification Service: FCM integration using Flareon library
  3. AMQP Setup: RabbitMQ integration with Kourier client
  4. Routes: Type-safe HTTP endpoints with Ktor
  5. Koin DI: Wiring all components together
  6. Testing: Running and verifying the complete system

The result is a production-ready, native-compiled backend that:

Congratulations on completing the tutorial!