ru.Bee 2.7.2 → 2.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/config.ru +1 -1
- data/lib/db/test.db +0 -0
- data/lib/rubee/async/sidekiq_async.rb +40 -7
- data/lib/rubee.rb +5 -5
- data/readme.md +315 -16
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2661a78ea4d5fe99234d85375b48b2020bee244951103f0514f51ad727804824
|
|
4
|
+
data.tar.gz: 581395ecfbce635f11b6647ee1c25eadea0a68ae737b9bb1b4f88584af33d66b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5175ca33f398ea99103fae92e84f83dd23926ce3ab1074b95cd00a98ef1515a1c0c56635f71898a732cfc55cd3bf70630a47150cd915af04a7b31ae174b22d25
|
|
7
|
+
data.tar.gz: 215401fd826cc1e003a6cfec1619ca8704370d319eae717d874f27a48c13506465aeffb8572b4f3c0ff8decbc1fc19e5b727891ac8b6598d6330ce35714379ba
|
data/lib/config.ru
CHANGED
data/lib/db/test.db
CHANGED
|
Binary file
|
|
@@ -1,15 +1,48 @@
|
|
|
1
1
|
module Rubee
|
|
2
2
|
class SidekiqAsync
|
|
3
3
|
def perform_async(**args)
|
|
4
|
-
options =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
options = serialize_options(args[:options])
|
|
5
|
+
args[:_class].perform_async(*options)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def perform_at(interval, **args)
|
|
9
|
+
options = serialize_options(args[:options])
|
|
10
|
+
args[:_class].perform_at(interval, *options)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def perform_in(interval, **args)
|
|
14
|
+
options = serialize_options(args[:options])
|
|
15
|
+
args[:_class].perform_in(interval, *options)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def perform_later(interval, **args)
|
|
19
|
+
perform_in(interval, **args)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def perform_bulk(jobs_args)
|
|
23
|
+
jobs_args.map! do |args|
|
|
24
|
+
options = serialize_options(args[:options])
|
|
25
|
+
{ args: options }
|
|
10
26
|
end
|
|
11
27
|
|
|
12
|
-
args[:_class].
|
|
28
|
+
args[:_class].perform_bulk(jobs_args)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def set(options, **args)
|
|
32
|
+
serialized_options = serialize_options(args[:options])
|
|
33
|
+
args[:_class].set(options).perform_async(*serialized_options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def serialize_options(options)
|
|
39
|
+
if options.is_a?(Hash)
|
|
40
|
+
[JSON.generate(options)]
|
|
41
|
+
elsif options.is_a?(Array)
|
|
42
|
+
options
|
|
43
|
+
else
|
|
44
|
+
[options]
|
|
45
|
+
end
|
|
13
46
|
end
|
|
14
47
|
end
|
|
15
48
|
end
|
data/lib/rubee.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Rubee
|
|
|
20
20
|
RUBEE_SUPPORT = { "Rubee::Support::Hash" => Hash, "Rubee::Support::String" => String }
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
VERSION = '2.7.
|
|
23
|
+
VERSION = '2.7.4'
|
|
24
24
|
|
|
25
25
|
require_relative 'rubee/router'
|
|
26
26
|
require_relative 'rubee/logger'
|
|
@@ -33,9 +33,11 @@ module Rubee
|
|
|
33
33
|
include Singleton
|
|
34
34
|
using(ChargedString)
|
|
35
35
|
|
|
36
|
-
def
|
|
37
|
-
# autoload rb files
|
|
36
|
+
def initialize
|
|
38
37
|
Autoload.call
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call(env)
|
|
39
41
|
# init rack request
|
|
40
42
|
request = Rack::Request.new(env)
|
|
41
43
|
# Add default path for assets
|
|
@@ -64,8 +66,6 @@ module Rubee
|
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
def middlewares
|
|
67
|
-
Autoload.call
|
|
68
|
-
|
|
69
69
|
Rubee::Configuration.middlewares
|
|
70
70
|
end
|
|
71
71
|
|
data/readme.md
CHANGED
|
@@ -91,6 +91,8 @@ The comparison is based on generic and subjective information available on the i
|
|
|
91
91
|
- [Rubee::Support](#rubee-support)
|
|
92
92
|
- [Testing](#testing)
|
|
93
93
|
- [Background jobs](#background-jobs)
|
|
94
|
+
- [Sidekiq engine](#sidekiq-engine)
|
|
95
|
+
- [ThreadAsync engine](#threadasync-engine)
|
|
94
96
|
- [Modular application](#modular-application)
|
|
95
97
|
- [Logger](#logger)
|
|
96
98
|
- [WebSocket](#websocket)
|
|
@@ -1240,14 +1242,24 @@ rubee test models/user_model_test.rb --line=12 # run a specific line
|
|
|
1240
1242
|
|
|
1241
1243
|
## Background jobs
|
|
1242
1244
|
|
|
1243
|
-
|
|
1245
|
+
There are currently two ways to integrate background jobs into your application:
|
|
1244
1246
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
+
- [Sidekiq](#sidekiq-engine)
|
|
1248
|
+
- [ThreadAsync](#threadasync-engine)
|
|
1249
|
+
|
|
1250
|
+
## Sidekiq Engine
|
|
1251
|
+
|
|
1252
|
+
## Installation & Setup
|
|
1253
|
+
|
|
1254
|
+
### 1. Add Sidekiq to your Gemfile
|
|
1255
|
+
|
|
1256
|
+
```ruby
|
|
1247
1257
|
gem 'sidekiq'
|
|
1258
|
+
gem 'rack-session' # Required for Sidekiq Web UI
|
|
1248
1259
|
```
|
|
1249
1260
|
|
|
1250
|
-
2. Configure the adapter for the desired environment
|
|
1261
|
+
### 2. Configure the adapter for the desired environment
|
|
1262
|
+
|
|
1251
1263
|
```ruby
|
|
1252
1264
|
# config/base_configuration.rb
|
|
1253
1265
|
Rubee::Configuration.setup(env = :development) do |config|
|
|
@@ -1256,20 +1268,32 @@ Rubee::Configuration.setup(env = :development) do |config|
|
|
|
1256
1268
|
end
|
|
1257
1269
|
```
|
|
1258
1270
|
|
|
1259
|
-
3. Install dependencies
|
|
1271
|
+
### 3. Install dependencies
|
|
1272
|
+
|
|
1260
1273
|
```bash
|
|
1261
1274
|
bundle install
|
|
1262
1275
|
```
|
|
1263
1276
|
|
|
1264
|
-
4. Start Redis
|
|
1277
|
+
### 4. Start Redis
|
|
1278
|
+
|
|
1279
|
+
Redis must be running before starting Sidekiq.
|
|
1280
|
+
|
|
1265
1281
|
```bash
|
|
1282
|
+
# Start Redis server
|
|
1266
1283
|
redis-server
|
|
1284
|
+
|
|
1285
|
+
# Or in background (macOS with Homebrew)
|
|
1286
|
+
brew services start redis
|
|
1287
|
+
|
|
1288
|
+
# Verify Redis is running
|
|
1289
|
+
redis-cli ping
|
|
1290
|
+
# Should respond: PONG
|
|
1267
1291
|
```
|
|
1268
1292
|
|
|
1269
|
-
5. Add
|
|
1293
|
+
### 5. Add Sidekiq configuration file
|
|
1294
|
+
|
|
1270
1295
|
```yaml
|
|
1271
1296
|
# config/sidekiq.yml
|
|
1272
|
-
|
|
1273
1297
|
development:
|
|
1274
1298
|
redis: redis://localhost:6379/0
|
|
1275
1299
|
concurrency: 5
|
|
@@ -1279,29 +1303,302 @@ development:
|
|
|
1279
1303
|
high:
|
|
1280
1304
|
```
|
|
1281
1305
|
|
|
1282
|
-
6. Create
|
|
1306
|
+
### 6. Create Sidekiq boot file
|
|
1307
|
+
|
|
1283
1308
|
```ruby
|
|
1284
|
-
#
|
|
1285
|
-
require_relative 'extensions/asyncable' unless defined? Asyncable
|
|
1309
|
+
# inits/sidekiq.rb
|
|
1286
1310
|
|
|
1311
|
+
# Configure Redis connection
|
|
1312
|
+
Sidekiq.configure_server do |config|
|
|
1313
|
+
config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') }
|
|
1314
|
+
end
|
|
1315
|
+
|
|
1316
|
+
Sidekiq.configure_client do |config|
|
|
1317
|
+
config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') }
|
|
1318
|
+
end
|
|
1319
|
+
|
|
1320
|
+
# Load Rubee application context
|
|
1321
|
+
unless Object.const_defined?('Rubee')
|
|
1322
|
+
require 'rubee'
|
|
1323
|
+
|
|
1324
|
+
# Load environment variables from the Ruby file.
|
|
1325
|
+
require_relative 'dev.rb' if File.exist?(File.join(__dir__, 'dev.rb'))
|
|
1326
|
+
|
|
1327
|
+
# Trigger Rubee autoload
|
|
1328
|
+
Rubee::Autoload.call
|
|
1329
|
+
end
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
### 7. Create a Sidekiq worker
|
|
1333
|
+
|
|
1334
|
+
```ruby
|
|
1335
|
+
# app/workers/test_async_runner.rb
|
|
1287
1336
|
class TestAsyncRunner
|
|
1288
1337
|
include Rubee::Asyncable
|
|
1289
1338
|
include Sidekiq::Worker
|
|
1290
1339
|
|
|
1291
|
-
sidekiq_options queue: :default
|
|
1340
|
+
sidekiq_options queue: :default, retry: 3
|
|
1292
1341
|
|
|
1293
1342
|
def perform(options)
|
|
1294
|
-
|
|
1343
|
+
options = parse_options(options)
|
|
1344
|
+
|
|
1345
|
+
User.create(
|
|
1346
|
+
email: options['email'],
|
|
1347
|
+
password: options['password']
|
|
1348
|
+
)
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
private
|
|
1352
|
+
|
|
1353
|
+
def parse_options(options)
|
|
1354
|
+
return options unless options.is_a?(String)
|
|
1355
|
+
|
|
1356
|
+
begin
|
|
1357
|
+
JSON.parse(options)
|
|
1358
|
+
rescue JSON::ParserError
|
|
1359
|
+
options
|
|
1360
|
+
end
|
|
1295
1361
|
end
|
|
1296
1362
|
end
|
|
1297
1363
|
```
|
|
1298
1364
|
|
|
1299
|
-
|
|
1365
|
+
### 8. Use it in your codebase
|
|
1366
|
+
|
|
1300
1367
|
```ruby
|
|
1301
|
-
|
|
1368
|
+
|
|
1369
|
+
TestAsyncRunner.new.perform_async(
|
|
1370
|
+
"email" => "new@new.com",
|
|
1371
|
+
"password" => "123"
|
|
1372
|
+
)
|
|
1373
|
+
```
|
|
1374
|
+
|
|
1375
|
+
---
|
|
1376
|
+
|
|
1377
|
+
## Running Sidekiq
|
|
1378
|
+
|
|
1379
|
+
### Start Sidekiq (Foreground)
|
|
1380
|
+
|
|
1381
|
+
```bash
|
|
1382
|
+
bundle exec sidekiq -C config/sidekiq.yml -r ./inits/sidekiq.rb
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
### Start Sidekiq (Background/Daemon)
|
|
1386
|
+
|
|
1387
|
+
```bash
|
|
1388
|
+
# Start as daemon
|
|
1389
|
+
bundle exec sidekiq -d -C config/sidekiq.yml -r ./inits/sidekiq.rb
|
|
1390
|
+
|
|
1391
|
+
# Stop daemon
|
|
1392
|
+
kill -TERM $(cat tmp/pids/sidekiq.pid)
|
|
1393
|
+
|
|
1394
|
+
# View logs
|
|
1395
|
+
tail -f log/sidekiq.log
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
### Helper Scripts
|
|
1399
|
+
|
|
1400
|
+
Create convenient management scripts:
|
|
1401
|
+
|
|
1402
|
+
```bash
|
|
1403
|
+
# bin/sidekiq_start
|
|
1404
|
+
#!/bin/bash
|
|
1405
|
+
bundle exec sidekiq -d \
|
|
1406
|
+
-C config/sidekiq.yml \
|
|
1407
|
+
-r ./inits/sidekiq.rb \
|
|
1408
|
+
```
|
|
1409
|
+
|
|
1410
|
+
```bash
|
|
1411
|
+
# bin/sidekiq_stop
|
|
1412
|
+
#!/bin/bash
|
|
1413
|
+
if [ -f tmp/pids/sidekiq.pid ]; then
|
|
1414
|
+
kill -TERM $(cat tmp/pids/sidekiq.pid)
|
|
1415
|
+
rm tmp/pids/sidekiq.pid
|
|
1416
|
+
echo "✓ Sidekiq stopped"
|
|
1417
|
+
else
|
|
1418
|
+
echo "✗ Sidekiq is not running"
|
|
1419
|
+
fi
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
Make them executable:
|
|
1423
|
+
```bash
|
|
1424
|
+
chmod +x bin/sidekiq_start bin/sidekiq_stop
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
---
|
|
1428
|
+
|
|
1429
|
+
## Enable Sidekiq Web Dashboard
|
|
1430
|
+
|
|
1431
|
+
### 1. Create Sidekiq middleware
|
|
1432
|
+
|
|
1433
|
+
```ruby
|
|
1434
|
+
# inits/middlewares/sidekiq_middleware.rb
|
|
1435
|
+
require 'sidekiq/web'
|
|
1436
|
+
require 'rack/session'
|
|
1437
|
+
|
|
1438
|
+
class SidekiqMiddleware
|
|
1439
|
+
def initialize(app)
|
|
1440
|
+
@app = app
|
|
1441
|
+
|
|
1442
|
+
# Get or generate session secret
|
|
1443
|
+
session_secret = ENV.fetch('SESSION_SECRET') { generate_secret }
|
|
1444
|
+
|
|
1445
|
+
# Build Sidekiq Web app with authentication
|
|
1446
|
+
@sidekiq_app = Rack::Builder.new do
|
|
1447
|
+
# Session support (required for CSRF protection)
|
|
1448
|
+
use Rack::Session::Cookie,
|
|
1449
|
+
secret: session_secret,
|
|
1450
|
+
same_site: true,
|
|
1451
|
+
max_age: 86400
|
|
1452
|
+
|
|
1453
|
+
# Basic authentication
|
|
1454
|
+
use Rack::Auth::Basic, "Sidekiq Dashboard" do |username, password|
|
|
1455
|
+
username == ENV.fetch('SIDEKIQ_USERNAME', 'admin') &&
|
|
1456
|
+
password == ENV.fetch('SIDEKIQ_PASSWORD', 'password')
|
|
1457
|
+
end
|
|
1458
|
+
|
|
1459
|
+
run Sidekiq::Web
|
|
1460
|
+
end
|
|
1461
|
+
end
|
|
1462
|
+
|
|
1463
|
+
def call(env)
|
|
1464
|
+
if env['PATH_INFO'].start_with?('/sidekiq')
|
|
1465
|
+
# Route to Sidekiq Web UI
|
|
1466
|
+
env['SCRIPT_NAME'] = '/sidekiq'
|
|
1467
|
+
env['PATH_INFO'] = env['PATH_INFO'].sub(%r{^/sidekiq}, '') || '/'
|
|
1468
|
+
@sidekiq_app.call(env)
|
|
1469
|
+
else
|
|
1470
|
+
# Pass through to main app
|
|
1471
|
+
@app.call(env)
|
|
1472
|
+
end
|
|
1473
|
+
end
|
|
1474
|
+
|
|
1475
|
+
private
|
|
1476
|
+
|
|
1477
|
+
def generate_secret
|
|
1478
|
+
secret_file = '.session.key'
|
|
1479
|
+
|
|
1480
|
+
if File.exist?(secret_file)
|
|
1481
|
+
File.read(secret_file).strip
|
|
1482
|
+
else
|
|
1483
|
+
require 'securerandom'
|
|
1484
|
+
secret = SecureRandom.hex(64)
|
|
1485
|
+
File.write(secret_file, secret)
|
|
1486
|
+
puts "Generated new session secret in #{secret_file}"
|
|
1487
|
+
secret
|
|
1488
|
+
end
|
|
1489
|
+
end
|
|
1490
|
+
end
|
|
1491
|
+
```
|
|
1492
|
+
|
|
1493
|
+
### 2. Access the dashboard
|
|
1494
|
+
|
|
1495
|
+
Start your Rubee application and visit /sidekiq:
|
|
1496
|
+
```
|
|
1497
|
+
http://localhost:7000/sidekiq
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
Login with credentials from your `/inits/dev.rb` file for developmet purposes.
|
|
1501
|
+
|
|
1502
|
+
---
|
|
1503
|
+
|
|
1504
|
+
## Worker Examples
|
|
1505
|
+
|
|
1506
|
+
### Worker with Database Records
|
|
1507
|
+
|
|
1508
|
+
```ruby
|
|
1509
|
+
# app/workers/booking_confirmation_worker.rb
|
|
1510
|
+
class BookingConfirmationWorker
|
|
1511
|
+
include Rubee::Asyncable
|
|
1512
|
+
include Sidekiq::Worker
|
|
1513
|
+
|
|
1514
|
+
sidekiq_options queue: :mailers, retry: 3
|
|
1515
|
+
|
|
1516
|
+
def perform(options)
|
|
1517
|
+
options = parse_options(options)
|
|
1518
|
+
|
|
1519
|
+
# Fetch records from database
|
|
1520
|
+
service = Service.find(options['service_id'])
|
|
1521
|
+
time_slot = TimeSlot.find(options['time_slot_id'])
|
|
1522
|
+
|
|
1523
|
+
Mailer.booking_confirmation(
|
|
1524
|
+
to: options['to'],
|
|
1525
|
+
client_name: options['client_name'],
|
|
1526
|
+
service: service,
|
|
1527
|
+
time_slot: time_slot
|
|
1528
|
+
)
|
|
1529
|
+
end
|
|
1530
|
+
|
|
1531
|
+
private
|
|
1532
|
+
|
|
1533
|
+
def parse_options(options)
|
|
1534
|
+
return options unless options.is_a?(String)
|
|
1535
|
+
JSON.parse(options) rescue options
|
|
1536
|
+
end
|
|
1537
|
+
end
|
|
1538
|
+
|
|
1539
|
+
# Usage
|
|
1540
|
+
BookingConfirmationWorker.new.perform_async(options: {
|
|
1541
|
+
"to" => "client@example.com",
|
|
1542
|
+
"client_name" => "John Doe",
|
|
1543
|
+
"service_id" => 15,
|
|
1544
|
+
"time_slot_id" => 91
|
|
1545
|
+
})
|
|
1302
1546
|
```
|
|
1303
1547
|
|
|
1304
|
-
|
|
1548
|
+
---
|
|
1549
|
+
## Monitoring & Troubleshooting
|
|
1550
|
+
|
|
1551
|
+
### Check Sidekiq Status
|
|
1552
|
+
|
|
1553
|
+
```bash
|
|
1554
|
+
# View running processes
|
|
1555
|
+
ps aux | grep sidekiq
|
|
1556
|
+
|
|
1557
|
+
# Check Redis connection
|
|
1558
|
+
redis-cli ping
|
|
1559
|
+
|
|
1560
|
+
# View queue sizes
|
|
1561
|
+
redis-cli LLEN queue:default
|
|
1562
|
+
```
|
|
1563
|
+
|
|
1564
|
+
### Common Issues
|
|
1565
|
+
|
|
1566
|
+
**Workers not processing:**
|
|
1567
|
+
- Ensure Redis is running: `redis-cli ping`
|
|
1568
|
+
- Check Sidekiq is started: `ps aux | grep sidekiq`
|
|
1569
|
+
- Verify queue names match in worker and config
|
|
1570
|
+
|
|
1571
|
+
**Authentication errors on Web UI:**
|
|
1572
|
+
- Ensure `rack-session` gem is installed
|
|
1573
|
+
- Check SESSION_SECRET is at least 64 bytes
|
|
1574
|
+
- Verify SIDEKIQ_USERNAME and SIDEKIQ_PASSWORD are set
|
|
1575
|
+
|
|
1576
|
+
**Jobs failing:**
|
|
1577
|
+
- Check `log/sidekiq.log` for errors
|
|
1578
|
+
- View failed jobs in Web UI at `/sidekiq/retries`
|
|
1579
|
+
- Verify environment variables are loaded in `inits/sidekiq.rb`
|
|
1580
|
+
|
|
1581
|
+
---
|
|
1582
|
+
|
|
1583
|
+
## Best Practices
|
|
1584
|
+
|
|
1585
|
+
1. **Pass IDs, not objects** - Use `booking.id`, not `booking` itself
|
|
1586
|
+
2. **Keep jobs small** - One job should do one thing
|
|
1587
|
+
3. **Make jobs idempotent** - Safe to run multiple times
|
|
1588
|
+
4. **Set appropriate retries** - Critical: more retries, notifications: fewer
|
|
1589
|
+
5. **Use different queues** - Separate critical from low-priority jobs
|
|
1590
|
+
6. **Handle JSON properly** - Always parse options in `perform` method
|
|
1591
|
+
7. **Monitor your queues** - Use Web UI to watch for backlogs
|
|
1592
|
+
|
|
1593
|
+
---
|
|
1594
|
+
|
|
1595
|
+
## Additional Resources
|
|
1596
|
+
|
|
1597
|
+
- [Sidekiq Official Docs](https://github.com/sidekiq/sidekiq/wiki)
|
|
1598
|
+
- [Best Practices](https://github.com/sidekiq/sidekiq/wiki/Best-Practices)
|
|
1599
|
+
- [Error Handling](https://github.com/sidekiq/sidekiq/wiki/Error-Handling)
|
|
1600
|
+
|
|
1601
|
+
### ThreadAsync engine
|
|
1305
1602
|
|
|
1306
1603
|
The default adapter is `ThreadAsync`. It is not yet recommended for production — use with caution.
|
|
1307
1604
|
|
|
@@ -1578,6 +1875,8 @@ end
|
|
|
1578
1875
|
2. Register the middleware in the `config/base_configuration.rb`
|
|
1579
1876
|
```ruby
|
|
1580
1877
|
# config/base_configuration.rb
|
|
1878
|
+
require_relative 'inits/middlewares/my_middleware'
|
|
1879
|
+
|
|
1581
1880
|
config.middlewares = { middlewares: [MyMiddleware], env: }
|
|
1582
1881
|
```
|
|
1583
1882
|
|