tep 0.11.0
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 +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
data/lib/tep/pg.rb
ADDED
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
# Tep ships the PG battery -- a libpq wrapper that mirrors the
|
|
2
|
+
# ruby-pg gem's public surface (PG::Connection / PG::Result /
|
|
3
|
+
# PG::Error and SQLSTATE-keyed subclasses) so an eventual
|
|
4
|
+
# ActiveRecord-on-spinel port reuses the existing AR pg adapter
|
|
5
|
+
# with minimal divergence.
|
|
6
|
+
#
|
|
7
|
+
# Implementation:
|
|
8
|
+
# - lib/tep/tep_pg.c -- the libpq C shim (integer-handle slot
|
|
9
|
+
# tables, rotating return-string buffer, param accumulator).
|
|
10
|
+
# - this file -- the Ruby surface.
|
|
11
|
+
#
|
|
12
|
+
# Why not the `pg` gem? It's a CRuby native extension against MRI's
|
|
13
|
+
# ABI; spinel produces a static binary with no MRI runtime. The
|
|
14
|
+
# C-shim model (same pattern as Tep::SQLite) replaces "load a gem"
|
|
15
|
+
# with "link a .o at compile time."
|
|
16
|
+
#
|
|
17
|
+
# Namespace note: PG lives at the top level (matching `require 'pg'`
|
|
18
|
+
# from gem-shaped code), not under Tep::. This is the one battery
|
|
19
|
+
# that bends the Tep::Foo convention to keep AR-portability free.
|
|
20
|
+
#
|
|
21
|
+
# See docs/PG-BATTERY.md for the full design + per-method
|
|
22
|
+
# compatibility table.
|
|
23
|
+
|
|
24
|
+
module Pg
|
|
25
|
+
ffi_cflags "@TEP_PG_O@"
|
|
26
|
+
ffi_cflags "@TEP_PG_CFLAGS@"
|
|
27
|
+
|
|
28
|
+
# Result-status constants (collapsed from libpq's 8-value enum
|
|
29
|
+
# into the 4 callers care about; see tep_pg.c). Stable across
|
|
30
|
+
# libpq versions.
|
|
31
|
+
ffi_const :RES_TUPLES, 0
|
|
32
|
+
ffi_const :RES_COMMAND, 1
|
|
33
|
+
ffi_const :RES_EMPTY, 2
|
|
34
|
+
ffi_const :RES_ERROR, 3
|
|
35
|
+
|
|
36
|
+
# Connection lifecycle
|
|
37
|
+
ffi_func :tep_pg_connect, [:str], :int
|
|
38
|
+
ffi_func :tep_pg_connect_kv, [:str, :str, :int], :int
|
|
39
|
+
ffi_func :tep_pg_finish, [:int], :int
|
|
40
|
+
ffi_func :tep_pg_reset, [:int], :int
|
|
41
|
+
ffi_func :tep_pg_status, [:int], :int
|
|
42
|
+
ffi_func :tep_pg_transaction_status, [:int], :int
|
|
43
|
+
ffi_func :tep_pg_error_message, [:int], :str
|
|
44
|
+
ffi_func :tep_pg_server_version, [:int], :int
|
|
45
|
+
ffi_func :tep_pg_set_client_encoding, [:int, :str], :int
|
|
46
|
+
|
|
47
|
+
# Sync exec + param accumulator
|
|
48
|
+
ffi_func :tep_pg_exec, [:int, :str], :int
|
|
49
|
+
ffi_func :tep_pg_param_clear, [], :int
|
|
50
|
+
ffi_func :tep_pg_param_push_str, [:str], :int
|
|
51
|
+
ffi_func :tep_pg_param_push_null, [], :int
|
|
52
|
+
ffi_func :tep_pg_exec_params, [:int, :str], :int
|
|
53
|
+
|
|
54
|
+
# Result inspection
|
|
55
|
+
ffi_func :tep_pg_clear, [:int], :int
|
|
56
|
+
ffi_func :tep_pg_result_status, [:int], :int
|
|
57
|
+
ffi_func :tep_pg_result_error_message, [:int], :str
|
|
58
|
+
ffi_func :tep_pg_result_error_field, [:int, :int], :str
|
|
59
|
+
ffi_func :tep_pg_cmd_status, [:int], :str
|
|
60
|
+
ffi_func :tep_pg_cmd_tuples, [:int], :int
|
|
61
|
+
|
|
62
|
+
ffi_func :tep_pg_ntuples, [:int], :int
|
|
63
|
+
ffi_func :tep_pg_nfields, [:int], :int
|
|
64
|
+
ffi_func :tep_pg_fname, [:int, :int], :str
|
|
65
|
+
ffi_func :tep_pg_fnumber, [:int, :str], :int
|
|
66
|
+
ffi_func :tep_pg_ftype, [:int, :int], :int
|
|
67
|
+
ffi_func :tep_pg_fformat, [:int, :int], :int
|
|
68
|
+
ffi_func :tep_pg_fmod, [:int, :int], :int
|
|
69
|
+
ffi_func :tep_pg_getvalue, [:int, :int, :int], :str
|
|
70
|
+
ffi_func :tep_pg_getisnull, [:int, :int, :int], :int
|
|
71
|
+
ffi_func :tep_pg_getlength, [:int, :int, :int], :int
|
|
72
|
+
|
|
73
|
+
# Escape
|
|
74
|
+
ffi_func :tep_pg_escape_string, [:int, :str], :str
|
|
75
|
+
ffi_func :tep_pg_escape_literal, [:int, :str], :str
|
|
76
|
+
ffi_func :tep_pg_escape_identifier, [:int, :str], :str
|
|
77
|
+
|
|
78
|
+
# Async connect (libpq PQconnectStart + PQconnectPoll). Used by
|
|
79
|
+
# Connection#initialize when called inside a scheduled fiber, so
|
|
80
|
+
# the connect's TCP handshake + auth round-trip parks via
|
|
81
|
+
# Tep::Scheduler.io_wait instead of blocking the worker fiber.
|
|
82
|
+
# PG::Pool's eager open at construction benefits when N
|
|
83
|
+
# connections warm up in parallel under Scheduled.
|
|
84
|
+
ffi_func :tep_pg_connect_start, [:str], :int
|
|
85
|
+
ffi_func :tep_pg_connect_poll, [:int], :int
|
|
86
|
+
|
|
87
|
+
# Async exec (libpq non-blocking surface). Used by
|
|
88
|
+
# Connection#async_exec to park the fiber on Tep::Scheduler.io_wait
|
|
89
|
+
# between PG round-trips under Tep::Server::Scheduled, so other
|
|
90
|
+
# fibers in the same worker can run while the query is in flight.
|
|
91
|
+
ffi_func :tep_pg_socket, [:int], :int
|
|
92
|
+
ffi_func :tep_pg_set_nonblocking, [:int, :int], :int
|
|
93
|
+
ffi_func :tep_pg_send_query, [:int, :str], :int
|
|
94
|
+
ffi_func :tep_pg_send_query_params, [:int, :str], :int
|
|
95
|
+
ffi_func :tep_pg_flush, [:int], :int
|
|
96
|
+
ffi_func :tep_pg_consume_input, [:int], :int
|
|
97
|
+
ffi_func :tep_pg_is_busy, [:int], :int
|
|
98
|
+
ffi_func :tep_pg_get_result, [:int], :int
|
|
99
|
+
|
|
100
|
+
# LISTEN / NOTIFY. Used by Tep::Broadcast's PG backend
|
|
101
|
+
# (Battery 2 chunk 2.2) for cross-worker pub/sub. Channel names
|
|
102
|
+
# are SQL identifiers (caller's responsibility to keep safe);
|
|
103
|
+
# payloads are escaped server-side via PQescapeLiteral.
|
|
104
|
+
ffi_func :tep_pg_listen, [:int, :str], :int
|
|
105
|
+
ffi_func :tep_pg_unlisten, [:int, :str], :int
|
|
106
|
+
ffi_func :tep_pg_notify, [:int, :str, :str], :int
|
|
107
|
+
ffi_func :tep_pg_poll_notification, [:int, :int], :int
|
|
108
|
+
ffi_func :tep_pg_notify_channel, [], :str
|
|
109
|
+
ffi_func :tep_pg_notify_payload, [], :str
|
|
110
|
+
|
|
111
|
+
# Version
|
|
112
|
+
ffi_func :tep_pg_libpq_version, [], :str
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Public-facing PG module -- mirrors the ruby-pg gem's class
|
|
116
|
+
# layout. Callers write `PG.connect(...)`, `PG::Connection`,
|
|
117
|
+
# `PG::Result#each`, `rescue PG::UniqueViolation => e`, ...
|
|
118
|
+
module PG
|
|
119
|
+
# Connection status constants (libpq's ConnStatusType collapsed
|
|
120
|
+
# to the two values tep cares about).
|
|
121
|
+
CONNECTION_OK = 0
|
|
122
|
+
CONNECTION_BAD = 1
|
|
123
|
+
|
|
124
|
+
# Transaction status (libpq's PGTransactionStatusType).
|
|
125
|
+
PQTRANS_IDLE = 0
|
|
126
|
+
PQTRANS_ACTIVE = 1
|
|
127
|
+
PQTRANS_INTRANS = 2
|
|
128
|
+
PQTRANS_INERROR = 3
|
|
129
|
+
PQTRANS_UNKNOWN = 4
|
|
130
|
+
|
|
131
|
+
# Diagnostic field codes for Result#error_field. libpq uses single
|
|
132
|
+
# ASCII chars internally (PG_DIAG_SQLSTATE = 'C' = 67); expose
|
|
133
|
+
# them as integer constants here so callers can write
|
|
134
|
+
# `r.error_field(PG::DIAG_SQLSTATE)` without magic numbers.
|
|
135
|
+
DIAG_SEVERITY = 83 # 'S'
|
|
136
|
+
DIAG_SQLSTATE = 67 # 'C'
|
|
137
|
+
DIAG_MESSAGE_PRIMARY = 77 # 'M'
|
|
138
|
+
DIAG_MESSAGE_DETAIL = 68 # 'D'
|
|
139
|
+
DIAG_MESSAGE_HINT = 72 # 'H'
|
|
140
|
+
DIAG_STATEMENT_POSITION = 80 # 'P'
|
|
141
|
+
DIAG_CONTEXT = 87 # 'W'
|
|
142
|
+
DIAG_SCHEMA_NAME = 115 # 's'
|
|
143
|
+
DIAG_TABLE_NAME = 116 # 't'
|
|
144
|
+
DIAG_COLUMN_NAME = 99 # 'c'
|
|
145
|
+
DIAG_DATATYPE_NAME = 100 # 'd'
|
|
146
|
+
DIAG_CONSTRAINT_NAME = 110 # 'n'
|
|
147
|
+
|
|
148
|
+
# Convenience constructor matching ruby-pg's PG.connect entry.
|
|
149
|
+
# opts is either a libpq conninfo String ("postgresql://...") or
|
|
150
|
+
# a String=>String Hash of libpq keys (host, port, dbname, user,
|
|
151
|
+
# password, sslmode, ...).
|
|
152
|
+
#
|
|
153
|
+
# Unlike ruby-pg (which raises PG::ConnectionBad), connect does NOT
|
|
154
|
+
# raise on failure: it returns a connection-failed Connection
|
|
155
|
+
# (`connected?` false, `last_error_message` set). This is deliberate
|
|
156
|
+
# -- PG::Pool type-seeds its free list with `PG::Connection.new("")`
|
|
157
|
+
# at module load, before any server is reachable, so the constructor
|
|
158
|
+
# has to be non-raising. Check `conn.connected?` before use. (Query
|
|
159
|
+
# methods #exec / #exec_params DO raise; see Connection#exec.)
|
|
160
|
+
def self.connect(opts)
|
|
161
|
+
Connection.new(opts)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
class Connection
|
|
165
|
+
# `:pgh` rather than `:handle` -- same poly-dispatch widening
|
|
166
|
+
# concern as Tep::SQLite#dbh (sharing a method name with
|
|
167
|
+
# Tep::Handler#handle confuses spinel's same-named-imeth-across-
|
|
168
|
+
# classes unifier).
|
|
169
|
+
attr_accessor :pgh
|
|
170
|
+
# Error context for the most recent exception raised by this
|
|
171
|
+
# connection. Spinel's `raise X.new(msg, ...)` lowering doesn't
|
|
172
|
+
# handle custom initializers (#622), so the SQLSTATE / message /
|
|
173
|
+
# owning-result-handle live here instead. Read after
|
|
174
|
+
# `rescue PG::Error => e`:
|
|
175
|
+
#
|
|
176
|
+
# begin; conn.exec_params(sql, params)
|
|
177
|
+
# rescue PG::Error => e
|
|
178
|
+
# sqlstate = conn.last_sqlstate
|
|
179
|
+
# full_msg = conn.last_error_message
|
|
180
|
+
# end
|
|
181
|
+
#
|
|
182
|
+
# AR's `translate_exception_class(message, sql, binds)` uses
|
|
183
|
+
# `e.is_a?(PG::UniqueViolation)` etc., which still works -- the
|
|
184
|
+
# class hierarchy is intact; only the per-exception accessors
|
|
185
|
+
# move to the connection.
|
|
186
|
+
attr_accessor :last_sqlstate, :last_error_message, :last_result_rh
|
|
187
|
+
|
|
188
|
+
def initialize(opts)
|
|
189
|
+
@pgh = -1
|
|
190
|
+
@last_sqlstate = ""
|
|
191
|
+
@last_error_message = ""
|
|
192
|
+
@last_result_rh = -1
|
|
193
|
+
if opts.is_a?(String)
|
|
194
|
+
if Tep::Scheduler.scheduled_context?
|
|
195
|
+
h = Connection.async_connect(opts)
|
|
196
|
+
else
|
|
197
|
+
h = Pg.tep_pg_connect(opts)
|
|
198
|
+
end
|
|
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)
|
|
214
|
+
end
|
|
215
|
+
if h < 0
|
|
216
|
+
# Slot 0 holds the most recent connect-failure error message
|
|
217
|
+
# (PQstatus on a failed PQconnectdb still gives a readable
|
|
218
|
+
# error, but the conn itself is closed by the time we get
|
|
219
|
+
# here -- the shim stashes the message before PQfinish).
|
|
220
|
+
@last_error_message = Pg.tep_pg_error_message(0)
|
|
221
|
+
@last_sqlstate = ""
|
|
222
|
+
# Connection-failure surfaces via `c.last_error_message` +
|
|
223
|
+
# `c.connected?` after the constructor returns -- the
|
|
224
|
+
# constructor stays non-raising on purpose (PG::Pool seeds its
|
|
225
|
+
# free list with `PG::Connection.new("")` before a server is
|
|
226
|
+
# reachable; a raising constructor would blow up at module
|
|
227
|
+
# load). Callers must check `c.connected?` before exec. NB:
|
|
228
|
+
# this is the lone non-raising path -- query methods raise
|
|
229
|
+
# PG::Error subclasses now that spinel supports namespaced
|
|
230
|
+
# raise + rescue (matz/spinel#627 + #1041).
|
|
231
|
+
end
|
|
232
|
+
@pgh = h
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def connected?
|
|
236
|
+
@pgh >= 0
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def close
|
|
240
|
+
if @pgh >= 0
|
|
241
|
+
Pg.tep_pg_finish(@pgh)
|
|
242
|
+
@pgh = -1
|
|
243
|
+
end
|
|
244
|
+
0
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def finish
|
|
248
|
+
close
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def reset
|
|
252
|
+
if @pgh >= 0
|
|
253
|
+
Pg.tep_pg_reset(@pgh)
|
|
254
|
+
end
|
|
255
|
+
self
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def status
|
|
259
|
+
@pgh < 0 ? PG::CONNECTION_BAD : Pg.tep_pg_status(@pgh)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def transaction_status
|
|
263
|
+
@pgh < 0 ? PG::PQTRANS_UNKNOWN : Pg.tep_pg_transaction_status(@pgh)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def server_version
|
|
267
|
+
@pgh < 0 ? 0 : Pg.tep_pg_server_version(@pgh)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def error_message
|
|
271
|
+
@pgh < 0 ? "" : Pg.tep_pg_error_message(@pgh)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# LISTEN / NOTIFY (Battery 2 chunk 2.2). Used by
|
|
275
|
+
# Tep::Broadcast's PG backend for cross-worker pub/sub.
|
|
276
|
+
# Channel names must be safe SQL identifiers (no caller-
|
|
277
|
+
# controlled interpolation -- use a hard-coded constant).
|
|
278
|
+
# Payload max size is 8000 bytes per PG default.
|
|
279
|
+
def listen(channel)
|
|
280
|
+
return -1 if @pgh < 0
|
|
281
|
+
Pg.tep_pg_listen(@pgh, channel)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def unlisten(channel)
|
|
285
|
+
return -1 if @pgh < 0
|
|
286
|
+
Pg.tep_pg_unlisten(@pgh, channel)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def notify(channel, payload)
|
|
290
|
+
return -1 if @pgh < 0
|
|
291
|
+
Pg.tep_pg_notify(@pgh, channel, payload)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Block up to `timeout_ms` waiting for one notification on the
|
|
295
|
+
# connection. Returns 1 on receipt (caller then reads
|
|
296
|
+
# #last_notify_channel + #last_notify_payload), 0 on timeout,
|
|
297
|
+
# -1 on connection error. Connection must already be in LISTEN
|
|
298
|
+
# mode for the channel of interest.
|
|
299
|
+
def poll_notification(timeout_ms)
|
|
300
|
+
return -1 if @pgh < 0
|
|
301
|
+
Pg.tep_pg_poll_notification(@pgh, timeout_ms)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def last_notify_channel
|
|
305
|
+
Pg.tep_pg_notify_channel
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def last_notify_payload
|
|
309
|
+
Pg.tep_pg_notify_payload
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Run a no-params query. Returns a PG::Result on success.
|
|
313
|
+
#
|
|
314
|
+
# ON ERROR IT RAISES the SQLSTATE-mapped PG::Error subclass
|
|
315
|
+
# (PG::UniqueViolation, PG::UndefinedTable, ... -> PG::ServerError
|
|
316
|
+
# for unmapped states) -- the ruby-pg / AR shape. The failed
|
|
317
|
+
# PGresult is freed before the raise; the SQLSTATE / message stay
|
|
318
|
+
# readable on `conn.last_sqlstate` / `#last_error_message` for
|
|
319
|
+
# post-rescue inspection:
|
|
320
|
+
#
|
|
321
|
+
# begin
|
|
322
|
+
# c.exec(sql)
|
|
323
|
+
# rescue PG::UniqueViolation => e
|
|
324
|
+
# ... # e.message + c.last_sqlstate
|
|
325
|
+
# rescue PG::Error => e # base catches any server error
|
|
326
|
+
# ...
|
|
327
|
+
# end
|
|
328
|
+
#
|
|
329
|
+
# Raising (instead of the old Result-on-error sentinel) became
|
|
330
|
+
# viable once spinel learned namespaced raise + hierarchy-walking
|
|
331
|
+
# rescue (matz/spinel#627 + #1041). NB: PG.connect is the one path
|
|
332
|
+
# that still does NOT raise -- it returns a connection-failed
|
|
333
|
+
# instance so PG::Pool can type-seed without a live server (check
|
|
334
|
+
# `conn.connected?`).
|
|
335
|
+
#
|
|
336
|
+
# Under `Tep::Server::Scheduled` this routes through the libpq
|
|
337
|
+
# async surface (PQsendQuery + PQflush + PQconsumeInput parked
|
|
338
|
+
# on Tep::Scheduler.io_wait), so other fibers in the same
|
|
339
|
+
# worker can run while the query is in flight. Under prefork
|
|
340
|
+
# it routes through the blocking PQexec. Both raise identically.
|
|
341
|
+
def exec(sql)
|
|
342
|
+
if Tep::Scheduler.scheduled_context?
|
|
343
|
+
return async_exec(sql)
|
|
344
|
+
end
|
|
345
|
+
rh = Pg.tep_pg_exec(@pgh, sql)
|
|
346
|
+
r = PG::Result.new(rh)
|
|
347
|
+
Connection.record_error_if_any(self, r)
|
|
348
|
+
r
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Parameterised query with positional binds ($1, $2, ...).
|
|
352
|
+
# `params` is an Array of String / Integer / nil. Same
|
|
353
|
+
# raise-on-error contract + auto-routing as `exec`.
|
|
354
|
+
def exec_params(sql, params)
|
|
355
|
+
Pg.tep_pg_param_clear
|
|
356
|
+
i = 0
|
|
357
|
+
n = params.length
|
|
358
|
+
while i < n
|
|
359
|
+
p = params[i]
|
|
360
|
+
if p == nil
|
|
361
|
+
Pg.tep_pg_param_push_null
|
|
362
|
+
else
|
|
363
|
+
Pg.tep_pg_param_push_str(p.to_s)
|
|
364
|
+
end
|
|
365
|
+
i += 1
|
|
366
|
+
end
|
|
367
|
+
if Tep::Scheduler.scheduled_context?
|
|
368
|
+
return async_exec_params_after_clear(sql)
|
|
369
|
+
end
|
|
370
|
+
rh = Pg.tep_pg_exec_params(@pgh, sql)
|
|
371
|
+
r = PG::Result.new(rh)
|
|
372
|
+
Connection.record_error_if_any(self, r)
|
|
373
|
+
r
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Explicit async exec. Same shape as `exec` but doesn't
|
|
377
|
+
# context-detect -- always uses the libpq async surface. If
|
|
378
|
+
# called outside Tep::Server::Scheduled, Tep::Scheduler.io_wait
|
|
379
|
+
# falls back to a single-shot poll(2), so this still works
|
|
380
|
+
# under prefork (just without the cross-fiber concurrency
|
|
381
|
+
# win).
|
|
382
|
+
def async_exec(sql)
|
|
383
|
+
Pg.tep_pg_set_nonblocking(@pgh, 1)
|
|
384
|
+
ok = Pg.tep_pg_send_query(@pgh, sql)
|
|
385
|
+
if ok != 1
|
|
386
|
+
Connection.raise_send_failure(self)
|
|
387
|
+
end
|
|
388
|
+
Connection.drain_send(@pgh)
|
|
389
|
+
Connection.wait_for_result_ready(@pgh)
|
|
390
|
+
rh = Pg.tep_pg_get_result(@pgh)
|
|
391
|
+
r = PG::Result.new(rh)
|
|
392
|
+
# Drain trailing NULL terminator (libpq requires reading
|
|
393
|
+
# until PQgetResult returns NULL to mark the conn ready for
|
|
394
|
+
# the next send_query).
|
|
395
|
+
Connection.drain_remaining_results(@pgh)
|
|
396
|
+
Connection.record_error_if_any(self, r)
|
|
397
|
+
r
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Parameterised async exec. `params` is an Array of
|
|
401
|
+
# String / Integer / nil; same conversion as exec_params.
|
|
402
|
+
def async_exec_params(sql, params)
|
|
403
|
+
Pg.tep_pg_param_clear
|
|
404
|
+
i = 0
|
|
405
|
+
n = params.length
|
|
406
|
+
while i < n
|
|
407
|
+
p = params[i]
|
|
408
|
+
if p == nil
|
|
409
|
+
Pg.tep_pg_param_push_null
|
|
410
|
+
else
|
|
411
|
+
Pg.tep_pg_param_push_str(p.to_s)
|
|
412
|
+
end
|
|
413
|
+
i += 1
|
|
414
|
+
end
|
|
415
|
+
async_exec_params_after_clear(sql)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Internal: param accumulator has already been populated by
|
|
419
|
+
# the caller (either exec_params routing here on context
|
|
420
|
+
# detect, or async_exec_params after its own push loop).
|
|
421
|
+
def async_exec_params_after_clear(sql)
|
|
422
|
+
Pg.tep_pg_set_nonblocking(@pgh, 1)
|
|
423
|
+
ok = Pg.tep_pg_send_query_params(@pgh, sql)
|
|
424
|
+
if ok != 1
|
|
425
|
+
Connection.raise_send_failure(self)
|
|
426
|
+
end
|
|
427
|
+
Connection.drain_send(@pgh)
|
|
428
|
+
Connection.wait_for_result_ready(@pgh)
|
|
429
|
+
rh = Pg.tep_pg_get_result(@pgh)
|
|
430
|
+
r = PG::Result.new(rh)
|
|
431
|
+
Connection.drain_remaining_results(@pgh)
|
|
432
|
+
Connection.record_error_if_any(self, r)
|
|
433
|
+
r
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# --- Async connect helper ---
|
|
437
|
+
|
|
438
|
+
# Drive PQconnectStart + PQconnectPoll, parking on io_wait
|
|
439
|
+
# between poll calls. Returns the conn slot (>=1) on success
|
|
440
|
+
# or -1 on failure. The C shim's tep_pg_connect_poll stashes
|
|
441
|
+
# the libpq error message on a FAILED return so
|
|
442
|
+
# `Pg.tep_pg_error_message(0)` still surfaces the diagnostic
|
|
443
|
+
# for the Connection.new "connect failed" branch.
|
|
444
|
+
#
|
|
445
|
+
# libpq's PostgresPollingStatusType:
|
|
446
|
+
# 0 = PGRES_POLLING_FAILED
|
|
447
|
+
# 1 = PGRES_POLLING_READING (wait for fd READ-ready)
|
|
448
|
+
# 2 = PGRES_POLLING_WRITING (wait for fd WRITE-ready)
|
|
449
|
+
# 3 = PGRES_POLLING_OK (connected; stop polling)
|
|
450
|
+
def self.async_connect(conninfo)
|
|
451
|
+
h = Pg.tep_pg_connect_start(conninfo)
|
|
452
|
+
if h < 0
|
|
453
|
+
return -1
|
|
454
|
+
end
|
|
455
|
+
fd = Pg.tep_pg_socket(h)
|
|
456
|
+
while true
|
|
457
|
+
state = Pg.tep_pg_connect_poll(h)
|
|
458
|
+
if state == 3
|
|
459
|
+
# PGRES_POLLING_OK
|
|
460
|
+
Pg.tep_pg_set_client_encoding(h, "UTF8")
|
|
461
|
+
return h
|
|
462
|
+
end
|
|
463
|
+
if state == 0
|
|
464
|
+
# PGRES_POLLING_FAILED. The shim has already stashed the
|
|
465
|
+
# error message; we PQfinish the slot.
|
|
466
|
+
Pg.tep_pg_finish(h)
|
|
467
|
+
return -1
|
|
468
|
+
end
|
|
469
|
+
mode = state == 1 ? Tep::Scheduler::READ : Tep::Scheduler::WRITE
|
|
470
|
+
Tep::Scheduler.io_wait(fd, mode, 10)
|
|
471
|
+
end
|
|
472
|
+
-1
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# --- Internal helpers for the async loop ---
|
|
476
|
+
|
|
477
|
+
# PQsendQuery returned 0 (immediate failure -- conn already
|
|
478
|
+
# closed, send buffer error, etc.). Mirror the error onto the
|
|
479
|
+
# conn's last_* and raise, matching the exec error path (ruby-pg
|
|
480
|
+
# surfaces a send failure as PG::UnableToSend < PG::Error). No
|
|
481
|
+
# SQLSTATE is available pre-result, so this maps to the transport
|
|
482
|
+
# leaf rather than going through raise_for_sqlstate.
|
|
483
|
+
def self.raise_send_failure(conn)
|
|
484
|
+
conn.last_sqlstate = ""
|
|
485
|
+
conn.last_error_message = conn.error_message
|
|
486
|
+
conn.last_result_rh = -1
|
|
487
|
+
raise PG::UnableToSend, conn.error_message
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Drain libpq's send buffer. PQflush returns 0 when done; 1
|
|
491
|
+
# when the kernel send-buffer is full and we should park on
|
|
492
|
+
# WRITE-ready; -1 on error. Timeout is generous (10s); a
|
|
493
|
+
# genuinely-stuck PG is the rare case worth bailing on.
|
|
494
|
+
def self.drain_send(pgh)
|
|
495
|
+
fd = Pg.tep_pg_socket(pgh)
|
|
496
|
+
while true
|
|
497
|
+
rc = Pg.tep_pg_flush(pgh)
|
|
498
|
+
if rc == 0
|
|
499
|
+
return 0
|
|
500
|
+
end
|
|
501
|
+
if rc < 0
|
|
502
|
+
return -1
|
|
503
|
+
end
|
|
504
|
+
# rc == 1: send buffer full, park on writability.
|
|
505
|
+
Tep::Scheduler.io_wait(fd, Tep::Scheduler::WRITE, 10)
|
|
506
|
+
end
|
|
507
|
+
0
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Wait until PQisBusy returns 0 (PQgetResult won't block).
|
|
511
|
+
# Pumps PQconsumeInput in between io_wait calls so the
|
|
512
|
+
# libpq state machine advances. Timeout is generous (30s)
|
|
513
|
+
# since the query itself can take that long; the io_wait
|
|
514
|
+
# timeout is per-iteration, not cumulative.
|
|
515
|
+
def self.wait_for_result_ready(pgh)
|
|
516
|
+
fd = Pg.tep_pg_socket(pgh)
|
|
517
|
+
while true
|
|
518
|
+
if Pg.tep_pg_consume_input(pgh) != 1
|
|
519
|
+
return -1
|
|
520
|
+
end
|
|
521
|
+
if Pg.tep_pg_is_busy(pgh) == 0
|
|
522
|
+
return 0
|
|
523
|
+
end
|
|
524
|
+
Tep::Scheduler.io_wait(fd, Tep::Scheduler::READ, 30)
|
|
525
|
+
end
|
|
526
|
+
0
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# After the first PQgetResult returned a real Result, libpq
|
|
530
|
+
# requires the conn be drained via additional PQgetResult
|
|
531
|
+
# calls until NULL is returned. This is a fast in-memory drain
|
|
532
|
+
# (no network), but it has to happen between async_exec calls
|
|
533
|
+
# or the next send_query will fail. Each tep_pg_get_result
|
|
534
|
+
# call that produces a non-NULL result stashes it in the slot
|
|
535
|
+
# table; we PQclear those immediately since they're trailing
|
|
536
|
+
# status results we don't expose.
|
|
537
|
+
def self.drain_remaining_results(pgh)
|
|
538
|
+
while true
|
|
539
|
+
rh = Pg.tep_pg_get_result(pgh)
|
|
540
|
+
if rh < 0
|
|
541
|
+
return 0
|
|
542
|
+
end
|
|
543
|
+
# A trailing result -- shouldn't normally happen for
|
|
544
|
+
# single-statement queries, but defensively free.
|
|
545
|
+
Pg.tep_pg_clear(rh)
|
|
546
|
+
end
|
|
547
|
+
0
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def escape_string(s)
|
|
551
|
+
Pg.tep_pg_escape_string(@pgh, s)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def escape_identifier(s)
|
|
555
|
+
Pg.tep_pg_escape_identifier(@pgh, s)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def escape_literal(s)
|
|
559
|
+
Pg.tep_pg_escape_literal(@pgh, s)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# Class-method form -- ruby-pg allows escape_string and
|
|
563
|
+
# quote_ident without a live conn. We route through slot 0
|
|
564
|
+
# which the shim treats as "no conn, fall back to standalone
|
|
565
|
+
# PQescapeString". Use the instance method when a conn is
|
|
566
|
+
# available -- it goes through PQescapeStringConn which is
|
|
567
|
+
# the standards-compliant path.
|
|
568
|
+
def self.escape_string(s)
|
|
569
|
+
Pg.tep_pg_escape_string(0, s)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def self.quote_ident(s)
|
|
573
|
+
# PQescapeIdentifier requires a conn; without one we fall
|
|
574
|
+
# through to "" which is wrong but rare. Apps with a live
|
|
575
|
+
# PG::Connection should use the instance method.
|
|
576
|
+
Pg.tep_pg_escape_identifier(0, s)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# If the Result is in an error state, mirror SQLSTATE +
|
|
580
|
+
# message + result-handle onto the conn so post-rescue (or
|
|
581
|
+
# post-`if !r.ok?`) callers can read them via `conn.last_*`.
|
|
582
|
+
# No raise here -- see the docstring on `exec` for why.
|
|
583
|
+
def self.record_error_if_any(conn, r)
|
|
584
|
+
st = r.status
|
|
585
|
+
if st == Pg::RES_TUPLES || st == Pg::RES_COMMAND || st == Pg::RES_EMPTY
|
|
586
|
+
return 0
|
|
587
|
+
end
|
|
588
|
+
sqlstate = r.error_field(PG::DIAG_SQLSTATE)
|
|
589
|
+
msg = r.error_message
|
|
590
|
+
if msg.length == 0
|
|
591
|
+
msg = conn.error_message
|
|
592
|
+
end
|
|
593
|
+
conn.last_sqlstate = sqlstate
|
|
594
|
+
conn.last_error_message = msg
|
|
595
|
+
# Free the failed PGresult NOW: once we raise out of
|
|
596
|
+
# exec/exec_params the caller's `r.clear` never runs, so this is
|
|
597
|
+
# the only chance to release it. The SQLSTATE / message are
|
|
598
|
+
# already copied onto conn.last_* (Strings) for post-rescue
|
|
599
|
+
# inspection, so dropping the handle loses nothing callers need.
|
|
600
|
+
conn.last_result_rh = -1
|
|
601
|
+
r.clear
|
|
602
|
+
# ruby-pg / AR parity: raise the SQLSTATE-mapped PG::Error
|
|
603
|
+
# subclass (live since matz/spinel#627 + #1041 -- namespaced
|
|
604
|
+
# raise + hierarchy-walking rescue). Callers `rescue
|
|
605
|
+
# PG::UniqueViolation` (leaf) or `rescue PG::Error` (base).
|
|
606
|
+
PG.raise_for_sqlstate(sqlstate, msg)
|
|
607
|
+
0
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
class Result
|
|
612
|
+
attr_accessor :rh
|
|
613
|
+
|
|
614
|
+
def initialize(rh)
|
|
615
|
+
@rh = rh
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def status
|
|
619
|
+
@rh < 0 ? Pg::RES_ERROR : Pg.tep_pg_result_status(@rh)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# True when the query reached the server and produced a
|
|
623
|
+
# non-error result (rows, command success, or empty query).
|
|
624
|
+
# Inspect `error_message` / `error_field(5)` on a non-ok result.
|
|
625
|
+
def ok?
|
|
626
|
+
st = status
|
|
627
|
+
st == Pg::RES_TUPLES || st == Pg::RES_COMMAND || st == Pg::RES_EMPTY
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def error_message
|
|
631
|
+
@rh < 0 ? "" : Pg.tep_pg_result_error_message(@rh)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def error_field(code)
|
|
635
|
+
@rh < 0 ? "" : Pg.tep_pg_result_error_field(@rh, code)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def cmd_status
|
|
639
|
+
@rh < 0 ? "" : Pg.tep_pg_cmd_status(@rh)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# ruby-pg's PG::Result#error_field shortcut: 5-char SQLSTATE
|
|
643
|
+
# string. Empty when the result isn't an error.
|
|
644
|
+
def sql_state
|
|
645
|
+
error_field(PG::DIAG_SQLSTATE)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def cmd_tuples
|
|
649
|
+
@rh < 0 ? 0 : Pg.tep_pg_cmd_tuples(@rh)
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def ntuples
|
|
653
|
+
@rh < 0 ? 0 : Pg.tep_pg_ntuples(@rh)
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def nfields
|
|
657
|
+
@rh < 0 ? 0 : Pg.tep_pg_nfields(@rh)
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# ruby-pg aliases for ntuples / nfields.
|
|
661
|
+
def num_tuples; ntuples; end
|
|
662
|
+
def num_fields; nfields; end
|
|
663
|
+
|
|
664
|
+
def fname(col)
|
|
665
|
+
@rh < 0 ? "" : Pg.tep_pg_fname(@rh, col)
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def fnumber(name)
|
|
669
|
+
@rh < 0 ? -1 : Pg.tep_pg_fnumber(@rh, name)
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def ftype(col)
|
|
673
|
+
@rh < 0 ? 0 : Pg.tep_pg_ftype(@rh, col)
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def fformat(col)
|
|
677
|
+
@rh < 0 ? 0 : Pg.tep_pg_fformat(@rh, col)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
def fmod(col)
|
|
681
|
+
@rh < 0 ? -1 : Pg.tep_pg_fmod(@rh, col)
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def getvalue(row, col)
|
|
685
|
+
@rh < 0 ? "" : Pg.tep_pg_getvalue(@rh, row, col)
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def getisnull(row, col)
|
|
689
|
+
@rh < 0 ? true : Pg.tep_pg_getisnull(@rh, row, col) == 1
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def getlength(row, col)
|
|
693
|
+
@rh < 0 ? 0 : Pg.tep_pg_getlength(@rh, row, col)
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# ruby-pg's #value is an alias for #getvalue.
|
|
697
|
+
def value(row, col)
|
|
698
|
+
getvalue(row, col)
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def fields
|
|
702
|
+
out = [""]
|
|
703
|
+
out.delete_at(0)
|
|
704
|
+
w = nfields
|
|
705
|
+
j = 0
|
|
706
|
+
while j < w
|
|
707
|
+
out.push(fname(j))
|
|
708
|
+
j += 1
|
|
709
|
+
end
|
|
710
|
+
out
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def values
|
|
714
|
+
rows = [[""]]
|
|
715
|
+
rows.delete_at(0)
|
|
716
|
+
n = ntuples
|
|
717
|
+
w = nfields
|
|
718
|
+
i = 0
|
|
719
|
+
while i < n
|
|
720
|
+
row = [""]
|
|
721
|
+
row.delete_at(0)
|
|
722
|
+
j = 0
|
|
723
|
+
while j < w
|
|
724
|
+
row.push(getvalue(i, j))
|
|
725
|
+
j += 1
|
|
726
|
+
end
|
|
727
|
+
rows.push(row)
|
|
728
|
+
i += 1
|
|
729
|
+
end
|
|
730
|
+
rows
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def column_values(col)
|
|
734
|
+
out = [""]
|
|
735
|
+
out.delete_at(0)
|
|
736
|
+
n = ntuples
|
|
737
|
+
i = 0
|
|
738
|
+
while i < n
|
|
739
|
+
out.push(getvalue(i, col))
|
|
740
|
+
i += 1
|
|
741
|
+
end
|
|
742
|
+
out
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Array-yielding iteration. Cleaner shape than #each for hot
|
|
746
|
+
# paths -- no Hash allocation per row.
|
|
747
|
+
def each_row
|
|
748
|
+
n = ntuples
|
|
749
|
+
w = nfields
|
|
750
|
+
i = 0
|
|
751
|
+
while i < n
|
|
752
|
+
row = [""]
|
|
753
|
+
row.delete_at(0)
|
|
754
|
+
j = 0
|
|
755
|
+
while j < w
|
|
756
|
+
row.push(getvalue(i, j))
|
|
757
|
+
j += 1
|
|
758
|
+
end
|
|
759
|
+
yield row
|
|
760
|
+
i += 1
|
|
761
|
+
end
|
|
762
|
+
self
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
# Hash-yielding iteration -- matches ruby-pg's #each. Pre-builds
|
|
766
|
+
# the field-name array to skip a per-row fname call. The Hash
|
|
767
|
+
# shape is pinned to str_str_hash via a seed in lib/tep.rb;
|
|
768
|
+
# without that seed spinel widens to poly_poly_hash on first
|
|
769
|
+
# use.
|
|
770
|
+
def each
|
|
771
|
+
flds = fields
|
|
772
|
+
n = ntuples
|
|
773
|
+
w = flds.length
|
|
774
|
+
i = 0
|
|
775
|
+
while i < n
|
|
776
|
+
row = Tep.str_hash
|
|
777
|
+
j = 0
|
|
778
|
+
while j < w
|
|
779
|
+
row[flds[j]] = getvalue(i, j)
|
|
780
|
+
j += 1
|
|
781
|
+
end
|
|
782
|
+
yield row
|
|
783
|
+
i += 1
|
|
784
|
+
end
|
|
785
|
+
self
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
def clear
|
|
789
|
+
if @rh >= 0
|
|
790
|
+
Pg.tep_pg_clear(@rh)
|
|
791
|
+
@rh = -1
|
|
792
|
+
end
|
|
793
|
+
0
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
# libpq version string ("16.2.0" etc.). Diagnostic / banner use.
|
|
798
|
+
def self.libpq_version
|
|
799
|
+
Pg.tep_pg_libpq_version
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# -------- Exception hierarchy --------
|
|
803
|
+
#
|
|
804
|
+
# Mirrors ruby-pg's PG::Error tree. ActiveRecord's adapter
|
|
805
|
+
# pattern-matches with `e.is_a?(PG::UniqueViolation)` etc.; the
|
|
806
|
+
# leaf classes are what makes that pattern work without a SQLSTATE
|
|
807
|
+
# parse at every callsite.
|
|
808
|
+
#
|
|
809
|
+
# v1 ships base + ConnectionBad + UnableToSend + ServerError; the
|
|
810
|
+
# SQLSTATE-keyed leaves below are the v1.5 surface (AR-coverage
|
|
811
|
+
# subset). Adding a leaf is one class definition + one line in
|
|
812
|
+
# error_class_for_sqlstate.
|
|
813
|
+
|
|
814
|
+
# PG::Error hierarchy -- ruby-pg-shape, SQLSTATE-keyed. AR's
|
|
815
|
+
# pg adapter does `e.is_a?(PG::UniqueViolation)` to translate
|
|
816
|
+
# libpq errors; the class identity has to match. Live since
|
|
817
|
+
# matz/spinel#627 (rescue ParentClass + is_a?(ParentClass) walk
|
|
818
|
+
# the class hierarchy).
|
|
819
|
+
#
|
|
820
|
+
# Raised by Connection#exec / #exec_params via the two-arg
|
|
821
|
+
# `raise PG::Klass, msg` form (spinel can't lower `raise
|
|
822
|
+
# X.new(msg, ...)` for custom Exception initializers --
|
|
823
|
+
# matz/spinel#622). SQLSTATE / result-handle context lives on
|
|
824
|
+
# `conn.last_sqlstate` / `#last_error_message` / `#last_result_rh`
|
|
825
|
+
# for callers who need them post-rescue.
|
|
826
|
+
class Error < StandardError; end
|
|
827
|
+
|
|
828
|
+
class ConnectionBad < Error; end
|
|
829
|
+
class UnableToSend < Error; end
|
|
830
|
+
class ServerError < Error; end
|
|
831
|
+
|
|
832
|
+
# SQLSTATE class 23 -- integrity constraint violation
|
|
833
|
+
class IntegrityConstraintViolation < ServerError; end
|
|
834
|
+
class NotNullViolation < IntegrityConstraintViolation; end # 23502
|
|
835
|
+
class ForeignKeyViolation < IntegrityConstraintViolation; end # 23503
|
|
836
|
+
class UniqueViolation < IntegrityConstraintViolation; end # 23505
|
|
837
|
+
class CheckViolation < IntegrityConstraintViolation; end # 23514
|
|
838
|
+
class ExclusionViolation < IntegrityConstraintViolation; end # 23P01
|
|
839
|
+
|
|
840
|
+
# SQLSTATE class 25 -- invalid transaction state
|
|
841
|
+
class InFailedSqlTransaction < ServerError; end # 25P02
|
|
842
|
+
class ReadOnlySqlTransaction < ServerError; end # 25006
|
|
843
|
+
|
|
844
|
+
# SQLSTATE class 40 -- transaction rollback
|
|
845
|
+
class SerializationFailure < ServerError; end # 40001
|
|
846
|
+
class DeadlockDetected < ServerError; end # 40P01
|
|
847
|
+
|
|
848
|
+
# SQLSTATE class 42 -- syntax / access rule violation
|
|
849
|
+
class SyntaxError < ServerError; end # 42601
|
|
850
|
+
class UndefinedColumn < ServerError; end # 42703
|
|
851
|
+
class UndefinedFunction < ServerError; end # 42883
|
|
852
|
+
class UndefinedTable < ServerError; end # 42P01
|
|
853
|
+
class DuplicateColumn < ServerError; end # 42701
|
|
854
|
+
class DuplicateTable < ServerError; end # 42P07
|
|
855
|
+
class InsufficientPrivilege < ServerError; end # 42501
|
|
856
|
+
|
|
857
|
+
# SQLSTATE class 57 -- operator intervention
|
|
858
|
+
class QueryCanceled < ServerError; end # 57014
|
|
859
|
+
class AdminShutdown < ServerError; end # 57P01
|
|
860
|
+
|
|
861
|
+
# SQLSTATE class 08 -- connection exception
|
|
862
|
+
class ConnectionException < ServerError; end # 08000
|
|
863
|
+
class ConnectionDoesNotExist < ServerError; end # 08003
|
|
864
|
+
|
|
865
|
+
# Pool-side error (no SQLSTATE): raised by PG::Pool#checkout when
|
|
866
|
+
# the pool stays empty past the checkout timeout. Subclasses Error
|
|
867
|
+
# so callers can `rescue PG::PoolExhausted` or the broader
|
|
868
|
+
# `rescue PG::Error`. (Raising namespaced errors from instance
|
|
869
|
+
# methods became viable with matz/spinel#1041; before that, checkout
|
|
870
|
+
# surfaced exhaustion as a sentinel nil-equivalent Connection.)
|
|
871
|
+
class PoolExhausted < Error; end
|
|
872
|
+
|
|
873
|
+
# Raise the PG::Error subclass mapped from a 5-char SQLSTATE.
|
|
874
|
+
# Connection#exec / #exec_params call this (via record_error_if_any)
|
|
875
|
+
# so a failed query surfaces as a typed exception -- the ruby-pg / AR
|
|
876
|
+
# shape, where the adapter does `rescue PG::UniqueViolation` /
|
|
877
|
+
# `e.is_a?(PG::UndefinedTable)`. An unmapped SQLSTATE falls through to
|
|
878
|
+
# PG::ServerError, so `rescue PG::Error` still catches every server
|
|
879
|
+
# error. The mapping is the SQLSTATE-keyed subset the leaf classes
|
|
880
|
+
# cover (AR-coverage); add a leaf + an arm here together.
|
|
881
|
+
#
|
|
882
|
+
# Literal-class dispatch (one `raise PG::Klass` per arm) rather than
|
|
883
|
+
# `raise klass_var` -- raising a Class held in a local doesn't lower
|
|
884
|
+
# under spinel; the constant-path raise is what matz/spinel#1041 made
|
|
885
|
+
# work.
|
|
886
|
+
def self.raise_for_sqlstate(state, msg)
|
|
887
|
+
# 23 -- integrity constraint violation
|
|
888
|
+
if state == "23502"
|
|
889
|
+
raise PG::NotNullViolation, msg
|
|
890
|
+
elsif state == "23503"
|
|
891
|
+
raise PG::ForeignKeyViolation, msg
|
|
892
|
+
elsif state == "23505"
|
|
893
|
+
raise PG::UniqueViolation, msg
|
|
894
|
+
elsif state == "23514"
|
|
895
|
+
raise PG::CheckViolation, msg
|
|
896
|
+
elsif state == "23P01"
|
|
897
|
+
raise PG::ExclusionViolation, msg
|
|
898
|
+
# 25 -- invalid transaction state
|
|
899
|
+
elsif state == "25P02"
|
|
900
|
+
raise PG::InFailedSqlTransaction, msg
|
|
901
|
+
elsif state == "25006"
|
|
902
|
+
raise PG::ReadOnlySqlTransaction, msg
|
|
903
|
+
# 40 -- transaction rollback
|
|
904
|
+
elsif state == "40001"
|
|
905
|
+
raise PG::SerializationFailure, msg
|
|
906
|
+
elsif state == "40P01"
|
|
907
|
+
raise PG::DeadlockDetected, msg
|
|
908
|
+
# 42 -- syntax / access rule violation
|
|
909
|
+
elsif state == "42601"
|
|
910
|
+
raise PG::SyntaxError, msg
|
|
911
|
+
elsif state == "42703"
|
|
912
|
+
raise PG::UndefinedColumn, msg
|
|
913
|
+
elsif state == "42883"
|
|
914
|
+
raise PG::UndefinedFunction, msg
|
|
915
|
+
elsif state == "42P01"
|
|
916
|
+
raise PG::UndefinedTable, msg
|
|
917
|
+
elsif state == "42701"
|
|
918
|
+
raise PG::DuplicateColumn, msg
|
|
919
|
+
elsif state == "42P07"
|
|
920
|
+
raise PG::DuplicateTable, msg
|
|
921
|
+
elsif state == "42501"
|
|
922
|
+
raise PG::InsufficientPrivilege, msg
|
|
923
|
+
# 57 -- operator intervention
|
|
924
|
+
elsif state == "57014"
|
|
925
|
+
raise PG::QueryCanceled, msg
|
|
926
|
+
elsif state == "57P01"
|
|
927
|
+
raise PG::AdminShutdown, msg
|
|
928
|
+
# 08 -- connection exception
|
|
929
|
+
elsif state == "08000"
|
|
930
|
+
raise PG::ConnectionException, msg
|
|
931
|
+
elsif state == "08003"
|
|
932
|
+
raise PG::ConnectionDoesNotExist, msg
|
|
933
|
+
else
|
|
934
|
+
raise PG::ServerError, msg
|
|
935
|
+
end
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
# -------- Connection pool --------
|
|
939
|
+
#
|
|
940
|
+
# PG::Pool -- a fixed-size connection pool for PG::Connection
|
|
941
|
+
# instances. Mirrors ruby-pg's `PG::Pool` shape from the
|
|
942
|
+
# external pg_pool gem (and the same idea as AR's
|
|
943
|
+
# ConnectionPool): hold N pre-opened connections, hand them out
|
|
944
|
+
# via `checkout` / take them back via `checkin`, park
|
|
945
|
+
# cooperatively under `Tep::Server::Scheduled` when the free
|
|
946
|
+
# list is empty.
|
|
947
|
+
#
|
|
948
|
+
# Typical use:
|
|
949
|
+
#
|
|
950
|
+
# POOL = PG::Pool.new(ENV["DATABASE_URL"], 8)
|
|
951
|
+
#
|
|
952
|
+
# get '/users/:id' do
|
|
953
|
+
# c = POOL.checkout
|
|
954
|
+
# r = c.exec_params("SELECT name FROM users WHERE id = $1",
|
|
955
|
+
# [params[:id]])
|
|
956
|
+
# name = r.getvalue(0, 0)
|
|
957
|
+
# r.clear
|
|
958
|
+
# POOL.checkin(c)
|
|
959
|
+
# name
|
|
960
|
+
# end
|
|
961
|
+
#
|
|
962
|
+
# The block-form `with { |c| ... }` is deferred until spinel
|
|
963
|
+
# lights up instance-method typed yields (matz/spinel#628 covers
|
|
964
|
+
# the top-level def case but not instance methods); manual
|
|
965
|
+
# checkout/checkin is the v1 shape.
|
|
966
|
+
#
|
|
967
|
+
# Concurrency model:
|
|
968
|
+
#
|
|
969
|
+
# - Under prefork (Tep::Server, the default): one Pool per
|
|
970
|
+
# worker process; eagerly opens its N conns at boot. N tunes
|
|
971
|
+
# the per-worker in-flight query count.
|
|
972
|
+
# - Under Tep::Server::Scheduled: one Pool for the whole
|
|
973
|
+
# worker; checkouts that find the free list empty park via
|
|
974
|
+
# `Tep::Scheduler.pause(0.001)` until a checkin happens.
|
|
975
|
+
# Other fibers run in the meantime; eventually a checkin
|
|
976
|
+
# refills the free list and the parked fiber retries.
|
|
977
|
+
#
|
|
978
|
+
# On exhaustion (non-scheduled callers only), checkout raises
|
|
979
|
+
# PG::PoolExhausted once it has waited past @checkout_timeout_ms.
|
|
980
|
+
# This used to be a sentinel nil-equivalent return because spinel
|
|
981
|
+
# couldn't rescue module-namespaced exception classes; matz/spinel#1041
|
|
982
|
+
# fixed that, so `rescue PG::PoolExhausted` / `rescue PG::Error` now
|
|
983
|
+
# work. The scheduled path parks indefinitely (waking on checkin) and
|
|
984
|
+
# so has no exhaustion timeout -- only the spin fallback does.
|
|
985
|
+
class Pool
|
|
986
|
+
attr_accessor :url, :size, :free, :waiter_idxs, :checkout_timeout_ms
|
|
987
|
+
|
|
988
|
+
def initialize(url, size)
|
|
989
|
+
@url = url
|
|
990
|
+
@size = size
|
|
991
|
+
@checkout_timeout_ms = 5000 # 5s default; bump for slow upstreams
|
|
992
|
+
# Type-seed @free as PtrArray<PG::Connection>. PG::Connection.new
|
|
993
|
+
# with an empty conninfo returns a connection-failed instance
|
|
994
|
+
# (@pgh=-1, populated @last_error_message) rather than raising,
|
|
995
|
+
# so this is safe to run at module load even when PG isn't
|
|
996
|
+
# reachable.
|
|
997
|
+
@free = [PG::Connection.new("")]
|
|
998
|
+
@free.delete_at(0)
|
|
999
|
+
# Waiter queue: IntArray of fiber indices into Tep::APP.sched_fibers.
|
|
1000
|
+
# `checkout` parks the current fiber here when @free is empty
|
|
1001
|
+
# (under Scheduled); `checkin` resumes the oldest waiter by
|
|
1002
|
+
# setting its wake_at = -1. Type-seed with an int + delete.
|
|
1003
|
+
@waiter_idxs = [0]
|
|
1004
|
+
@waiter_idxs.delete_at(0)
|
|
1005
|
+
# Eager open of N real conns. If the URL isn't reachable, each
|
|
1006
|
+
# Connection will have @pgh=-1; the caller can check
|
|
1007
|
+
# `pool.healthy?` after construction.
|
|
1008
|
+
i = 0
|
|
1009
|
+
while i < size
|
|
1010
|
+
c = PG::Connection.new(url)
|
|
1011
|
+
@free.push(c)
|
|
1012
|
+
i += 1
|
|
1013
|
+
end
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
# True iff every pooled connection opened successfully. Use
|
|
1017
|
+
# after construction to fail loud rather than handing out
|
|
1018
|
+
# broken conns:
|
|
1019
|
+
#
|
|
1020
|
+
# POOL = PG::Pool.new(url, 8)
|
|
1021
|
+
# raise "PG unreachable" unless POOL.healthy?
|
|
1022
|
+
def healthy?
|
|
1023
|
+
i = 0
|
|
1024
|
+
while i < @free.length
|
|
1025
|
+
if !@free[i].connected?
|
|
1026
|
+
return false
|
|
1027
|
+
end
|
|
1028
|
+
i += 1
|
|
1029
|
+
end
|
|
1030
|
+
@free.length == @size
|
|
1031
|
+
end
|
|
1032
|
+
|
|
1033
|
+
def set_checkout_timeout_ms(ms)
|
|
1034
|
+
@checkout_timeout_ms = ms
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
# Acquire a connection. Returns a PG::Connection on success.
|
|
1038
|
+
#
|
|
1039
|
+
# Two paths:
|
|
1040
|
+
#
|
|
1041
|
+
# - Under Tep::Server::Scheduled: park the current fiber in
|
|
1042
|
+
# the pool's waiter queue (via Fiber.yield with a far-future
|
|
1043
|
+
# wake_at sentinel). `checkin` wakes the oldest waiter by
|
|
1044
|
+
# setting its wake_at=-1, which marks it as due on the next
|
|
1045
|
+
# scheduler tick. No busy-spin -- the scheduler runs other
|
|
1046
|
+
# fibers (handlers, accept loop, async-exec parkers) until
|
|
1047
|
+
# a checkin happens.
|
|
1048
|
+
#
|
|
1049
|
+
# - Outside scheduled context (prefork-blocking or top-level
|
|
1050
|
+
# code): fall back to a small-step pause-and-retry. Each
|
|
1051
|
+
# worker is single-threaded in prefork, so a busy
|
|
1052
|
+
# checkout-on-empty only happens if user code holds two
|
|
1053
|
+
# checkouts inside one handler. Document; rarely matters.
|
|
1054
|
+
def checkout
|
|
1055
|
+
if @free.length > 0
|
|
1056
|
+
return @free.delete_at(0)
|
|
1057
|
+
end
|
|
1058
|
+
if !Tep::Scheduler.scheduled_context?
|
|
1059
|
+
return checkout_spin_fallback
|
|
1060
|
+
end
|
|
1061
|
+
# Cooperative wait. Stash our fiber index, park, wait for
|
|
1062
|
+
# checkin to set wake_at=-1.
|
|
1063
|
+
idx = Tep::APP.sched_current
|
|
1064
|
+
@waiter_idxs.push(idx)
|
|
1065
|
+
# Far-future sentinel: the scheduler won't pick us as
|
|
1066
|
+
# time-due until checkin lowers our wake_at. Tep::Scheduler's
|
|
1067
|
+
# int-second resolution means "not soon enough to matter"
|
|
1068
|
+
# = a few hours.
|
|
1069
|
+
Tep::APP.sched_wake_at[idx] = Time.now.to_i + 86400
|
|
1070
|
+
Fiber.yield
|
|
1071
|
+
# When we resume, checkin pushed a conn to @free + woke us.
|
|
1072
|
+
# Pop it.
|
|
1073
|
+
@free.delete_at(0)
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
# Return a connection to the pool. If there's a parked waiter,
|
|
1077
|
+
# wake it (push to @free + set wake_at=-1 on the waiter's
|
|
1078
|
+
# fiber index). Otherwise just push to @free.
|
|
1079
|
+
def checkin(c)
|
|
1080
|
+
@free.push(c)
|
|
1081
|
+
if @waiter_idxs.length > 0
|
|
1082
|
+
widx = @waiter_idxs.delete_at(0)
|
|
1083
|
+
# wake_at = -1 makes the fiber the "earliest due" in the
|
|
1084
|
+
# next tick's pick (the tick comparator chooses the lowest
|
|
1085
|
+
# wake_at among the time-due set, so -1 always wins).
|
|
1086
|
+
Tep::APP.sched_wake_at[widx] = -1
|
|
1087
|
+
end
|
|
1088
|
+
0
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
# Pause-and-retry fallback for non-scheduled callers. Used by
|
|
1092
|
+
# checkout when called outside a fiber. Since pause's seconds
|
|
1093
|
+
# arg is stored as an mrb_int (rounds sub-second values to 0),
|
|
1094
|
+
# this actually busy-spins under the scheduler -- but the
|
|
1095
|
+
# branch is only taken outside scheduled context, so there's
|
|
1096
|
+
# no fiber starvation concern: the worker is single-threaded
|
|
1097
|
+
# and either has a free conn or doesn't.
|
|
1098
|
+
def checkout_spin_fallback
|
|
1099
|
+
waited_ms = 0
|
|
1100
|
+
while @free.length == 0
|
|
1101
|
+
Tep::Scheduler.pause(1) # full-second pause; non-scheduled fallback
|
|
1102
|
+
waited_ms += 1000
|
|
1103
|
+
if waited_ms >= @checkout_timeout_ms
|
|
1104
|
+
raise PG::PoolExhausted,
|
|
1105
|
+
"PG::Pool#checkout timed out after " +
|
|
1106
|
+
@checkout_timeout_ms.to_s + "ms; all " +
|
|
1107
|
+
@size.to_s + " connections in use"
|
|
1108
|
+
end
|
|
1109
|
+
end
|
|
1110
|
+
@free.delete_at(0)
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
# Diagnostic: how many connections are currently available.
|
|
1114
|
+
def available
|
|
1115
|
+
@free.length
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
# Close every connection. Call at app shutdown if needed; the
|
|
1119
|
+
# OS recovers them on process exit anyway.
|
|
1120
|
+
def close_all
|
|
1121
|
+
while @free.length > 0
|
|
1122
|
+
c = @free.delete_at(0)
|
|
1123
|
+
c.close
|
|
1124
|
+
end
|
|
1125
|
+
0
|
|
1126
|
+
end
|
|
1127
|
+
end
|
|
1128
|
+
end
|