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