Initial implementation of global-refdb for Spanner
It builds in-tree and uses the Spanner Java client to connect with a
Cloud Spanner emulator instance. If a refs table does not exist in
the given Spanner instance, the plugin creates one.
Change-Id: I6c9cd6dd0d6afad4e77bde99dd20b22b638263b9
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..bb0e739
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,119 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+ "//tools/bzl:plugin.bzl",
+ "gerrit_plugin",
+ "PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS"
+)
+
+gerrit_plugin(
+ name = "spanner-refdb",
+ srcs = glob(["src/main/java/**/*.java"]),
+ manifest_entries = [
+ "Gerrit-PluginName: spanner-refdb",
+ "Gerrit-Module: com.googlesource.gerrit.plugins.spannerrefdb.Module",
+ "Implementation-Title: Google Cloud Spanner shared ref-database implementation",
+ "Implementation-URL: https://gerrit.googlesource.com/plugins/spanner-refdb",
+ ],
+ resources = glob(["src/main/resources/**/*"]),
+ deps = [
+ "@animal-sniffer-annotations//jar",
+ "@auto-value-annotations//jar",
+ "@auto-value//jar",
+ "@checker-qual//jar",
+ "@commons-codec//jar",
+ "@commons-logging//jar",
+ "@conscrypt-openjdk-uber//jar",
+ "@error_prone_annotations//jar",
+ "@failureaccess//jar",
+ "@gax-grpc//jar",
+ "@gax-httpjson//jar",
+ "@gax//jar",
+ "@global-refdb//jar",
+ "@google-api-client-json//jar",
+ "@google-api-client//jar",
+ "@google-api-common//jar",
+ "@google-auth-library-credentials//jar",
+ "@google-auth-library-oauth2-http//jar",
+ "@google-cloud-core-grpc//jar",
+ "@google-cloud-core//jar",
+ "@google-cloud-spanner//jar",
+ "@google-cloud-storage//jar",
+ "@google-http-client-gson//jar",
+ "@google-http-client//jar",
+ "@grpc-alts//jar",
+ "@grpc-api//jar",
+ "@grpc-auth//jar",
+ "@grpc-context//jar",
+ "@grpc-core//jar",
+ "@grpc-gcp//jar",
+ "@grpc-google-cloud-spanner-admin-database-v1//jar",
+ "@grpc-google-cloud-spanner-admin-instance-v1//jar",
+ "@grpc-google-cloud-spanner-v1//jar",
+ "@grpc-google-common-protos//jar",
+ "@grpc-googleapis//jar",
+ "@grpc-grpclb//jar",
+ "@grpc-netty//jar",
+ "@grpc-protobuf-lite//jar",
+ "@grpc-protobuf//jar",
+ "@grpc-rls//jar",
+ "@grpc-services//jar",
+ "@grpc-stub//jar",
+ "@grpc-xds//jar",
+ "@guava//jar",
+ "@httpclient//jar",
+ "@httpcore//jar",
+ "@j2objc-annotations//jar",
+ "@jackson-annotations//jar",
+ "@jackson-core//jar",
+ "@jackson-databind//jar",
+ "@jackson-dataformat-cbor//jar",
+ "@javax-annotation-api//jar",
+ "@joda-time//jar",
+ "@listenablefuture//jar",
+ "@netty-buffer//jar",
+ "@netty-codec-http//jar",
+ "@netty-codec-http2//jar",
+ "@netty-codec//jar",
+ "@netty-common//jar",
+ "@netty-handler-proxy//jar",
+ "@netty-handler//jar",
+ "@netty-resolver//jar",
+ "@netty-transport-native-unix-common//jar",
+ "@netty-transport//jar",
+ "@opencensus-api//jar",
+ "@opencensus-contrib-grpc-util//jar",
+ "@opencensus-contrib-http-util//jar",
+ "@opencensus-proto//jar",
+ "@perfmark-api//jar",
+ "@proto-google-cloud-spanner-admin-database-v1//jar",
+ "@proto-google-cloud-spanner-admin-instance-v1//jar",
+ "@proto-google-cloud-spanner-v1//jar",
+ "@proto-google-common-protos//jar",
+ "@proto-google-iam-v1//jar",
+ "@protobuf-java-util//jar",
+ "@protobuf-java//jar",
+ "@re2j//jar",
+ "@threetenbp//jar",
+ ],
+)
+
+junit_tests(
+ name = "spanner-refdb_tests",
+ srcs = glob(["src/test/java/**/*.java"]),
+ resources = glob(["src/main/resources/**/*"]),
+ tags = ["spanner-refdb"],
+ deps = [
+ ":spanner-refdb__plugin_test_deps",
+ ],
+)
+
+java_library(
+ name = "spanner-refdb__plugin_test_deps",
+ testonly = 1,
+ visibility = ["//visibility:public"],
+ exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+ ":spanner-refdb__plugin",
+ "@global-refdb//jar",
+ ],
+)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..54136ee
--- /dev/null
+++ b/README.md
@@ -0,0 +1,58 @@
+# Gerrit Cloud Spanner refdb
+
+This plugin provides an implementation of the Gerrit global refdb backed by
+[Cloud Spanner](https://cloud.google.com/spanner).
+
+Requirements for using this plugin are:
+
+- Gerrit v3.6 or later
+- Cloud spanner as a provisioned instance
+- Optional, for testing: a locally hosted
+[emulator](https://cloud.google.com/spanner/docs/emulator)
+
+## Typical use case
+
+A global refdb implementation is a requirement for a Gerrit multi-master
+scenario in a multi-site setup to ensure refs are updated consistently across
+multiple Gerrit primary sites.
+
+Refer to the
+[Gerrit multi-site plugin](https://gerrit.googlesource.com/plugins/multi-site/+/master/DESIGN.md)
+for more details on the high level architecture.
+
+## Use with Cloud Spanner emulator
+
+The [in-memory Cloud Spanner emulator](https://cloud.google.com/spanner/docs/emulator)
+can be used during development. There are several limitations of the emulator
+that differ from the production service.
+
+An easy way to run the emulator is to use gcloud CLI and Docker. Following the
+install instructions [here](https://cloud.google.com/spanner/docs/emulator#install)
+will provide a local instance that the plugin can access. The gcloud CLI
+commands will be sent to the emulator instead of the production service. A test
+spanner instance and database will need to be created.
+
+```
+gcloud spanner instances create test-instance --config=emulator-config --description="Test Instance" --nodes=1
+
+gcloud spanner databases create global-refdb --instance=test-instance
+```
+
+There are several
+[limitations](https://cloud.google.com/spanner/docs/emulator#limitations)
+of the emulator which differ from the production service.
+
+## Use with simulated multi-site setup
+
+In order to easily set up and begin testing the spanner-refdb plugin, the
+multi-site plugin's
+[local environment setup](https://gerrit.googlesource.com/plugins/multi-site/+/refs/heads/master/setup_local_env/)
+can be used.
+
+This simulates a multi-site setup using two local gerrit instances. Build the
+release.war to include spanner-refdb and follow the linked example setup.
+
+## Configuration options
+
+Configuration details can be found in the
+[spanner-refdb config documentation](src/main/resources/Documentation/config.md).
\ No newline at end of file
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..c10bb0d
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,398 @@
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+JACKSON_VER = "2.10.4"
+CLOUD_SPANNER_VER = "6.43.2"
+NETTY_VER = "4.1.79.Final"
+GRPC_VER = "1.53.0"
+PROTOBUF_JAVA_VER = "3.21.12"
+OPENCENSUS_VER = "0.31.1"
+OPENCENSUS_PROTO_VER = "0.2.0"
+GOOGLE_HTTP_CLIENT_VER = "1.43.1"
+CLOUD_CORE_VER = "2.12.0"
+GOOGLE_COMMON_PROTOS_VERS = "2.14.2"
+
+def external_plugin_deps():
+ maven_jar(
+ name = "error_prone_annotations",
+ sha1 = "89b684257096f548fa39a7df9fdaa409d4d4df91",
+ artifact = "com.google.errorprone:error_prone_annotations:2.18.0",
+ )
+ maven_jar(
+ name = "j2objc-annotations",
+ sha1 = "ba035118bc8bac37d7eff77700720999acd9986d",
+ artifact = "com.google.j2objc:j2objc-annotations:1.3",
+ )
+ maven_jar(
+ name = "opencensus-api",
+ sha1 = "66a60c7201c2b8b20ce495f0295b32bb0ccbbc57",
+ artifact = "io.opencensus:opencensus-api:" + OPENCENSUS_VER,
+ )
+ maven_jar(
+ name = "opencensus-contrib-http-util",
+ sha1 = "3c13fc5715231fadb16a9b74a44d9d59c460cfa8",
+ artifact = "io.opencensus:opencensus-contrib-http-util:" + OPENCENSUS_VER,
+ )
+ maven_jar(
+ name = "opencensus-contrib-grpc-util",
+ sha1 = "89b1d2f8f64749256983446cbdfae25da5450de7",
+ artifact = "io.opencensus:opencensus-contrib-grpc-util:" + OPENCENSUS_VER,
+ )
+ maven_jar(
+ name = "opencensus-proto",
+ sha1 = "c05b6b32b69d5d9144087ea0ebc6fab183fb9151",
+ artifact = "io.opencensus:opencensus-proto:" + OPENCENSUS_PROTO_VER,
+ )
+ maven_jar(
+ name = "protobuf-java-util",
+ sha1 = "1a800bf7976d939217c8d91ed9a17d7a78bf2187",
+ artifact = "com.google.protobuf:protobuf-java-util:" + PROTOBUF_JAVA_VER,
+ )
+ maven_jar(
+ name = "protobuf-java",
+ sha1 = "5589e79a33cb6509f7e681d7cf4fc59d47c51c71",
+ artifact = "com.google.protobuf:protobuf-java:" + PROTOBUF_JAVA_VER,
+ )
+ maven_jar(
+ name = "proto-google-iam-v1",
+ sha1 = "d60d59913b82994f416c120946dcf6457ab109e4",
+ artifact = "com.google.api.grpc:proto-google-iam-v1:1.9.2",
+ )
+ maven_jar(
+ name = "failureaccess",
+ sha1 = "1dcf1de382a0bf95a3d8b0849546c88bac1292c9",
+ artifact = "com.google.guava:failureaccess:1.0.1",
+ )
+ maven_jar(
+ name = "listenablefuture",
+ sha1 = "c949a840a6acbc5268d088e47b04177bf90b3cad",
+ artifact = "com.google.guava:listenablefuture:1.0",
+ )
+ maven_jar(
+ name = "checker-qual",
+ sha1 = "eeefd4af42e2f4221d145c1791582f91868f99ab",
+ artifact = "org.checkerframework:checker-qual:3.31.0",
+ )
+ maven_jar(
+ name = "conscrypt-openjdk-uber",
+ sha1 = "d858f142ea189c62771c505a6548d8606ac098fe",
+ artifact = "org.conscrypt:conscrypt-openjdk-uber:2.5.2",
+ )
+ maven_jar(
+ name = "grpc-auth",
+ sha1 = "c0d249372ab1409ec9cb7fcfe8d432abdd41f070",
+ artifact = "io.grpc:grpc-auth:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "grpc-grpclb",
+ sha1 = "e2b9e60fcbea543da23cddf985c6a2cc21ab217e",
+ artifact = "io.grpc:grpc-grpclb:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "grpc-api",
+ sha1 = "968fdbb4369cbf03302b1137e52e97c4d49bd548",
+ artifact = "io.grpc:grpc-api:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "grpc-rls",
+ sha1 = "3c0cdb42525ddaabb80bbc59be447ed917b60eea",
+ artifact = "io.grpc:grpc-rls:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "grpc-alts",
+ sha1 = "6cff34ea1066b657e7d5b8592ad40e1546ab6f13",
+ artifact = "io.grpc:grpc-alts:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "grpc-services",
+ sha1 = "4e25f3e96eb18fcb251e53697cce549c4ede737d",
+ artifact = "io.grpc:grpc-services:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "grpc-googleapis",
+ sha1 = "c649adfdf38e82f4a949650d0788aaf53e384cc8",
+ artifact = "io.grpc:grpc-googleapis:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "grpc-xds",
+ sha1 = "b198b90963693e79c5ec2dd2c1b5537608439c36",
+ artifact = "io.grpc:grpc-xds:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "grpc-protobuf",
+ sha1 = "41f37de10ef5e4a30cbb9ef89405b864b9342f5e",
+ artifact = "io.grpc:grpc-protobuf:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "grpc-protobuf-lite",
+ sha1 = "e54a4ff36048a5a28d033791187b985dc5abbe73",
+ artifact = "io.grpc:grpc-protobuf-lite:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "grpc-stub",
+ sha1 = "391663182785fdb6f92486f432e927cff60c6bcf",
+ artifact = "io.grpc:grpc-stub:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "grpc-core",
+ sha1 = "be781334b80a78f11044813fba123826d2df4b6b",
+ artifact = "io.grpc:grpc-core:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "grpc-context",
+ sha1 = "70aa77c3e715b6f22a42c466eb2f48031bc468fb",
+ artifact = "io.grpc:grpc-context:" + GRPC_VER,
+ )
+ maven_jar(
+ name = "gax-httpjson",
+ sha1 = "2aad7ab9500a4188f8d0967244c21ce6a023cd80",
+ artifact = "com.google.api:gax-httpjson:0.108.2",
+ )
+ maven_jar(
+ name = "gax-grpc",
+ sha1 = "f72d1ac348b29ed121e0166d875e8d22db7c3a7a",
+ artifact = "com.google.api:gax-grpc:2.23.3",
+ )
+ maven_jar(
+ name = "gax",
+ sha1 = "34442411d1ae36c61508189c4d198ef69dbde4ab",
+ artifact = "com.google.api:gax:2.23.3",
+ )
+ maven_jar(
+ name = "grpc-gcp",
+ sha1 = "14b35eed70f1911fdc046c4a21ee403386828c6a",
+ artifact = "com.google.cloud:grpc-gcp:1.4.1",
+ )
+ maven_jar(
+ name = "google-cloud-spanner",
+ sha1 = "2891af6853c7024a3e6e11267d4b4f040c70b75d",
+ artifact = "com.google.cloud:google-cloud-spanner:" + CLOUD_SPANNER_VER,
+ )
+ maven_jar(
+ name = "proto-google-cloud-spanner-v1",
+ artifact = "com.google.api.grpc:proto-google-cloud-spanner-v1:" + CLOUD_SPANNER_VER,
+ sha1 = "6fc3d73b1c83d87120a08240a756280e682a4824",
+ )
+ maven_jar(
+ name = "proto-google-cloud-spanner-admin-instance-v1",
+ sha1 = "892c44c816642ac4b9e107154449227904d839cf",
+ artifact = "com.google.api.grpc:proto-google-cloud-spanner-admin-instance-v1:" + CLOUD_SPANNER_VER,
+ )
+ maven_jar(
+ name = "proto-google-cloud-spanner-admin-database-v1",
+ sha1 = "965f6da2cb0ff8dd435ae65d4f7f64adad535211",
+ artifact = "com.google.api.grpc:proto-google-cloud-spanner-admin-database-v1:" + CLOUD_SPANNER_VER,
+ )
+ maven_jar(
+ name = "grpc-google-cloud-spanner-admin-instance-v1",
+ sha1 = "f8a3ff352412226536da5b3d080b6fc399d47a60",
+ artifact = "com.google.api.grpc:grpc-google-cloud-spanner-admin-instance-v1:" + CLOUD_SPANNER_VER,
+ )
+ maven_jar(
+ name = "grpc-google-cloud-spanner-v1",
+ sha1 = "817a2bab6270f83567da23ee26979d3493fadb94",
+ artifact = "com.google.api.grpc:grpc-google-cloud-spanner-v1:" + CLOUD_SPANNER_VER,
+ )
+ maven_jar(
+ name = "grpc-google-cloud-spanner-admin-database-v1",
+ sha1 = "5cf39124432f1301040f9eb83fd82c5de0d6bd15",
+ artifact = "com.google.api.grpc:grpc-google-cloud-spanner-admin-database-v1:" + CLOUD_SPANNER_VER,
+ )
+ maven_jar(
+ name = "google-cloud-storage",
+ artifact = "com.google.cloud:google-cloud-storage:1.63.0",
+ sha1 = "339f65e2a0557a6c40b8f79a7a4a43fb6f77ee27",
+ )
+ maven_jar(
+ name = "google-api-client",
+ artifact = "com.google.api-client:google-api-client:2.2.0",
+ sha1 = "10e53fd4d987e37190432e896bdaa62e8ea2c628",
+ )
+ maven_jar(
+ name = "google-api-common",
+ artifact = "com.google.api:api-common:2.6.3",
+ sha1 = "b47c8a2c25005b94c4c43884e0a78bf965de17d8",
+ )
+ maven_jar(
+ name = "threetenbp",
+ artifact = "org.threeten:threetenbp:1.6.5",
+ sha1 = "9c83a035274df46c998b3bcb0710489ad7788abd",
+ )
+ maven_jar(
+ name = "google-http-client",
+ artifact = "com.google.http-client:google-http-client:" + GOOGLE_HTTP_CLIENT_VER,
+ sha1 = "2ef9413e65319ac448534b424522a2c48087d884",
+ )
+ maven_jar(
+ name = "google-http-client-gson",
+ artifact = "com.google.http-client:google-http-client-gson:" + GOOGLE_HTTP_CLIENT_VER,
+ sha1 = "06f000784f9813ee33f976e42822d98ffacf3dbd",
+ )
+ maven_jar(
+ name = "google-api-client-util",
+ artifact = "com.google.api.client:google-api-client-util:1.2.3-alpha",
+ sha1 = "5613058f449666061dbab2f824fb72b9de441b4d",
+ )
+ maven_jar(
+ name = "google-api-client-gson",
+ artifact = "com.google.api-client:google-api-client-gson:2.2.0",
+ sha1 = "319d792c7df5164ba33f5741b642071915d8c046",
+ )
+ maven_jar(
+ name = "google-api-client-json",
+ artifact = "com.google.api.client:google-api-client-json:1.2.3-alpha",
+ sha1 = "96fc0eb531f79ca3d1cbb347e91b61df5743f051",
+ )
+ maven_jar(
+ name = "google-cloud-core-grpc",
+ artifact = "com.google.cloud:google-cloud-core-grpc:" + CLOUD_CORE_VER,
+ sha1 = "14fba1b12a8069aeb1bcd60006c76496ff766f6a",
+ )
+ maven_jar(
+ name = "google-cloud-core",
+ sha1 = "5f31f9f7fb5f3fdf14112bbd93c00af24463c60b",
+ artifact = "com.google.cloud:google-cloud-core:" + CLOUD_CORE_VER,
+ )
+ maven_jar(
+ name = "jackson-databind",
+ artifact = "com.fasterxml.jackson.core:jackson-databind:" + JACKSON_VER,
+ sha1 = "76e9152e93d4cf052f93a64596f633ba5b1c8ed9",
+ )
+ maven_jar(
+ name = "jackson-dataformat-cbor",
+ artifact = "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:" + JACKSON_VER,
+ sha1 = "c854bb2d46138198cb5d4aae86ef6c04b8bc1e70",
+ )
+ maven_jar(
+ name = "jackson-annotations",
+ artifact = "com.fasterxml.jackson.core:jackson-annotations:" + JACKSON_VER,
+ sha1 = "6ae6028aff033f194c9710ad87c224ccaadeed6c",
+ )
+ maven_jar(
+ name = "jackson-core",
+ artifact = "com.fasterxml.jackson.core:jackson-core:" + JACKSON_VER,
+ sha1 = "8796585e716440d6dd5128b30359932a9eb74d0d",
+ )
+ maven_jar(
+ name = "joda-time",
+ artifact = "joda-time:joda-time:2.10.10",
+ sha1 = "29e8126e31f41e5c12b9fe3a7eb02e704c47d70b",
+ )
+ maven_jar(
+ name = "google-auto-value-annotations",
+ sha1 = "9e5162c15f6033c524134cba05a5e93dc1d37c4b",
+ artifact = "com.google.auto.value:auto-value-annotations:1.10.1",
+ )
+ maven_jar(
+ name = "perfmark-api",
+ sha1 = "ef65452adaf20bf7d12ef55913aba24037b82738",
+ artifact = "io.perfmark:perfmark-api:0.26.0",
+ )
+ maven_jar(
+ name = "animal-sniffer-annotations",
+ sha1 = "911f763493163e03d750b369cc162085db09b46b",
+ artifact = "org.codehaus.mojo:animal-sniffer-annotations:1.22",
+ )
+ maven_jar(
+ name = "re2j",
+ sha1 = "a13e879fd7971738d06020fefeb108cc14e14169",
+ artifact = "com.google.re2j:re2j:1.6",
+ )
+ maven_jar(
+ name = "commons-codec",
+ sha1 = "49d94806b6e3dc933dacbd8acb0fdbab8ebd1e5d",
+ artifact = "commons-codec:commons-codec:1.15",
+ )
+ maven_jar(
+ name = "javax-annotation-api",
+ sha1 = "934c04d3cfef185a8008e7bf34331b79730a9d43",
+ artifact = "javax.annotation:javax.annotation-api:1.3.2",
+ )
+ maven_jar(
+ name = "commons-logging",
+ sha1 = "4bfc12adfe4842bf07b657f0369c4cb522955686",
+ artifact = "commons-logging:commons-logging:1.2",
+ )
+ maven_jar(
+ name = "proto-google-common-protos",
+ sha1 = "84af747eac0f286fccaac5602b42b95088415c2b",
+ artifact = "com.google.api.grpc:proto-google-common-protos:" + GOOGLE_COMMON_PROTOS_VERS,
+ )
+ maven_jar(
+ name = "grpc-google-common-protos",
+ sha1 = "4d50b3b00a397f6786d2bd8bccdb68dbc652e5de",
+ artifact = "com.google.api.grpc:grpc-google-common-protos:" + GOOGLE_COMMON_PROTOS_VERS,
+ )
+
+ # use unshaded grpc-netty
+ maven_jar(
+ name = "grpc-netty",
+ sha1 = "0dd6088f048629487133ca3549264938d8d7443f",
+ artifact = "io.grpc:grpc-netty:" + GRPC_VER,
+ )
+
+ # transitive dependencies of grpc-netty
+ maven_jar(
+ name = "netty-common",
+ sha1 = "2814bd465731355323aba0fdd22163bfce638a75",
+ artifact = "io.netty:netty-common:" + NETTY_VER,
+ )
+ maven_jar(
+ name = "netty-buffer",
+ sha1 = "6c014412b599489b1db27c6bc08d8a46da94e397",
+ artifact = "io.netty:netty-buffer:" + NETTY_VER,
+ )
+ maven_jar(
+ name = "netty-transport",
+ sha1 = "6cc2b49749b4fbcc39c687027e04e65e857552a9",
+ artifact = "io.netty:netty-transport:" + NETTY_VER,
+ )
+ maven_jar(
+ name = "netty-codec",
+ sha1 = "18f5b02af7ca611978bc28f2cb58cbb3b9b0f0ef",
+ artifact = "io.netty:netty-codec:" + NETTY_VER,
+ )
+ maven_jar(
+ name = "netty-handler",
+ sha1 = "2dc22423c8ed19906615fb936a5fcb7db14a4e6c",
+ artifact = "io.netty:netty-handler:" + NETTY_VER,
+ )
+ maven_jar(
+ name = "netty-resolver",
+ sha1 = "55ecb1ff4464b56564a90824a741c3911264aaa4",
+ artifact = "io.netty:netty-resolver:" + NETTY_VER,
+ )
+ maven_jar(
+ name = "netty-codec-http",
+ sha1 = "882c70bc0a30a98bf3ce477f043e967ac026044c",
+ artifact = "io.netty:netty-codec-http:" + NETTY_VER,
+ )
+ maven_jar(
+ name = "netty-codec-http2",
+ sha1 = "0eeffab0cd5efb699d5e4ab9b694d32fef6694b3",
+ artifact = "io.netty:netty-codec-http2:" + NETTY_VER,
+ )
+ maven_jar(
+ name = "netty-handler-proxy",
+ sha1 = "054aace8683de7893cf28d4aab72cd60f49b5700",
+ artifact = "io.netty:netty-handler-proxy:" + NETTY_VER,
+ )
+ maven_jar(
+ name = "netty-transport-native-unix-common",
+ sha1 = "731937caec938b77b39df932a8da8aaca8d5ec05",
+ artifact = "io.netty:netty-transport-native-unix-common:" + NETTY_VER,
+ )
+ maven_jar(
+ name = "google-auth-library-credentials",
+ sha1 = "8ae88acbd572253c99c36e22b17efff388db3d12",
+ artifact = "com.google.auth:google-auth-library-credentials:1.16.0",
+ )
+ maven_jar(
+ name = "google-auth-library-oauth2-http",
+ sha1 = "4eeb81450d4f61725cbd047393ff5243c7cc6bd7",
+ artifact = "com.google.auth:google-auth-library-oauth2-http:1.16.0",
+ )
+ maven_jar(
+ name = "global-refdb",
+ sha1 = "00b6b0f39b3c8fc280a19d91fb0681954ebccd02",
+ artifact = "com.gerritforge:global-refdb:3.3.2.1",
+ )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/Configuration.java
new file mode 100644
index 0000000..bbdea23
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/Configuration.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.googlesource.gerrit.plugins.spannerrefdb;
+
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.SpannerOptions;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+class Configuration {
+ private final SpannerOptions options;
+ private final DatabaseId databaseId;
+
+ @Inject
+ Configuration(PluginConfigFactory configFactory, @PluginName String pluginName) {
+ PluginConfig pluginConfig = configFactory.getFromGerritConfig(pluginName);
+ String spannerInstance = pluginConfig.getString("spannerInstance", "test-instance");
+ String spannerDatabase = pluginConfig.getString("spannerDatabase", "global-refdb");
+ this.options = SpannerOptions.newBuilder().build();
+ this.databaseId = DatabaseId.of(options.getProjectId(), spannerInstance, spannerDatabase);
+ }
+
+ final SpannerOptions getOptions() {
+ return options;
+ }
+
+ final DatabaseId getDatabaseId() {
+ return databaseId;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/Module.java b/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/Module.java
new file mode 100644
index 0000000..fc66666
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/Module.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.googlesource.gerrit.plugins.spannerrefdb;
+
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase;
+import com.google.cloud.spanner.DatabaseAdminClient;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.inject.Provides;
+import com.google.inject.Scopes;
+import com.google.inject.Singleton;
+
+class Module extends LifecycleModule {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ @Override
+ protected void configure() {
+ logger.atInfo().log("Configuring Cloud Spanner for global refdb.");
+ DynamicItem.bind(binder(), GlobalRefDatabase.class)
+ .to(SpannerRefDatabase.class)
+ .in(Scopes.SINGLETON);
+ listener().to(SpannerLifeCycleManager.class);
+ }
+
+ @Provides
+ @Singleton
+ public DatabaseAdminClient createDatabaseAdminClient(Configuration configuration) {
+ return configuration.getOptions().getService().getDatabaseAdminClient();
+ }
+
+ @Provides
+ @Singleton
+ public DatabaseClient createDatabaseClient(Configuration configuration) {
+ return configuration.getOptions().getService().getDatabaseClient(configuration.getDatabaseId());
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/SpannerLifeCycleManager.java b/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/SpannerLifeCycleManager.java
new file mode 100644
index 0000000..5a95276
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/SpannerLifeCycleManager.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.googlesource.gerrit.plugins.spannerrefdb;
+
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
+import com.google.api.gax.longrunning.OperationFuture;
+import com.google.cloud.spanner.DatabaseAdminClient;
+import com.google.cloud.spanner.ErrorCode;
+import com.google.cloud.spanner.SpannerException;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
+import java.util.Arrays;
+
+@Singleton
+class SpannerLifeCycleManager implements LifecycleListener {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final Configuration configuration;
+ private DatabaseAdminClient dbAdminClient;
+
+ @Inject
+ SpannerLifeCycleManager(Configuration configuration, DatabaseAdminClient dbAdminClient) {
+ this.configuration = configuration;
+ this.dbAdminClient = dbAdminClient;
+ }
+
+ @Override
+ public void start() {
+ createTable(
+ "CREATE TABLE refs ("
+ + "project STRING(MAX) NOT NULL,"
+ + "ref STRING(MAX) NOT NULL,"
+ + "value STRING(MAX) NOT NULL"
+ + ") PRIMARY KEY (project, ref)");
+ createTable(
+ "CREATE TABLE locks ("
+ + "project STRING(MAX) NOT NULL,"
+ + "ref STRING(MAX) NOT NULL"
+ + ") PRIMARY KEY (project, ref)");
+ }
+
+ @Override
+ public void stop() {}
+
+ private void createTable(String statement) {
+ try {
+ OperationFuture<Void, UpdateDatabaseDdlMetadata> op =
+ dbAdminClient.updateDatabaseDdl(
+ configuration.getDatabaseId().getInstanceId().getInstance(),
+ configuration.getDatabaseId().getDatabase(),
+ Arrays.asList(statement),
+ null);
+ op.get();
+ logger.atInfo().log(
+ "Created table in database [%s] with statement %s",
+ configuration.getDatabaseId(), statement);
+ } catch (Exception e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof SpannerException) {
+ ErrorCode code = ((SpannerException) cause).getErrorCode();
+ if (code != ErrorCode.FAILED_PRECONDITION) {
+ throw new GlobalRefDbSystemError("Failed to create table", e);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/SpannerRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/SpannerRefDatabase.java
new file mode 100644
index 0000000..94a34f8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/spannerrefdb/SpannerRefDatabase.java
@@ -0,0 +1,245 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.googlesource.gerrit.plugins.spannerrefdb;
+
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase;
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException;
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.Key;
+import com.google.cloud.spanner.KeySet;
+import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.Struct;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.inject.Inject;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Optional;
+import javax.inject.Singleton;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+@Singleton
+public class SpannerRefDatabase implements GlobalRefDatabase {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final DatabaseClient dbClient;
+
+ @Inject
+ SpannerRefDatabase(DatabaseClient dbClient) {
+ this.dbClient = dbClient;
+ }
+
+ @Override
+ public boolean isUpToDate(Project.NameKey project, Ref ref) throws GlobalRefDbLockException {
+ String valueInSpanner = get(project, ref.getName());
+ if (valueInSpanner == null) {
+ // If it doesn't exist, assume up to date (will populate on next compareAndPut)
+ return true;
+ }
+ ObjectId objectIdInSharedRefDb = ObjectId.fromString(valueInSpanner);
+ boolean isUpToDate = objectIdInSharedRefDb.equals(ref.getObjectId());
+ if (!isUpToDate) {
+ logger.atFine().log(
+ "%s:%s is out of sync: local=%s spanner=%s",
+ project, ref.getName(), ref.getObjectId(), objectIdInSharedRefDb);
+ }
+ return isUpToDate;
+ }
+
+ @Override
+ public boolean compareAndPut(Project.NameKey project, Ref ref, ObjectId newValue)
+ throws GlobalRefDbSystemError {
+ newValue = Optional.ofNullable(newValue).orElse(ObjectId.zeroId());
+ ObjectId currValue = Optional.ofNullable(ref.getObjectId()).orElse(ObjectId.zeroId());
+ return doCompareAndPut(project, ref.getName(), currValue.getName(), newValue.name());
+ }
+
+ @Override
+ public <T> boolean compareAndPut(
+ Project.NameKey project, String refName, T expectedValue, T newValue)
+ throws GlobalRefDbSystemError {
+ String newRefValue =
+ Optional.ofNullable(newValue).map(Object::toString).orElse(ObjectId.zeroId().getName());
+ String expectedRefValue =
+ Optional.ofNullable(expectedValue)
+ .map(Object::toString)
+ .orElse(ObjectId.zeroId().getName());
+
+ return doCompareAndPut(project, refName, expectedRefValue, newRefValue);
+ }
+
+ /**
+ * Run the compare and put to set the ref value to the newValue if and only if the current ref
+ * value is expectedValue. Its primary key is the refPath.
+ *
+ * <p>If the ref doesn't exist at all, insert it and return true. If the ref exists and its
+ * current value is the same as expected current value, update and return true. Otherwise, return
+ * false.
+ *
+ * @param project - the project
+ * @param refName - the ref
+ * @param expectedValue - the expected value to be found in the global-refdb
+ * @param newValue - the new value to put in the global-refdb if the expectedValue is present
+ * @return success
+ * @throws GlobalRefDbSystemError
+ */
+ private boolean doCompareAndPut(
+ Project.NameKey project, String refName, String expectedValue, String newValue)
+ throws GlobalRefDbSystemError {
+ logger.atInfo().log(
+ "Do compare and put for %s / %s. Expected value: %s. New value: %s",
+ project.get(), refName, expectedValue, newValue);
+
+ return dbClient
+ .readWriteTransaction()
+ .run(
+ transaction -> {
+ Struct row =
+ transaction.readRow(
+ "refs", Key.of(project.get(), refName), Arrays.asList("value"));
+ // If the newValue is zeroId, delete the row if the expected value is correct
+ if (newValue.equals(ObjectId.zeroId().name())) {
+ if (row == null) {
+ return true;
+ }
+ if (row.getString(0).equals(expectedValue)) {
+ transaction.buffer(Mutation.delete("refs", Key.of(project.get(), refName)));
+ return true;
+ }
+ return false;
+ }
+ // If the row is null, the row doesn't exist and will be created.
+ // If the value is the expected value, the row should be updated.
+ if (row == null || row.getString(0).equals(expectedValue)) {
+ transaction.buffer(
+ Mutation.newInsertOrUpdateBuilder("refs")
+ .set("project")
+ .to(project.get())
+ .set("ref")
+ .to(refName)
+ .set("value")
+ .to(newValue)
+ .build());
+ return true;
+ }
+ return false;
+ });
+ }
+
+ public class Lock implements AutoCloseable {
+
+ private final String projectName;
+ private final String refName;
+
+ public Lock(String projectName, String refName) throws GlobalRefDbLockException {
+ // Attempt to create the lock here
+ this.projectName = projectName;
+ this.refName = refName;
+ try {
+ dbClient
+ .readWriteTransaction()
+ .run(
+ transaction -> {
+ transaction.buffer(
+ Mutation.newInsertBuilder("locks")
+ .set("project")
+ .to(projectName)
+ .set("ref")
+ .to(refName)
+ .build());
+ return true;
+ });
+ } catch (Exception e) {
+ logger.atSevere().withCause(e).log(
+ "Failed to acquire lock for %s %s", projectName, refName);
+ throw new GlobalRefDbLockException(projectName, refName, e);
+ }
+ }
+
+ @Override
+ public void close() throws Exception {
+ dbClient.write(
+ Collections.singletonList(Mutation.delete("locks", Key.of(projectName, refName))));
+ }
+ }
+
+ @Override
+ public AutoCloseable lockRef(Project.NameKey project, String refName)
+ throws GlobalRefDbLockException {
+ logger.atInfo().log("Attempting to lock %s %s.", project.get(), refName);
+ // TODO: Some sort of heartbeat that removes stale locks if they haven't been
+ return new Lock(project.get(), refName);
+ }
+
+ @Override
+ public boolean exists(Project.NameKey project, String refName) {
+ logger.atInfo().log("Checking if ref %s %s exists.", project.get(), refName);
+ try (ResultSet resultSet =
+ dbClient
+ .singleUse()
+ .executeQuery(
+ Statement.newBuilder("SELECT * FROM refs WHERE project = @project and ref = @ref")
+ .bind("project")
+ .to(project.get())
+ .bind("ref")
+ .to(refName)
+ .build())) {
+ return resultSet.next();
+ }
+ }
+
+ @Override
+ public void remove(Project.NameKey project) throws GlobalRefDbSystemError {
+ // Delete all rows with matching project
+ logger.atInfo().log("Removing project %s from global-refdb", project.get());
+ dbClient.write(
+ Collections.singletonList(
+ Mutation.delete("refs", KeySet.prefixRange(Key.of(project.get())))));
+ }
+
+ private String get(Project.NameKey project, String refName) throws GlobalRefDbSystemError {
+ try (ResultSet resultSet =
+ dbClient
+ .singleUse()
+ .executeQuery(
+ Statement.newBuilder(
+ "SELECT value FROM refs WHERE project = @project and ref = @ref")
+ .bind("project")
+ .to(project.get())
+ .bind("ref")
+ .to(refName)
+ .build())) {
+ if (resultSet.next()) {
+ return resultSet.getString("value");
+ }
+ return null;
+ } catch (Exception e) {
+ throw new GlobalRefDbSystemError(String.format("Cannot get value for %s", project.get()), e);
+ }
+ }
+
+ @Override
+ public <T> Optional<T> get(Project.NameKey project, String refName, Class<T> clazz)
+ throws GlobalRefDbSystemError {
+ // The only usage of this is as a String but the API requires Optional<T>?
+ // It looks like zookeeper has/uses a StringDeserializerFactory/StringDeserializer to deal
+ // with this, while the aws implementation opts for this.
+ return Optional.ofNullable((T) get(project, refName));
+ }
+}
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..fb978d1
--- /dev/null
+++ b/src/main/resources/Documentation/build.md
@@ -0,0 +1,20 @@
+# Build
+
+The spanner-refdb plugin can be built in-tree in Gerrit's `/plugins` path.
+
+The `plugins/external_plugin_deps.bzl` file will need to be updated to match
+or contain `spanner-refdb/external_plugin_deps.bzl`.
+
+```
+git clone --recursive https://gerrit.googlesource.com/gerrit
+cd gerrit
+git clone "https://gerrit.googlesource.com/plugins/spanner-refdb" plugins/spanner-refdb
+ln -sf plugins/spanner-refdb/external_plugin_deps.bzl plugins/.
+bazelisk build plugins/spanner-refdb
+```
+
+The output is created in
+
+```
+gerrit/bazel-bin/plugins/spanner-refdb/spanner-refdb.jar
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..b9409e0
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,21 @@
+Configuration
+=========================
+
+The plugin can be configured as usual in the gerrit.config. All configuration
+is optional, if no values are provided these will be the defaults.
+
+```
+[plugin "spanner-refdb"]
+ gcpProject = test-project
+ spannerInstance = test-instance
+ spannerDatabase = global-refdb
+```
+
+`plugin.spanner-refdb.gcpProject`
+: Optional. The name of the google cloud platform project.
+
+`plugin.spanner-refdb.spannerInstance`
+: Optional. The name of the spanner instance.
+
+`plugin.spanner-refdb.spannerDatabase`
+: Optional. The name of the spanner database.
\ No newline at end of file