Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nonblocking) implement non blocking mode for connections #280

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
6 changes: 5 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
arch:
- x64
- x86
nonblocking:
- true
- false
exclude:
# Don't test 32-bit on macOS
- os: macOS-latest
Expand Down Expand Up @@ -67,7 +70,7 @@ jobs:
cache-name: cache-artifacts
with:
path: ~/.julia/artifacts
key: ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
key: ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}-nonblocking-${{ matrix.nonblocking }}-${{ hashFiles('**/Project.toml') }}
restore-keys: |
${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}-
${{ runner.os }}-${{ matrix.arch }}-test-
Expand Down Expand Up @@ -100,6 +103,7 @@ jobs:
run: |
echo "PGUSER=$USER" >> $GITHUB_ENV
echo "LIBPQJL_DATABASE_USER=$USER" >> $GITHUB_ENV
echo "LIBPQJL_CONNECTION_NONBLOCKING=${{ matrix.nonblocking }}" >> $GITHUB_ENV
if: ${{ runner.os == 'macOS' }}
- name: Start Homebrew PostgreSQL service
run: pg_ctl -D /usr/local/var/postgresql@$(psql --version | cut -f3 -d' ' | cut -f1 -d.) start
Expand Down
31 changes: 22 additions & 9 deletions src/asyncresults.jl
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ function _consume(jl_conn::Connection)
# this is important?
# https://github.com/postgres/postgres/blob/master/src/interfaces/libpq/fe-exec.c#L1266
# if we used non-blocking connections we would need to check for `1` as well
if libpq_c.PQflush(jl_conn.conn) < 0
# See _flush(jl_conn::Connection) in connections.jl
if !_flush(jl_conn)
error(LOGGER, Errors.PQConnectionError(jl_conn))
end

Expand Down Expand Up @@ -231,7 +232,7 @@ end

function _multi_async_execute(jl_conn::Connection, query::AbstractString; kwargs...)
async_result = _async_execute(jl_conn; kwargs...) do jl_conn
_async_submit(jl_conn.conn, query)
_async_submit(jl_conn, query)
end

return async_result
Expand All @@ -252,9 +253,10 @@ function async_execute(
string_params = string_parameters(parameters)
pointer_params = parameter_pointers(string_params)

async_result = _async_execute(jl_conn; binary_format=binary_format, kwargs...) do jl_conn
async_result =
_async_execute(jl_conn; binary_format=binary_format, kwargs...) do jl_conn
GC.@preserve string_params _async_submit(
jl_conn.conn, query, pointer_params; binary_format=binary_format
jl_conn, query, pointer_params; binary_format=binary_format
)
end

Expand Down Expand Up @@ -289,16 +291,22 @@ function _async_execute(
return async_result
end

function _async_submit(conn_ptr::Ptr{libpq_c.PGconn}, query::AbstractString)
return libpq_c.PQsendQuery(conn_ptr, query) == 1
function _async_submit(jl_conn::Connection, query::AbstractString)
send_status = libpq_c.PQsendQuery(jl_conn.conn::Ptr{libpq_c.PGconn}, query)
if isnonblocking(jl_conn)
return _flush(jl_conn)
else
return send_status == 1
end
end

function _async_submit(
conn_ptr::Ptr{libpq_c.PGconn},
jl_conn::Connection,
query::AbstractString,
parameters::Vector{Ptr{UInt8}};
binary_format::Bool=false,
)
conn_ptr::Ptr{libpq_c.PGconn} = jl_conn.conn
num_params = length(parameters)

send_status = libpq_c.PQsendQueryParams(
Expand All @@ -311,6 +319,11 @@ function _async_submit(
zeros(Cint, num_params), # all parameters in text format
Cint(binary_format), # return result in text or binary format
)

return send_status == 1
# send_status must be 1
# if nonblock, we also want to _flush
if isnonblocking(jl_conn)
return send_status == 1 && _flush(jl_conn)
else
return send_status == 1
end
end
92 changes: 88 additions & 4 deletions src/connections.jl
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,9 @@
str::AbstractString;
throw_error::Bool=true,
connect_timeout::Real=0,
options::Dict{String, String}=CONNECTION_OPTION_DEFAULTS,

Check warning on line 269 in src/connections.jl

View workflow job for this annotation

GitHub Actions / format

[JuliaFormatter] reported by reviewdog 🐶 Raw Output: src/connections.jl:269:- options::Dict{String, String}=CONNECTION_OPTION_DEFAULTS, src/connections.jl:269:+ options::Dict{String,String}=CONNECTION_OPTION_DEFAULTS,
nonblocking::Bool=false,
kwargs...

Check warning on line 271 in src/connections.jl

View workflow job for this annotation

GitHub Actions / format

[JuliaFormatter] reported by reviewdog 🐶 Raw Output: src/connections.jl:271:- kwargs... src/connections.jl:271:+ kwargs...,
)
if options === CONNECTION_OPTION_DEFAULTS
# avoid allocating another dict in the common case
Expand Down Expand Up @@ -300,7 +301,7 @@
)

# If password needed and not entered, prompt the user
if libpq_c.PQconnectionNeedsPassword(jl_conn.conn) == 1
connection = if libpq_c.PQconnectionNeedsPassword(jl_conn.conn) == 1
push!(keywords, "password")
user = unsafe_string(libpq_c.PQuser(jl_conn.conn))
# close this connection; will open another one below with the user-provided password
Expand All @@ -309,19 +310,33 @@
pass = Base.getpass(prompt)
push!(values, read(pass, String))
Base.shred!(pass)
return handle_new_connection(
handle_new_connection(
Connection(
_connect_nonblocking(keywords, values, false; timeout=connect_timeout);
kwargs...

Check warning on line 316 in src/connections.jl

View workflow job for this annotation

GitHub Actions / format

[JuliaFormatter] reported by reviewdog 🐶 Raw Output: src/connections.jl:316:- kwargs... src/connections.jl:316:+ kwargs...,
);
throw_error=throw_error,
)
else
return handle_new_connection(
handle_new_connection(

Check warning on line 321 in src/connections.jl

View workflow job for this annotation

GitHub Actions / format

[JuliaFormatter] reported by reviewdog 🐶 Raw Output: src/connections.jl:321:- handle_new_connection( src/connections.jl:322:- jl_conn; src/connections.jl:323:- throw_error=throw_error, src/connections.jl:324:- ) src/connections.jl:321:+ handle_new_connection(jl_conn; throw_error=throw_error)
jl_conn;
throw_error=throw_error,
)
end

if nonblocking

Check warning on line 327 in src/connections.jl

View workflow job for this annotation

GitHub Actions / format

[JuliaFormatter] reported by reviewdog 🐶 Raw Output: src/connections.jl:327:- if nonblocking src/connections.jl:324:+ if nonblocking
success = libpq_c.PQsetnonblocking(connection.conn, convert(Cint, nonblocking)) == 0
if success
return connection
elseif throw_error
close(connection)
error(LOGGER, "Could not provide a non-blocking connection")
else
warn(LOGGER, "Could not provide a non-blocking connection")
return connection
end
end
return connection
end

# AbstractLock primitives:
Expand Down Expand Up @@ -672,7 +687,7 @@
"""
ConnectionOption(pq_opt::libpq_c.PQconninfoOption) -> ConnectionOption

Construct a `ConnectionOption` from a `libpg_c.PQconninfoOption`.
Construct a `ConnectionOption` from a `libpq_c.PQconninfoOption`.
"""
function ConnectionOption(pq_opt::libpq_c.PQconninfoOption)
return ConnectionOption(
Expand Down Expand Up @@ -789,3 +804,72 @@
end

socket(jl_conn::Connection) = socket(jl_conn.conn)

"""
isnonblocking(jl_conn::Connection)
pankgeorg marked this conversation as resolved.
Show resolved Hide resolved

Sets the nonblocking connection status of the PG connections.
While async_execute is non-blocking on the receiving side,
the sending side is still nonblocking without this
Returns true on success, false on failure

https://www.postgresql.org/docs/current/libpq-async.html
"""
function setnonblocking(jl_conn::Connection; nonblocking=true)
return libpq_c.PQsetnonblocking(jl_conn.conn, convert(Cint, nonblocking)) == 0
end

"""
pankgeorg marked this conversation as resolved.
Show resolved Hide resolved
Checks whether the connection is non-blocking.
Returns true if the connection is set to non-blocking, false otherwise

https://www.postgresql.org/docs/current/libpq-async.html
"""
pankgeorg marked this conversation as resolved.
Show resolved Hide resolved
function isnonblocking(jl_conn::Connection)
return libpq_c.PQisnonblocking(jl_conn.conn) == 1
pankgeorg marked this conversation as resolved.
Show resolved Hide resolved
end

"""
_flush(jl_conn::Connection)

Do the _flush dance described in the libpq docs. Required when the
connections are set to nonblocking and we want do send queries/data
without blocking.

https://www.postgresql.org/docs/current/libpq-async.html#LIBPQ-PQFLUSH
"""
function _flush(jl_conn::Connection)
local watcher = nothing
if isnonblocking(jl_conn)
watcher = FDWatcher(socket(jl_conn), true, true) # can wait for reads and writes
end
try
while true
flushstatus = libpq_c.PQflush(jl_conn.conn)
# 0 indicates success
if flushstatus == 0
return true
# -1 indicates error

Check warning on line 852 in src/connections.jl

View workflow job for this annotation

GitHub Actions / format

[JuliaFormatter] reported by reviewdog 🐶 Raw Output: src/connections.jl:852:- # -1 indicates error src/connections.jl:864:+ # -1 indicates error
elseif flushstatus < 0
return false
# 1 indicates that we could not send all data without blocking,

Check warning on line 855 in src/connections.jl

View workflow job for this annotation

GitHub Actions / format

[JuliaFormatter] reported by reviewdog 🐶 Raw Output: src/connections.jl:855:- # 1 indicates that we could not send all data without blocking, src/connections.jl:867:+ # 1 indicates that we could not send all data without blocking,
elseif flushstatus == 1
# need to wait FD
# Only applicable when the connection is in nonblocking mode
wait(watcher) # Wait for the watcher
# If it becomes write-ready, call PQflush again.
if watcher.mask.writable
continue # Call PGflush again, to send more data
end
if watcher.mask.readable
# if the stream is readable, we have to consume data from the server first.
success = libpq_c.PQconsumeInput(jl_conn.conn) == 1
!success && return false
end
end
end
finally
# Just close the watcher
!isnothing(watcher) && close(watcher)
end
end
Loading
Loading