-
Notifications
You must be signed in to change notification settings - Fork 26
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
chore: refactor ProxyServer to use channels instead of sockets #184
base: postgresql-dialect
Are you sure you want to change the base?
Changes from all commits
cf0c568
16eeecc
716cc11
63e6c0c
d3cb9c8
0502835
c3c8cf1
d07aaa2
acb8988
40eef3b
8639a9e
41a5906
338bf7c
2c64f88
1d1dec8
c68e11a
17d07eb
a0aa5d6
b426d22
4b279b7
7d9d1a6
1c277b3
45385c4
c1ac96b
3f91983
3a47142
b9dced6
13d4f01
cc9ad50
f53c692
9bdea78
e8f1b3e
591e90d
ff7701c
f2eeaa4
09e2da4
6ca83a4
b389979
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
on: | ||
# This allows manual activation of this action for testing. | ||
workflow_dispatch: | ||
pull_request: | ||
schedule: | ||
- cron: '0 2 * * 1,2,3,4,5' | ||
name: native-image | ||
env: | ||
GOOGLE_CLOUD_PROJECT: "span-cloud-testing" | ||
GOOGLE_CLOUD_INSTANCE: "pgadapter-testing" | ||
GOOGLE_CLOUD_DATABASE: "testdb_integration" | ||
GOOGLE_CLOUD_ENDPOINT: "spanner.googleapis.com" | ||
jobs: | ||
check-env: | ||
outputs: | ||
has-key: ${{ steps.project-id.outputs.defined }} | ||
runs-on: ubuntu-latest | ||
steps: | ||
- id: project-id | ||
env: | ||
GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} | ||
if: "${{ env.GCP_PROJECT_ID != '' }}" | ||
run: echo "::set-output name=defined::true" | ||
|
||
build-native-image: | ||
needs: [check-env] | ||
if: needs.check-env.outputs.has-key == 'true' | ||
timeout-minutes: 60 | ||
runs-on: macos-latest | ||
steps: | ||
- uses: actions/checkout@v2 | ||
- uses: graalvm/setup-graalvm@v1 | ||
with: | ||
version: 'latest' | ||
java-version: '17' | ||
components: 'native-image' | ||
github-token: ${{ secrets.GITHUB_TOKEN }} | ||
- name: Setup GCloud | ||
uses: google-github-actions/setup-gcloud@v0 | ||
with: | ||
project_id: ${{ secrets.GCP_PROJECT_ID }} | ||
service_account_key: ${{ secrets.JSON_SERVICE_ACCOUNT_CREDENTIALS }} | ||
export_default_credentials: true | ||
- name: Set up swap space | ||
if: runner.os == 'Linux' | ||
uses: pierotofy/[email protected] | ||
with: | ||
swap-size-gb: 12 | ||
- name: Build and run PGAdapter native image | ||
run: | | ||
mvn package -Passembly -Pnative-image -DskipTests | ||
cd "target/pgadapter" | ||
native-image -J-Xmx14g -H:IncludeResources=".*metadata.*json$" -jar pgadapter.jar --no-fallback | ||
./pgadapter -p ${{env.GOOGLE_CLOUD_PROJECT}} -i ${{env.GOOGLE_CLOUD_INSTANCE}} & | ||
- name: Run integration tests | ||
run: | | ||
mvn verify \ | ||
-Dclirr.skip=true \ | ||
-DskipITs=false \ | ||
-DPG_ADAPTER_PROJECT="${{env.GOOGLE_CLOUD_PROJECT}}" \ | ||
-DPG_ADAPTER_INSTANCE="${{env.GOOGLE_CLOUD_INSTANCE}}" \ | ||
-DPG_ADAPTER_DATABASE="${{env.GOOGLE_CLOUD_DATABASE}}" \ | ||
-DPG_ADAPTER_ADDRESS="localhost" \ | ||
-DPG_ADAPTER_SOCKET_DIR="/tmp" \ | ||
-DPG_ADAPTER_PORT="5432" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,7 +53,10 @@ | |
import java.io.DataOutputStream; | ||
import java.io.EOFException; | ||
import java.io.IOException; | ||
import java.net.Socket; | ||
import java.net.InetSocketAddress; | ||
import java.nio.channels.ByteChannel; | ||
import java.nio.channels.Channels; | ||
import java.nio.channels.SocketChannel; | ||
import java.security.SecureRandom; | ||
import java.text.MessageFormat; | ||
import java.util.HashMap; | ||
|
@@ -83,7 +86,15 @@ public class ConnectionHandler extends Thread { | |
private static final String CHANNEL_PROVIDER_PROPERTY = "CHANNEL_PROVIDER"; | ||
|
||
private final ProxyServer server; | ||
private final Socket socket; | ||
private final ByteChannel socketChannel; | ||
|
||
/** | ||
* Remote address of the client that is connected. This is null for Unix domain socket | ||
* connections. | ||
*/ | ||
@Nullable private final InetSocketAddress remoteAddress; | ||
|
||
private final String remoteClient; | ||
private final Map<String, IntermediatePreparedStatement> statementsMap = new HashMap<>(); | ||
private final Map<String, IntermediatePortalStatement> portalsMap = new HashMap<>(); | ||
private static final Map<Integer, IntermediateStatement> activeStatementsMap = | ||
|
@@ -103,18 +114,30 @@ public class ConnectionHandler extends Thread { | |
private WellKnownClient wellKnownClient; | ||
private ExtendedQueryProtocolHandler extendedQueryProtocolHandler; | ||
|
||
ConnectionHandler(ProxyServer server, Socket socket) { | ||
ConnectionHandler(ProxyServer server, ByteChannel byteChannel) throws IOException { | ||
super("ConnectionHandler-" + CONNECTION_HANDLER_ID_GENERATOR.incrementAndGet()); | ||
this.server = server; | ||
this.socket = socket; | ||
this.socketChannel = byteChannel; | ||
// byteChannel is an instance of SocketChannel in case of a TCP connection. | ||
// Unix domain socket connections use other implementations. The exact implementation depends on | ||
// whether the connection uses the third-party AFUNIXSocketFactory or the Java 16+ Unix domain | ||
// socket implementation. | ||
if (byteChannel instanceof SocketChannel | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are the cases when byteChannel is not of SocketChannel type? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added a comment to explain. |
||
&& ((SocketChannel) byteChannel).getRemoteAddress() instanceof InetSocketAddress) { | ||
SocketChannel socketChannel = (SocketChannel) byteChannel; | ||
this.remoteAddress = (InetSocketAddress) socketChannel.getRemoteAddress(); | ||
this.remoteClient = remoteAddress.getAddress().getHostAddress(); | ||
} else { | ||
this.remoteAddress = null; | ||
this.remoteClient = "(local)"; | ||
} | ||
this.secret = new SecureRandom().nextInt(); | ||
setDaemon(true); | ||
logger.log( | ||
Level.INFO, | ||
() -> | ||
String.format( | ||
"Connection handler with ID %s created for client %s", | ||
getName(), socket.getInetAddress().getHostAddress())); | ||
"Connection handler with ID %s created for client %s", getName(), remoteClient)); | ||
} | ||
|
||
@InternalApi | ||
|
@@ -217,15 +240,16 @@ public void run() { | |
Level.INFO, | ||
() -> | ||
String.format( | ||
"Connection handler with ID %s starting for client %s", | ||
getName(), socket.getInetAddress().getHostAddress())); | ||
"Connection handler with ID %s starting for client %s", getName(), remoteClient)); | ||
|
||
try (ConnectionMetadata connectionMetadata = | ||
new ConnectionMetadata(this.socket.getInputStream(), this.socket.getOutputStream())) { | ||
new ConnectionMetadata( | ||
Channels.newInputStream(socketChannel), Channels.newOutputStream(socketChannel))) { | ||
this.connectionMetadata = connectionMetadata; | ||
if (!server.getOptions().disableLocalhostCheck() | ||
&& !this.socket.getInetAddress().isAnyLocalAddress() | ||
&& !this.socket.getInetAddress().isLoopbackAddress()) { | ||
&& this.remoteAddress != null | ||
&& !this.remoteAddress.getAddress().isAnyLocalAddress() | ||
&& !this.remoteAddress.getAddress().isLoopbackAddress()) { | ||
handleError( | ||
PGException.newBuilder() | ||
.setMessage("This proxy may only be accessed from localhost.") | ||
|
@@ -268,15 +292,15 @@ public void run() { | |
() -> | ||
String.format( | ||
"Exception on connection handler with ID %s for client %s: %s", | ||
getName(), socket.getInetAddress().getHostAddress(), e)); | ||
getName(), remoteClient, e)); | ||
} finally { | ||
logger.log( | ||
Level.INFO, () -> String.format("Closing connection handler with ID %s", getName())); | ||
try { | ||
if (this.spannerConnection != null) { | ||
this.spannerConnection.close(); | ||
} | ||
this.socket.close(); | ||
this.socketChannel.close(); | ||
} catch (SpannerException | IOException e) { | ||
logger.log( | ||
Level.WARNING, | ||
|
@@ -332,9 +356,7 @@ public void handleTerminate() { | |
void terminate() { | ||
handleTerminate(); | ||
try { | ||
if (!socket.isClosed()) { | ||
socket.close(); | ||
} | ||
socketChannel.close(); | ||
} catch (IOException exception) { | ||
logger.log( | ||
Level.WARNING, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// Copyright 2022 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package com.google.cloud.spanner.pgadapter; | ||
|
||
import static java.net.StandardSocketOptions.SO_RCVBUF; | ||
|
||
import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.net.InetSocketAddress; | ||
import java.net.StandardProtocolFamily; | ||
import java.net.UnixDomainSocketAddress; | ||
import java.nio.channels.ServerSocketChannel; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
|
||
// If your IDE is complaining about this file not being compatible with the language level | ||
// of the project, then that is an indication that the IDE did not pick up the exclusion of this | ||
// file that is defined in the pom.xml. | ||
// This is a known issue in IntelliJ: https://youtrack.jetbrains.com/issue/IDEA-87868 | ||
// | ||
// Follow these steps to make IntelliJ ignore this compilation error: | ||
// 1. Go to Settings. | ||
// 2. Select Build, Execution, Deployment| Compiler | Excludes | ||
// 3. Add this file to the list of excludes. | ||
// See also https://www.jetbrains.com/help/idea/specifying-compilation-settings.html#5a737cfc | ||
|
||
class Java17ServerSocketFactory implements ServerSocketFactory { | ||
|
||
public Java17ServerSocketFactory() {} | ||
|
||
public ServerSocketChannel createTcpServerSocketChannel(OptionsMetadata options) | ||
throws IOException { | ||
ServerSocketChannel channel = ServerSocketChannel.open(); | ||
InetSocketAddress address = new InetSocketAddress(options.getProxyPort()); | ||
channel.configureBlocking(true); | ||
channel.bind(address, options.getMaxBacklog()); | ||
|
||
return channel; | ||
} | ||
|
||
public ServerSocketChannel creatUnixDomainSocketChannel(OptionsMetadata options, int localPort) | ||
throws IOException { | ||
File socketFile = new File(options.getSocketFile(localPort)); | ||
Path path = Path.of(socketFile.toURI()); | ||
if (socketFile.getParentFile() != null && !socketFile.getParentFile().exists()) { | ||
socketFile.mkdirs(); | ||
} | ||
|
||
// deleteOnExit() does not work when PGAdapter is built as a native image. It also does not get | ||
// deleted if the JVM crashes. We therefore need to try to delete the file at startup to ensure | ||
// we don't get a lot of 'address already in use' errors. | ||
try { | ||
Files.deleteIfExists(path); | ||
} catch (IOException ignore) { | ||
// Ignore and let the Unix domain socket subsystem throw an 'Address in use' error. | ||
} | ||
|
||
UnixDomainSocketAddress address = UnixDomainSocketAddress.of(path); | ||
ServerSocketChannel domainSocketChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX); | ||
domainSocketChannel.configureBlocking(true); | ||
domainSocketChannel.setOption(SO_RCVBUF, 65536); | ||
domainSocketChannel.bind(address, options.getMaxBacklog()); | ||
|
||
return domainSocketChannel; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it fine to to put these in public repo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The endpoint is public information. It's for example also included in the generated client for each programming language.
The project, instance and database names are also included in many other repositories.