-
Notifications
You must be signed in to change notification settings - Fork 10
Add RetryableClient implementation #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kbrwn
wants to merge
2
commits into
authzed:main
Choose a base branch
from
kbrwn:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
/* | ||
* Authzed API examples for RetryableClient | ||
*/ | ||
package v1; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Random; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import com.authzed.api.v1.ConflictStrategy; | ||
import com.authzed.api.v1.Core; | ||
import com.authzed.api.v1.Core.ObjectReference; | ||
import com.authzed.api.v1.Core.Relationship; | ||
import com.authzed.api.v1.Core.SubjectReference; | ||
import com.authzed.api.v1.RetryableClient; | ||
import com.authzed.api.v1.SchemaServiceOuterClass; | ||
import com.authzed.api.v1.SchemaServiceOuterClass.ReadSchemaRequest; | ||
import com.authzed.api.v1.SchemaServiceOuterClass.ReadSchemaResponse; | ||
import com.authzed.api.v1.SchemaServiceOuterClass.WriteSchemaRequest; | ||
import com.authzed.api.v1.SchemaServiceOuterClass.WriteSchemaResponse; | ||
import com.authzed.grpcutil.BearerToken; | ||
|
||
/** | ||
* RetryClientDemo demonstrates using RetryableClient with different conflict strategies. | ||
* | ||
* This program connects to a local SpiceDB instance and imports relationships | ||
* using each of the available conflict strategies: | ||
* - FAIL: Returns an error if duplicate relationships are found | ||
* - SKIP: Ignores duplicates and continues with import | ||
* - TOUCH: Retries the import with TOUCH semantics for duplicates | ||
*/ | ||
public class RetryClientDemo { | ||
// SpiceDB connection details | ||
private static final String SPICEDB_ADDRESS = "localhost:50051"; | ||
private static final String PRESHARED_KEY = "foobar"; | ||
|
||
// Number of relationships to create in each test | ||
private static final int RELATIONSHIPS_COUNT = 1000; | ||
|
||
public static void main(String[] args) { | ||
System.out.println("RetryClientDemo: Demonstrating RetryableClient with different conflict strategies"); | ||
|
||
// Create a RetryableClient connected to SpiceDB | ||
RetryableClient client = null; | ||
try { | ||
client = RetryableClient.newClient( | ||
SPICEDB_ADDRESS, | ||
new BearerToken(PRESHARED_KEY), | ||
true); // Using plaintext connection | ||
|
||
// Write schema for document and user types | ||
writeSchema(client); | ||
|
||
// Verify connection and read schema | ||
verifyConnection(client); | ||
|
||
// Demonstrate each conflict strategy | ||
demonstrateFailStrategy(client); | ||
demonstrateSkipStrategy(client); | ||
demonstrateTouchStrategy(client); | ||
|
||
System.out.println("\nDemo completed successfully!"); | ||
} catch (Exception e) { | ||
System.err.println("Error in RetryClientDemo: " + e.getMessage()); | ||
e.printStackTrace(); | ||
} finally { | ||
if (client != null) { | ||
client.close(); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Write a schema to SpiceDB with document and user types. | ||
*/ | ||
private static void writeSchema(RetryableClient client) { | ||
System.out.println("Writing schema to SpiceDB..."); | ||
|
||
// Define a schema with document and user types | ||
String schema = """ | ||
definition document { | ||
relation reader: user | ||
relation writer: user | ||
|
||
permission read = reader + writer | ||
permission write = writer | ||
} | ||
|
||
definition user {} | ||
"""; | ||
|
||
// Build the write schema request | ||
WriteSchemaRequest request = WriteSchemaRequest.newBuilder() | ||
.setSchema(schema) | ||
.build(); | ||
|
||
try { | ||
// Write the schema | ||
WriteSchemaResponse response = client.schemaService() | ||
.withDeadlineAfter(5, TimeUnit.SECONDS) | ||
.writeSchema(request); | ||
|
||
System.out.println("Schema written successfully!"); | ||
} catch (Exception e) { | ||
System.err.println("Failed to write schema: " + e.getMessage()); | ||
throw new RuntimeException("Could not write schema to SpiceDB", e); | ||
} | ||
} | ||
|
||
/** | ||
* Verify connection to SpiceDB by reading the schema. | ||
*/ | ||
private static void verifyConnection(RetryableClient client) { | ||
try { | ||
ReadSchemaResponse response = client.schemaService() | ||
.withDeadlineAfter(5, TimeUnit.SECONDS) | ||
.readSchema(ReadSchemaRequest.newBuilder().build()); | ||
|
||
System.out.println("\nSuccessfully connected to SpiceDB!"); | ||
System.out.println("Schema: " + response.getSchemaText()); | ||
} catch (Exception e) { | ||
System.err.println("Failed to connect to SpiceDB: " + e.getMessage()); | ||
throw new RuntimeException("Could not connect to SpiceDB", e); | ||
} | ||
} | ||
|
||
/** | ||
* Demonstrate FAIL conflict strategy. | ||
* This strategy will fail if duplicate relationships are found. | ||
*/ | ||
private static void demonstrateFailStrategy(RetryableClient client) { | ||
System.out.println("\n=== Demonstrating FAIL Strategy ==="); | ||
try { | ||
// Create unique relationships | ||
List<Relationship> relationships = generateUniqueRelationships(RELATIONSHIPS_COUNT); | ||
|
||
System.out.println("Importing " + relationships.size() + " unique relationships with FAIL strategy..."); | ||
long numLoaded = client.retryableBulkImportRelationships(relationships, ConflictStrategy.FAIL); | ||
System.out.println("Successfully imported " + numLoaded + " relationships!"); | ||
|
||
// Now try with some duplicate relationships | ||
try { | ||
System.out.println("Now attempting to import same relationships again..."); | ||
client.retryableBulkImportRelationships(relationships, ConflictStrategy.FAIL); | ||
System.out.println("ERROR: Import should have failed but didn't!"); | ||
} catch (Exception e) { | ||
System.out.println("As expected, import failed with error: " + e.getMessage()); | ||
} | ||
} catch (Exception e) { | ||
System.err.println("Error demonstrating FAIL strategy: " + e.getMessage()); | ||
e.printStackTrace(); | ||
} | ||
} | ||
|
||
/** | ||
* Demonstrate SKIP conflict strategy. | ||
* This strategy will ignore duplicates and continue with the import. | ||
*/ | ||
private static void demonstrateSkipStrategy(RetryableClient client) { | ||
System.out.println("\n=== Demonstrating SKIP Strategy ==="); | ||
try { | ||
// Create a mix of new and existing relationships | ||
List<Relationship> mixedRelationships = generateMixedRelationships(RELATIONSHIPS_COUNT / 2); | ||
|
||
System.out.println("Importing " + mixedRelationships.size() + " relationships (mix of new and existing) with SKIP strategy..."); | ||
long numLoaded = client.retryableBulkImportRelationships(mixedRelationships, ConflictStrategy.SKIP); | ||
|
||
System.out.println("Successfully processed " + numLoaded + " relationships with SKIP strategy!"); | ||
System.out.println("Note: Duplicates were skipped, but operation completed successfully"); | ||
} catch (Exception e) { | ||
System.err.println("Error demonstrating SKIP strategy: " + e.getMessage()); | ||
e.printStackTrace(); | ||
} | ||
} | ||
|
||
/** | ||
* Demonstrate TOUCH conflict strategy. | ||
* This strategy will retry the import with TOUCH semantics for duplicates. | ||
*/ | ||
private static void demonstrateTouchStrategy(RetryableClient client) { | ||
System.out.println("\n=== Demonstrating TOUCH Strategy ==="); | ||
try { | ||
// Create all new relationships to ensure initial write works | ||
List<Relationship> newRelationships = generateUniqueRelationships(RELATIONSHIPS_COUNT / 2, RELATIONSHIPS_COUNT); | ||
|
||
System.out.println("Importing " + newRelationships.size() + " new relationships..."); | ||
long numLoaded = client.retryableBulkImportRelationships(newRelationships, ConflictStrategy.TOUCH); | ||
System.out.println("Successfully imported " + numLoaded + " relationships!"); | ||
|
||
// Now use TOUCH on a mix of new and existing | ||
List<Relationship> mixedRelationships = new ArrayList<>(newRelationships); | ||
mixedRelationships.addAll(generateUniqueRelationships(RELATIONSHIPS_COUNT / 4, RELATIONSHIPS_COUNT * 2)); | ||
|
||
System.out.println("Now importing " + mixedRelationships.size() + " relationships with some duplicates using TOUCH strategy..."); | ||
numLoaded = client.retryableBulkImportRelationships(mixedRelationships, ConflictStrategy.TOUCH); | ||
|
||
System.out.println("Successfully processed " + numLoaded + " relationships with TOUCH strategy!"); | ||
System.out.println("Note: Duplicates were touched (re-written) rather than causing an error"); | ||
} catch (Exception e) { | ||
System.err.println("Error demonstrating TOUCH strategy: " + e.getMessage()); | ||
e.printStackTrace(); | ||
} | ||
} | ||
|
||
/** | ||
* Generate a list of unique relationships. | ||
*/ | ||
private static List<Relationship> generateUniqueRelationships(int count) { | ||
return generateUniqueRelationships(count, 0); | ||
} | ||
|
||
/** | ||
* Generate a list of unique relationships with IDs starting from offset. | ||
*/ | ||
private static List<Relationship> generateUniqueRelationships(int count, int offset) { | ||
List<Relationship> relationships = new ArrayList<>(count); | ||
Random random = new Random(); | ||
|
||
for (int i = 0; i < count; i++) { | ||
String docId = "doc" + (i + offset); | ||
String userId = "user" + (random.nextInt(20) + 1); // 20 possible users | ||
String relation = random.nextBoolean() ? "reader" : "writer"; | ||
|
||
relationships.add(createRelationship(docId, relation, userId)); | ||
} | ||
|
||
return relationships; | ||
} | ||
|
||
/** | ||
* Generate a mix of new and potentially duplicate relationships. | ||
*/ | ||
private static List<Relationship> generateMixedRelationships(int count) { | ||
List<Relationship> relationships = new ArrayList<>(count); | ||
Random random = new Random(); | ||
|
||
for (int i = 0; i < count; i++) { | ||
// Use a lower document ID range to increase chance of duplicates | ||
String docId = "doc" + (random.nextInt(count / 2) + 1); | ||
String userId = "user" + (random.nextInt(10) + 1); | ||
String relation = random.nextBoolean() ? "reader" : "writer"; | ||
|
||
relationships.add(createRelationship(docId, relation, userId)); | ||
} | ||
|
||
return relationships; | ||
} | ||
|
||
/** | ||
* Create a relationship between a document and user with the specified relation. | ||
*/ | ||
private static Relationship createRelationship(String docId, String relation, String userId) { | ||
return Relationship.newBuilder() | ||
.setResource(ObjectReference.newBuilder() | ||
.setObjectType("document") | ||
.setObjectId(docId) | ||
.build()) | ||
.setRelation(relation) | ||
.setSubject(SubjectReference.newBuilder() | ||
.setObject(ObjectReference.newBuilder() | ||
.setObjectType("user") | ||
.setObjectId(userId) | ||
.build()) | ||
.build()) | ||
.build(); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
import org.junit.Test; | ||
|
||
import com.authzed.api.v1.ConflictStrategy; | ||
import com.authzed.api.v1.ObjectReference; | ||
import com.authzed.api.v1.Relationship; | ||
import com.authzed.api.v1.RetryableClient; | ||
import com.authzed.api.v1.SubjectReference; | ||
import com.authzed.grpcutil.BearerToken; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
|
||
/** | ||
* Simple test for RetryableClient that doesn't use mocking. | ||
* This allows us to test the basic compilation and functionality. | ||
*/ | ||
public class SimpleRetryableClientTest { | ||
|
||
@Test | ||
public void testRetryableClientInitialization() { | ||
// Create a real RetryableClient | ||
RetryableClient client = RetryableClient.newClient( | ||
"localhost:50051", | ||
new BearerToken("test-token"), | ||
true); | ||
|
||
// If we can create the client without errors, the test passes | ||
client.close(); | ||
} | ||
|
||
@Test | ||
public void testCreateRelationship() { | ||
// Create a relationship | ||
Relationship relationship = createTestRelationship(); | ||
|
||
// Just verify the relationship object was created correctly | ||
assertEquals("document", relationship.getResource().getObjectType()); | ||
assertEquals("doc1", relationship.getResource().getObjectId()); | ||
assertEquals("viewer", relationship.getRelation()); | ||
assertEquals("user", relationship.getSubject().getObject().getObjectType()); | ||
assertEquals("user1", relationship.getSubject().getObject().getObjectId()); | ||
} | ||
|
||
/** | ||
* Helper method to create a test relationship. | ||
*/ | ||
private Relationship createTestRelationship() { | ||
return Relationship.newBuilder() | ||
.setResource(ObjectReference.newBuilder() | ||
.setObjectType("document") | ||
.setObjectId("doc1") | ||
.build()) | ||
.setRelation("viewer") | ||
.setSubject(SubjectReference.newBuilder() | ||
.setObject(ObjectReference.newBuilder() | ||
.setObjectType("user") | ||
.setObjectId("user1") | ||
.build()) | ||
.build()) | ||
.build(); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package com.authzed.api.v1; | ||
|
||
/** | ||
* ConflictStrategy represents the strategy to be used when a conflict occurs | ||
* during a bulk import of relationships into SpiceDB. | ||
*/ | ||
public enum ConflictStrategy { | ||
/** | ||
* FAIL - The operation will fail if any duplicate relationships are found. | ||
*/ | ||
FAIL, | ||
|
||
/** | ||
* SKIP - The operation will ignore duplicates and continue with the import. | ||
*/ | ||
SKIP, | ||
|
||
/** | ||
* TOUCH - The operation will retry the import with TOUCH semantics in case of duplicates. | ||
*/ | ||
TOUCH | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests construct a retryable client but they don't actually exercise any of the behavior 🤔