Skip to content

Commit fb9a80e

Browse files
committed
reworking examples using spiral and RR
1 parent 67ce873 commit fb9a80e

File tree

81 files changed

+15345
-993
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+15345
-993
lines changed

Dockerfile

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
FROM dunglas/frankenphp:php8.5.1
1+
FROM spiralscout/roadrunner as roadrunner
2+
# OR
3+
# FROM ghcr.io/roadrunner-server/roadrunner as roadrunner
4+
5+
FROM php:8.5-cli
26

37
WORKDIR /app
48

5-
# Install PHP extensions
6-
RUN install-php-extensions @composer redis memcached pcov
9+
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
10+
RUN install-php-extensions @composer redis memcached pcov sockets mbstring
711

812
COPY . /app
913

10-
RUN composer install --no-interaction --prefer-dist --ignore-platform-req=ext-zookeeper --ignore-platform-req=ext-memcached
14+
RUN composer install --no-interaction --prefer-dist --ignore-platform-reqs
15+
16+
COPY --from=roadrunner /usr/bin/rr /usr/local/bin/rr
17+
18+
CMD ["rr", "serve"]

Taskfile.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
version: '3'
22

33
tasks:
4+
bash:
5+
desc: Opens a bash shell in the container
6+
cmds:
7+
- docker compose exec php bash
8+
49
test:
510
desc: Runs all tests
611
cmds:

compose.yaml

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,33 @@
11
services:
2+
gateway:
3+
image: caddy:2-alpine
4+
volumes:
5+
- ./examples/Caddyfile:/etc/caddy/Caddyfile:ro
6+
ports:
7+
- "80:80"
8+
depends_on:
9+
- php
10+
- mercure
11+
212
php:
313
build:
414
context: .
515
dockerfile: Dockerfile
616
working_dir: /app
7-
volumes:
8-
- ./:/app
9-
- ./Caddyfile.dev:/etc/frankenphp/Caddyfile:ro
10-
- caddy_data:/data
11-
- caddy_config:/config
1217
environment:
1318
REDIS_HOST: redis
1419
REDIS_PORT: 6379
15-
SERVER_ROOT: /app/examples/public
1620
MERCURE_HUB_URL: http://mercure/.well-known/mercure
1721
MERCURE_JWT_SECRET: ${MERCURE_JWT_SECRET:-airlock-mercure-secret-32chars-minimum}
1822
MERCURE_PUBLIC_URL: ${MERCURE_PUBLIC_URL:-http://localhost/.well-known/mercure}
19-
env_file:
20-
- .env
21-
tty: true
23+
volumes:
24+
- ./examples:/app
2225
depends_on:
2326
- redis
2427
- mercure
25-
ports:
26-
- "80:80" # HTTP
2728

2829
mercure:
2930
image: dunglas/mercure
30-
ports:
31-
- "3000:80"
3231
environment:
3332
SERVER_NAME: :80
3433
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET:-airlock-mercure-secret-32chars-minimum}
@@ -40,8 +39,6 @@ services:
4039

4140
redis:
4241
image: redis:8.4.0
43-
ports:
44-
- "6379:6379"
4542

4643
e2e:
4744
build:
@@ -51,14 +48,10 @@ services:
5148
volumes:
5249
- ./e2e:/app/e2e
5350
environment:
54-
BASE_URL: http://php
51+
BASE_URL: http://gateway
5552
env_file:
5653
- .env
5754
depends_on:
58-
- redis
55+
- gateway
5956
command: >
6057
sh -c "npm install && npx playwright test"
61-
62-
volumes:
63-
caddy_data:
64-
caddy_config:

e2e/tests/01-lock.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,28 @@ test.describe.serial('01-global-lock - Double-Click Protection', () => {
1010
});
1111

1212
test('should display initial state correctly', async ({page}) => {
13-
await page.goto('/global-lock/');
13+
await page.goto(`${BASE_URL}/global-lock/`);
1414

15-
await expect(page.locator('h1')).toHaveText('Airlock: Double-Click Protection');
15+
await expect(page.locator('h1')).toHaveText('Double-Click Protection');
1616
await expect(page.locator('#go')).toBeEnabled();
1717
await expect(page.locator('#status')).toBeEmpty();
1818
});
1919

2020
test('should process action and show done state', async ({page}) => {
21-
await page.goto('/global-lock/');
21+
await page.goto(`${BASE_URL}/global-lock/`);
2222

2323
await page.locator('#go').click();
2424

2525
// Should show submitting, then running
2626
await expect(page.locator('#status')).toContainText('Processing');
2727

2828
// Wait for completion (5s work + buffer)
29-
await expect(page.locator('#status')).toContainText('Done', {timeout: 5000});
29+
await expect(page.locator('#status')).toContainText('Done', {timeout: 11000});
3030
await expect(page.locator('#status')).toHaveClass(/ok/);
3131
});
3232

3333
test('should block double-click with instant rejection', async ({page}) => {
34-
await page.goto('/global-lock/');
34+
await page.goto(`${BASE_URL}/global-lock/`);
3535

3636
// First click - starts processing
3737
await page.locator('#go').click();
@@ -52,12 +52,12 @@ test.describe.serial('01-global-lock - Double-Click Protection', () => {
5252

5353
try {
5454
// First user starts action
55-
await page1.goto('/global-lock/');
55+
await page1.goto(`${BASE_URL}/global-lock/`);
5656
await page1.locator('#go').click();
5757
await expect(page1.locator('#status')).toContainText('Processing');
5858

5959
// Second user tries to start - should be instantly blocked
60-
await page2.goto('/global-lock/');
60+
await page2.goto(`${BASE_URL}/global-lock/`);
6161
await page2.locator('#go').click();
6262
await expect(page2.locator('#status')).toContainText('Already processing');
6363
await expect(page2.locator('#status')).toHaveClass(/error/);
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {test, expect} from '@playwright/test';
2+
3+
const BASE_URL = (process.env.BASE_URL || 'http://localhost').replace(/\/+$/, '');
4+
const SUCCESS_PATH = /\/redis-lottery-queue\/success\/?$/;
5+
6+
test.describe.serial('redis-lottery-queue - Redis Lottery Queue', () => {
7+
test.beforeEach(async ({request}) => {
8+
// Clear all Redis keys for this example
9+
const response = await request.get(`${BASE_URL}/reset`);
10+
console.log('Reset response:', await response.json());
11+
});
12+
13+
test('should display initial state correctly', async ({page}) => {
14+
await page.goto(`${BASE_URL}/redis-lottery-queue/`);
15+
16+
await expect(page.locator('h1')).toHaveText('Lottery Queue');
17+
await expect(page.locator('#go')).toBeEnabled();
18+
await expect(page.locator('#reset')).toBeEnabled();
19+
await expect(page.locator('#status')).toBeEmpty();
20+
});
21+
22+
test('should allow one user in immediately', async ({page}) => {
23+
await page.goto(`${BASE_URL}/redis-lottery-queue/`);
24+
25+
await page.locator('#go').click();
26+
27+
// Should show you got in immediately
28+
await expect(page.locator('#status')).toContainText('You got in immediately! Redirecting...');
29+
30+
// Wait for redirect and check page title
31+
await expect(page).toHaveURL(SUCCESS_PATH);
32+
await expect(page.locator('h1')).toHaveText('You Made It!');
33+
});
34+
35+
test('should allow one user in and make one user wait', async ({browser}) => {
36+
const context1 = await browser.newContext();
37+
const context2 = await browser.newContext();
38+
const page1 = await context1.newPage();
39+
const page2 = await context2.newPage();
40+
41+
try {
42+
// First user gets in
43+
await page1.goto(`${BASE_URL}/redis-lottery-queue/`);
44+
await page1.locator('#go').click();
45+
await expect(page1.locator('#status')).toContainText('You got in immediately!');
46+
await expect(page1).toHaveURL(SUCCESS_PATH);
47+
48+
// Second user has to wait
49+
await page2.goto(`${BASE_URL}/redis-lottery-queue/`);
50+
await page2.locator('#go').click();
51+
await expect(page2.locator('#status')).toContainText('Waiting in lottery queue...');
52+
53+
// Release explicitly from user 1's session instead of relying on the 5s client timer.
54+
const releaseResponse = await context1.request.post(`${BASE_URL}/redis-lottery-queue/release`);
55+
expect(releaseResponse.ok()).toBeTruthy();
56+
57+
// Second user redirected to the success page
58+
await expect(page2.locator('#status')).toContainText('Your turn! Redirecting...', {timeout: 10000});
59+
await expect(page2).toHaveURL(SUCCESS_PATH, {timeout: 5000});
60+
await expect(page2.locator('h1')).toHaveText('You Made It!');
61+
} finally {
62+
await context1.close();
63+
await context2.close();
64+
}
65+
});
66+
67+
/*test('second user should be blocked when lock is held', async ({browser}) => {
68+
// Create two browser contexts (simulating two users)
69+
const context1 = await browser.newContext();
70+
const context2 = await browser.newContext();
71+
const page1 = await context1.newPage();
72+
const page2 = await context2.newPage();
73+
74+
try {
75+
// First user starts action
76+
await page1.goto('/global-lock/');
77+
await page1.locator('#go').click();
78+
await expect(page1.locator('#status')).toContainText('Processing');
79+
80+
// Second user tries to start - should be instantly blocked
81+
await page2.goto('/global-lock/');
82+
await page2.locator('#go').click();
83+
await expect(page2.locator('#status')).toContainText('Already processing');
84+
await expect(page2.locator('#status')).toHaveClass(/error/);
85+
86+
// Wait for first user to complete
87+
await expect(page1.locator('#status')).toContainText('Done', {timeout: 5000});
88+
89+
// Second user can now start
90+
await page2.locator('#go').click();
91+
await expect(page2.locator('#status')).toContainText('Processing');
92+
} finally {
93+
await context1.close();
94+
await context2.close();
95+
}
96+
});*!/*/
97+
});

examples/.editorconfig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
insert_final_newline = true
7+
indent_style = space
8+
indent_size = 4
9+
trim_trailing_whitespace = true
10+
11+
[*.yml]
12+
indent_size = 2
13+
14+
[*.yaml]
15+
indent_size = 2

examples/.env.sample

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Environment (prod or local)
2+
APP_ENV=local
3+
4+
# Debug mode set to TRUE disables view caching and enables higher verbosity
5+
DEBUG=true
6+
7+
# Verbosity level
8+
VERBOSITY_LEVEL=verbose # basic, verbose, debug
9+
10+
# Set to an application specific value, used to encrypt/decrypt cookies etc
11+
ENCRYPTER_KEY={encrypt-key}
12+
13+
# Monolog
14+
MONOLOG_DEFAULT_CHANNEL=default # Use "roadrunner" channel if you want to use RoadRunner logger
15+
MONOLOG_DEFAULT_LEVEL=DEBUG # DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY
16+
17+
# Queue
18+
QUEUE_CONNECTION=in-memory
19+
20+
# Cache
21+
CACHE_STORAGE=rr-local
22+
23+
# Telemetry
24+
TELEMETRY_DRIVER=null
25+
26+
# Tokenizer
27+
TOKENIZER_CACHE_TARGETS=false
28+
TOKENIZER_LOAD_CLASSES=true
29+
TOKENIZER_LOAD_ENUMS=true
30+
TOKENIZER_LOAD_INTERFACES=true
31+
32+
# View component options
33+
VIEW_CACHE=false
34+
35+
# Session
36+
SESSION_LIFETIME=86400
37+
SESSION_COOKIE=sid
38+
39+
# Authorization
40+
AUTH_TOKEN_TRANSPORT=cookie
41+
AUTH_TOKEN_STORAGE=session

examples/.gitignore

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Symfony cache
2-
var/cache/
1+
/.*/
2+
!/.github
3+
/vendor
4+
/runtime/
5+
*/runtime/
36

4-
# Composer dependencies
5-
vendor/
6-
composer.lock
7+
/rr*
8+
/protoc-gen-php-grpc*
79

8-
# IDE
9-
/.idea
10-
11-
/config/reference.php
10+
/.env
11+
/*.cache

examples/.php-cs-fixer.dist.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
require_once 'vendor/autoload.php';
6+
7+
return \Spiral\CodeStyle\Builder::create()
8+
->include(__DIR__)
9+
->include(__FILE__)
10+
->cache('./runtime/php-cs-fixer.cache')
11+
->allowRisky()
12+
->build();

examples/.rr.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
version: '3'
2+
rpc:
3+
listen: 'tcp://127.0.0.1:6001'
4+
http:
5+
address: '0.0.0.0:8080'
6+
middleware:
7+
- gzip
8+
- static
9+
static:
10+
dir: public
11+
forbid:
12+
- .php
13+
- .htaccess
14+
pool:
15+
debug: true
16+
num_workers: 1
17+
supervisor:
18+
max_worker_memory: 100
19+
server:
20+
command: 'php app.php'
21+
relay: pipes
22+
kv:
23+
local:
24+
driver: memory
25+
config:
26+
interval: 60
27+
jobs:
28+
pool:
29+
num_workers: 2
30+
max_worker_memory: 100
31+
consume: { }

0 commit comments

Comments
 (0)