tep 0.11.4 → 0.11.5

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