tep 0.11.4 → 0.11.6

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.
data/lib/tep/pg.rb CHANGED
@@ -197,11 +197,10 @@ module PG
197
197
  h = Pg.tep_pg_connect(opts)
198
198
  end
199
199
  else
200
- # Hash form. Pack keys and values into parallel \0-delimited
201
- # buffers; the shim splits them apart and calls
202
- # PQconnectdbParams. (No async-connect path for the Hash
203
- # form yet -- AR uses the String form for connect, so the
204
- # Scheduled-context shortcut points only at conninfo.)
200
+ # Hash-conninfo form: pack the key/value pairs into NUL-delimited
201
+ # buffers for the C shim. (`opts` narrows to Hash in this
202
+ # is_a?(String) ELSE branch -- the narrowing gap that blocked the
203
+ # re-pin, matz/spinel#1434, is fixed as of the SPINEL_PIN bump.)
205
204
  keys = ""
206
205
  vals = ""
207
206
  n = 0
@@ -1126,3 +1125,443 @@ module PG
1126
1125
  end
1127
1126
  end
1128
1127
  end
1128
+
1129
+ # ===================================================================
1130
+ # Opt-in PG backend overrides (#216). Loaded only via `require "tep/pg"`.
1131
+ # These REDEFINE the no-op hooks in core Broadcast/Presence (last-
1132
+ # definition-wins) so a PG app gets the real LISTEN/NOTIFY + mirror
1133
+ # behavior, while a non-PG app keeps the no-ops and DCEs libpq.
1134
+ # ===================================================================
1135
+ module Tep
1136
+ module Broadcast
1137
+ # Cross-worker NOTIFY override (#216). Replaces the core no-op so a
1138
+ # `require "tep/pg"` app fans publishes out to other workers over the
1139
+ # LISTEN/NOTIFY channel. Mirrors the pre-#216 inline branch in
1140
+ # Broadcast.publish.
1141
+ def self.cross_worker_notify(topic, payload)
1142
+ if Tep::APP.broadcast_pg_enabled != 0
1143
+ wire = Tep::Broadcast.encode_wire(topic, payload)
1144
+ Tep::APP.broadcast_pg_conn.notify(
1145
+ Tep::APP.broadcast_pg_channel, wire)
1146
+ end
1147
+ 0
1148
+ end
1149
+
1150
+ def self.enable_pg_backend(conninfo, channel)
1151
+ conn = PG::Connection.new(conninfo)
1152
+ if conn.pgh < 0
1153
+ return -1
1154
+ end
1155
+ if conn.listen(channel) < 0
1156
+ return -1
1157
+ end
1158
+ Tep::APP.set_broadcast_pg_conn(conn)
1159
+ Tep::APP.set_broadcast_pg_channel(channel)
1160
+ Tep::APP.set_broadcast_pg_enabled(1)
1161
+ 0
1162
+ end
1163
+
1164
+ def self.disable_pg_backend
1165
+ if Tep::APP.broadcast_pg_enabled == 0
1166
+ return 0
1167
+ end
1168
+ Tep::APP.broadcast_pg_conn.unlisten(Tep::APP.broadcast_pg_channel)
1169
+ Tep::APP.broadcast_pg_conn.finish
1170
+ Tep::APP.set_broadcast_pg_enabled(0)
1171
+ 0
1172
+ end
1173
+
1174
+ def self.poll_pg_once(timeout_ms)
1175
+ if Tep::APP.broadcast_pg_enabled == 0
1176
+ return -1
1177
+ end
1178
+ r = Tep::APP.broadcast_pg_conn.poll_notification(timeout_ms)
1179
+ if r != 1
1180
+ return r
1181
+ end
1182
+ wire = Tep::APP.broadcast_pg_conn.last_notify_payload
1183
+ Tep::Broadcast.deliver_wire_local(wire)
1184
+ 1
1185
+ end
1186
+
1187
+ def self.encode_wire(topic, payload)
1188
+ topic.length.to_s + ":" + topic + payload
1189
+ end
1190
+
1191
+ def self.deliver_wire_local(wire)
1192
+ colon = Tep.str_find(wire, ":", 0)
1193
+ if colon <= 0
1194
+ return -1
1195
+ end
1196
+ len_str = wire[0, colon]
1197
+ tlen = len_str.to_i
1198
+ if tlen < 0 || colon + 1 + tlen > wire.length
1199
+ return -1
1200
+ end
1201
+ topic = wire[colon + 1, tlen]
1202
+ payload = wire[colon + 1 + tlen, wire.length - colon - 1 - tlen]
1203
+ Tep::Broadcast.publish_local_only(topic, payload)
1204
+ end
1205
+
1206
+ end
1207
+
1208
+ module Presence
1209
+ def self.enable_pg_mirror(conninfo)
1210
+ conn = PG::Connection.new(conninfo)
1211
+ if conn.pgh < 0
1212
+ return -1
1213
+ end
1214
+ # exec raises PG::Error on failure now; degrade gracefully
1215
+ # (close + return -1) rather than letting it escape the worker.
1216
+ begin
1217
+ r = conn.exec(Tep::Presence.schema_sql)
1218
+ r.clear
1219
+ # Heartbeat table for the prune-stale-workers path (#47).
1220
+ r = conn.exec(Tep::Presence.worker_schema_sql)
1221
+ r.clear
1222
+ rescue PG::Error
1223
+ conn.finish
1224
+ return -1
1225
+ end
1226
+ Tep::APP.set_presence_pg_conn(conn)
1227
+ worker_id = Sock.sphttp_getpid.to_s + "-" + Time.now.to_i.to_s
1228
+ Tep::APP.set_presence_pg_worker_id(worker_id)
1229
+ Tep::APP.set_presence_pg_enabled(1)
1230
+ # Drop any rows from a prior worker that managed to leave
1231
+ # stale entries with this same worker_id (unlikely thanks
1232
+ # to the boot-epoch suffix, but defensive). Best-effort.
1233
+ Tep::Presence.mirror_exec(
1234
+ "DELETE FROM tep_presence WHERE worker_id = $1",
1235
+ [worker_id])
1236
+ # Register this worker's heartbeat row immediately. Apps
1237
+ # refresh it periodically via Tep::Presence.heartbeat;
1238
+ # prune_stale_workers deletes rows whose heartbeat is stale.
1239
+ Tep::Presence.heartbeat
1240
+ 0
1241
+ end
1242
+
1243
+ def self.disable_pg_mirror
1244
+ if Tep::APP.presence_pg_enabled == 0
1245
+ return 0
1246
+ end
1247
+ # Best-effort cleanup -- swallow PG errors (we're tearing the
1248
+ # mirror down regardless) and still finish + disable below.
1249
+ begin
1250
+ r = Tep::APP.presence_pg_conn.exec_params(
1251
+ "DELETE FROM tep_presence WHERE worker_id = $1",
1252
+ [Tep::APP.presence_pg_worker_id])
1253
+ r.clear
1254
+ # Remove the heartbeat row so prune_stale_workers doesn't
1255
+ # see this worker as live after we're gone.
1256
+ r = Tep::APP.presence_pg_conn.exec_params(
1257
+ "DELETE FROM tep_presence_worker WHERE worker_id = $1",
1258
+ [Tep::APP.presence_pg_worker_id])
1259
+ r.clear
1260
+ rescue PG::Error
1261
+ # swallow -- shutting the mirror down anyway
1262
+ end
1263
+ Tep::APP.presence_pg_conn.finish
1264
+ Tep::APP.set_presence_pg_enabled(0)
1265
+ 0
1266
+ end
1267
+
1268
+ def self.schema_sql
1269
+ "CREATE TABLE IF NOT EXISTS tep_presence (" +
1270
+ "worker_id TEXT NOT NULL, " +
1271
+ "topic TEXT NOT NULL, " +
1272
+ "fd INTEGER NOT NULL, " +
1273
+ "principal_id TEXT NOT NULL, " +
1274
+ "kind TEXT NOT NULL, " +
1275
+ "agent_id TEXT NOT NULL, " +
1276
+ "since_ts BIGINT NOT NULL, " +
1277
+ "status_state TEXT NOT NULL, " +
1278
+ "status_note TEXT NOT NULL, " +
1279
+ "status_until BIGINT NOT NULL, " +
1280
+ "PRIMARY KEY (worker_id, topic, fd)" +
1281
+ ")"
1282
+ end
1283
+
1284
+ def self.worker_schema_sql
1285
+ "CREATE TABLE IF NOT EXISTS tep_presence_worker (" +
1286
+ "worker_id TEXT PRIMARY KEY, " +
1287
+ "last_seen_ts BIGINT NOT NULL" +
1288
+ ")"
1289
+ end
1290
+
1291
+ def self.heartbeat
1292
+ if Tep::APP.presence_pg_enabled == 0
1293
+ return 0
1294
+ end
1295
+ wid = Tep::APP.presence_pg_worker_id
1296
+ if wid.length == 0
1297
+ return 0
1298
+ end
1299
+ begin
1300
+ r = Tep::APP.presence_pg_conn.exec_params(
1301
+ "INSERT INTO tep_presence_worker (worker_id, last_seen_ts) " +
1302
+ "VALUES ($1, $2) " +
1303
+ "ON CONFLICT (worker_id) DO UPDATE SET " +
1304
+ " last_seen_ts = EXCLUDED.last_seen_ts",
1305
+ [wid, Time.now.to_i.to_s])
1306
+ r.clear
1307
+ rescue PG::Error
1308
+ return 0
1309
+ end
1310
+ 1
1311
+ end
1312
+
1313
+ def self.prune_stale_workers(ttl_seconds)
1314
+ if Tep::APP.presence_pg_enabled == 0
1315
+ return 0
1316
+ end
1317
+ cutoff = Time.now.to_i - ttl_seconds
1318
+ conn = Tep::APP.presence_pg_conn
1319
+ begin
1320
+ # Drop dead heartbeats first; the second DELETE then walks
1321
+ # the worker_id space that's still alive.
1322
+ r1 = conn.exec_params(
1323
+ "DELETE FROM tep_presence_worker WHERE last_seen_ts < $1",
1324
+ [cutoff.to_s])
1325
+ r1.clear
1326
+ # Now drop presence rows whose worker_id isn't in the live
1327
+ # heartbeat table. NOT IN handles both crashed-and-pruned
1328
+ # workers and workers that never registered (legacy rows
1329
+ # from before this prune feature shipped).
1330
+ r2 = conn.exec(
1331
+ "DELETE FROM tep_presence " +
1332
+ "WHERE worker_id NOT IN (SELECT worker_id FROM tep_presence_worker)")
1333
+ n = r2.cmd_tuples
1334
+ r2.clear
1335
+ rescue PG::Error
1336
+ return 0
1337
+ end
1338
+ n
1339
+ end
1340
+
1341
+ def self.mirror_exec(sql, params)
1342
+ begin
1343
+ r = Tep::APP.presence_pg_conn.exec_params(sql, params)
1344
+ r.clear
1345
+ rescue PG::Error
1346
+ # swallow -- advisory mirror, local presence is authoritative
1347
+ end
1348
+ 0
1349
+ end
1350
+
1351
+ def self.mirror_insert(entry)
1352
+ if Tep::APP.presence_pg_enabled == 0
1353
+ return 0
1354
+ end
1355
+ Tep::Presence.mirror_exec(
1356
+ "INSERT INTO tep_presence " +
1357
+ "(worker_id, topic, fd, principal_id, kind, agent_id, " +
1358
+ " since_ts, status_state, status_note, status_until) " +
1359
+ "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) " +
1360
+ "ON CONFLICT (worker_id, topic, fd) DO UPDATE SET " +
1361
+ " principal_id = EXCLUDED.principal_id, " +
1362
+ " kind = EXCLUDED.kind, " +
1363
+ " agent_id = EXCLUDED.agent_id, " +
1364
+ " since_ts = EXCLUDED.since_ts, " +
1365
+ " status_state = EXCLUDED.status_state, " +
1366
+ " status_note = EXCLUDED.status_note, " +
1367
+ " status_until = EXCLUDED.status_until",
1368
+ [
1369
+ Tep::APP.presence_pg_worker_id,
1370
+ entry.topic,
1371
+ entry.fd.to_s,
1372
+ entry.principal_id,
1373
+ entry.kind.to_s,
1374
+ entry.agent_id,
1375
+ entry.since.to_s,
1376
+ entry.status_state.to_s,
1377
+ entry.status_note,
1378
+ entry.status_until.to_s
1379
+ ])
1380
+ end
1381
+
1382
+ def self.mirror_delete(topic, fd)
1383
+ if Tep::APP.presence_pg_enabled == 0
1384
+ return 0
1385
+ end
1386
+ Tep::Presence.mirror_exec(
1387
+ "DELETE FROM tep_presence " +
1388
+ "WHERE worker_id = $1 AND topic = $2 AND fd = $3",
1389
+ [Tep::APP.presence_pg_worker_id, topic, fd.to_s])
1390
+ end
1391
+
1392
+ def self.mirror_status(topic, fd, state, note, until_ts)
1393
+ if Tep::APP.presence_pg_enabled == 0
1394
+ return 0
1395
+ end
1396
+ Tep::Presence.mirror_exec(
1397
+ "UPDATE tep_presence " +
1398
+ "SET status_state = $4, status_note = $5, status_until = $6 " +
1399
+ "WHERE worker_id = $1 AND topic = $2 AND fd = $3",
1400
+ [Tep::APP.presence_pg_worker_id, topic, fd.to_s,
1401
+ state.to_s, note, until_ts.to_s])
1402
+ end
1403
+
1404
+ def self.list_global(topic)
1405
+ result = [Tep::PresenceEntry.new("", "", :human, "", -1, 0)]
1406
+ result.delete_at(0)
1407
+ if Tep::APP.presence_pg_enabled == 0
1408
+ return result
1409
+ end
1410
+ begin
1411
+ r = Tep::APP.presence_pg_conn.exec_params(
1412
+ "SELECT principal_id, kind, agent_id, fd, since_ts, " +
1413
+ " status_state, status_note, status_until " +
1414
+ "FROM tep_presence WHERE topic = $1 ORDER BY since_ts",
1415
+ [topic])
1416
+ rescue PG::Error
1417
+ return result
1418
+ end
1419
+ i = 0
1420
+ n = r.ntuples
1421
+ while i < n
1422
+ kind_sym = :human
1423
+ if r.getvalue(i, 1) == "agent_for"
1424
+ kind_sym = :agent_for
1425
+ end
1426
+ state_sym = :available
1427
+ sstr = r.getvalue(i, 5)
1428
+ if sstr == "busy"
1429
+ state_sym = :busy
1430
+ elsif sstr == "blocked"
1431
+ state_sym = :blocked
1432
+ end
1433
+ e = Tep::PresenceEntry.new(
1434
+ topic,
1435
+ r.getvalue(i, 0),
1436
+ kind_sym,
1437
+ r.getvalue(i, 2),
1438
+ r.getvalue(i, 3).to_i,
1439
+ r.getvalue(i, 4).to_i)
1440
+ e.status_state = state_sym
1441
+ e.status_note = r.getvalue(i, 6)
1442
+ e.status_until = r.getvalue(i, 7).to_i
1443
+ result.push(e)
1444
+ i += 1
1445
+ end
1446
+ r.clear
1447
+ result
1448
+ end
1449
+
1450
+ def self.count_global(topic)
1451
+ if Tep::APP.presence_pg_enabled == 0
1452
+ return 0
1453
+ end
1454
+ begin
1455
+ r = Tep::APP.presence_pg_conn.exec_params(
1456
+ "SELECT count(*) FROM tep_presence WHERE topic = $1",
1457
+ [topic])
1458
+ rescue PG::Error
1459
+ return 0
1460
+ end
1461
+ n = r.getvalue(0, 0).to_i
1462
+ r.clear
1463
+ n
1464
+ end
1465
+
1466
+ end
1467
+ end
1468
+
1469
+ # ===================================================================
1470
+ # Opt-in PG seeds (#216, relocated from lib/tep.rb). Pin parameter /
1471
+ # return C types for every PG-backed cmeth so a `require "tep/pg"`
1472
+ # app compiles cleanly even when it exercises only a subset.
1473
+ # PG::Connection.new("") returns a failed-conn instance (@pgh<0)
1474
+ # rather than raising, so all of this is safe at module load.
1475
+ # ===================================================================
1476
+
1477
+ # Broadcast PG-backend setters + cmeths. set_* via constant because
1478
+ # PG::Connection.new cannot run inside App#initialize (Tep::APP is
1479
+ # mid-construction). enable_pg_backend("","") connect-fails (-1).
1480
+ Tep::APP.set_broadcast_pg_enabled(0)
1481
+ Tep::APP.set_broadcast_pg_channel("")
1482
+ Tep::APP.set_broadcast_pg_conn(PG::Connection.new(""))
1483
+ Tep::Broadcast.enable_pg_backend("", "")
1484
+ Tep::Broadcast.poll_pg_once(0)
1485
+ Tep::Broadcast.disable_pg_backend
1486
+ Tep::Broadcast.encode_wire("", "")
1487
+ Tep::Broadcast.deliver_wire_local("0:")
1488
+ Tep::Broadcast.cross_worker_notify("_seed", "")
1489
+
1490
+ # Presence PG mirror cmeths. mirror_insert needs a PresenceEntry.
1491
+ _tep_pg_seed_entry = Tep::PresenceEntry.new("_seed", "_seed", :human, "", -1, 0)
1492
+ Tep::Presence.enable_pg_mirror("")
1493
+ Tep::Presence.schema_sql
1494
+ Tep::Presence.mirror_insert(_tep_pg_seed_entry)
1495
+ Tep::Presence.mirror_delete("_seed", -1)
1496
+ Tep::Presence.mirror_status("_seed", -1, :available, "", 0)
1497
+ Tep::Presence.list_global("_seed")
1498
+ Tep::Presence.count_global("_seed")
1499
+ Tep::Presence.worker_schema_sql
1500
+ Tep::Presence.heartbeat
1501
+ Tep::Presence.prune_stale_workers(90)
1502
+ Tep::Presence.disable_pg_mirror
1503
+ Tep::APP.set_presence_pg_enabled(0)
1504
+ Tep::APP.set_presence_pg_worker_id("")
1505
+ Tep::APP.set_presence_pg_conn(PG::Connection.new(""))
1506
+
1507
+ # PG::Connection / Result / Pool type-seeding.
1508
+ _tep_seed_pg_conn = PG::Connection.new("")
1509
+ _tep_seed_pg_conn.connected?
1510
+ _tep_seed_pg_conn.status
1511
+ _tep_seed_pg_conn.transaction_status
1512
+ _tep_seed_pg_conn.server_version
1513
+ _tep_seed_pg_conn.error_message
1514
+ _tep_seed_pg_conn.escape_string("")
1515
+ _tep_seed_pg_conn.escape_identifier("")
1516
+ _tep_seed_pg_conn.escape_literal("")
1517
+ _tep_seed_pg_conn.last_sqlstate = ""
1518
+ _tep_seed_pg_conn.last_error_message = ""
1519
+ _tep_seed_pg_conn.last_result_rh = -1
1520
+ # Async surface seed -- calling these on a failed-conn instance
1521
+ # is harmless (the C shim short-circuits on conn slot < 1).
1522
+ _tep_seed_pg_conn.async_exec("")
1523
+ _tep_seed_pg_seed_arr = [""]
1524
+ _tep_seed_pg_seed_arr.delete_at(0)
1525
+ _tep_seed_pg_conn.async_exec_params("", _tep_seed_pg_seed_arr)
1526
+ # Async connect cmeth. Returns -1 for empty conninfo from a
1527
+ # non-scheduled context (the shim's PQconnectStart-then-FAILED
1528
+ # path), which is type-equivalent to the success path.
1529
+ PG::Connection.async_connect("")
1530
+ # LISTEN / NOTIFY surface (Tep::Broadcast PG backend lands here).
1531
+ _tep_seed_pg_conn.listen("_seed")
1532
+ _tep_seed_pg_conn.unlisten("_seed")
1533
+ _tep_seed_pg_conn.notify("_seed", "")
1534
+ _tep_seed_pg_conn.poll_notification(0)
1535
+ _tep_seed_pg_conn.last_notify_channel
1536
+ _tep_seed_pg_conn.last_notify_payload
1537
+ _tep_seed_pg_res = PG::Result.new(-1)
1538
+ _tep_seed_pg_res.ntuples
1539
+ _tep_seed_pg_res.nfields
1540
+ _tep_seed_pg_res.fname(0)
1541
+ _tep_seed_pg_res.fnumber("")
1542
+ _tep_seed_pg_res.ftype(0)
1543
+ _tep_seed_pg_res.fformat(0)
1544
+ _tep_seed_pg_res.fmod(0)
1545
+ _tep_seed_pg_res.getvalue(0, 0)
1546
+ _tep_seed_pg_res.getisnull(0, 0)
1547
+ _tep_seed_pg_res.getlength(0, 0)
1548
+ _tep_seed_pg_res.value(0, 0)
1549
+ _tep_seed_pg_res.error_field(67)
1550
+ _tep_seed_pg_res.cmd_status
1551
+ _tep_seed_pg_res.cmd_tuples
1552
+ _tep_seed_pg_res.error_message
1553
+ _tep_seed_pg_res.sql_state
1554
+ _tep_seed_pg_res.fields
1555
+ _tep_seed_pg_res.values
1556
+ _tep_seed_pg_res.column_values(0)
1557
+ _tep_seed_pg_res.clear
1558
+ _tep_seed_pg_conn.close
1559
+ # Pool seed -- size 0 so we don't try to open real conns at load.
1560
+ _tep_seed_pg_pool = PG::Pool.new("", 0)
1561
+ _tep_seed_pg_pool.healthy?
1562
+ _tep_seed_pg_pool.available
1563
+ _tep_seed_pg_pool.size
1564
+ _tep_seed_pg_pool.set_checkout_timeout_ms(0)
1565
+ _tep_seed_pg_pool.close_all
1566
+ # NB: don't checkout/checkin against the size-0 seed pool; it'd
1567
+ # spin until timeout. The seed has @free.length=0 forever.