Skip to content

Commit cf1dba8

Browse files
committed
chore: bring in retryableclient
1 parent 10cb1ba commit cf1dba8

3 files changed

Lines changed: 467 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Authzed API examples — BulkImportWithRetry
3+
*
4+
* Shows how to use RetryableClient to write a batch of relationships via
5+
* ImportBulkRelationships with automatic fallback to WriteRelationships on failure.
6+
*/
7+
package v1;
8+
9+
import com.authzed.api.v1.*;
10+
import com.authzed.grpcutil.BearerToken;
11+
import com.authzed.grpcutil.RetryableClient;
12+
import io.grpc.ManagedChannel;
13+
import io.grpc.ManagedChannelBuilder;
14+
import io.grpc.StatusRuntimeException;
15+
16+
import java.util.Arrays;
17+
import java.util.List;
18+
import java.util.concurrent.TimeUnit;
19+
import java.util.logging.Level;
20+
import java.util.logging.Logger;
21+
22+
public class BulkImportWithRetry {
23+
private static final Logger logger = Logger.getLogger(BulkImportWithRetry.class.getName());
24+
private static final String target = "grpc.authzed.com:443";
25+
private static final String token = "tc_your_token_here";
26+
27+
public static void main(String[] args) throws InterruptedException {
28+
ManagedChannel channel = ManagedChannelBuilder
29+
.forTarget(target)
30+
.useTransportSecurity()
31+
.build();
32+
33+
try {
34+
BearerToken bearerToken = new BearerToken(token);
35+
36+
// Write the schema first.
37+
SchemaServiceGrpc.SchemaServiceBlockingStub schemaService =
38+
SchemaServiceGrpc.newBlockingStub(channel).withCallCredentials(bearerToken);
39+
40+
schemaService.writeSchema(WriteSchemaRequest.newBuilder()
41+
.setSchema("""
42+
definition document {
43+
relation editor: user
44+
relation viewer: user
45+
permission edit = editor
46+
permission view = viewer + editor
47+
}
48+
definition user {}
49+
""")
50+
.build());
51+
52+
// Build the relationships to import.
53+
List<Relationship> relationships = Arrays.asList(
54+
relationship("document", "readme", "editor", "user", "alice"),
55+
relationship("document", "readme", "viewer", "user", "bob"),
56+
relationship("document", "handbook", "editor", "user", "alice")
57+
);
58+
59+
RetryableClient retryableClient = new RetryableClient(channel, bearerToken);
60+
61+
// --- Example 1: first import succeeds directly via ImportBulkRelationships ---
62+
logger.info("Importing relationships (first time)...");
63+
retryableClient.retryableBulkImportRelationships(relationships, RetryableClient.ConflictStrategy.FAIL);
64+
logger.info("Import succeeded.");
65+
66+
// --- Example 2: re-import with SKIP ignores duplicates silently ---
67+
logger.info("Re-importing with SKIP strategy...");
68+
retryableClient.retryableBulkImportRelationships(relationships, RetryableClient.ConflictStrategy.SKIP);
69+
logger.info("SKIP import completed (duplicates ignored).");
70+
71+
// --- Example 3: re-import with TOUCH overwrites via WriteRelationships ---
72+
logger.info("Re-importing with TOUCH strategy...");
73+
retryableClient.retryableBulkImportRelationships(relationships, RetryableClient.ConflictStrategy.TOUCH);
74+
logger.info("TOUCH import completed (relationships upserted).");
75+
76+
// --- Example 4: re-import with FAIL raises an error ---
77+
logger.info("Re-importing with FAIL strategy (expecting error)...");
78+
try {
79+
retryableClient.retryableBulkImportRelationships(relationships, RetryableClient.ConflictStrategy.FAIL);
80+
} catch (StatusRuntimeException e) {
81+
logger.log(Level.WARNING, "Got expected error: {0}", e.getStatus());
82+
}
83+
84+
} finally {
85+
channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
86+
}
87+
}
88+
89+
private static Relationship relationship(
90+
String resourceType, String resourceId,
91+
String relation,
92+
String subjectType, String subjectId) {
93+
return Relationship.newBuilder()
94+
.setResource(ObjectReference.newBuilder()
95+
.setObjectType(resourceType)
96+
.setObjectId(resourceId)
97+
.build())
98+
.setRelation(relation)
99+
.setSubject(SubjectReference.newBuilder()
100+
.setObject(ObjectReference.newBuilder()
101+
.setObjectType(subjectType)
102+
.setObjectId(subjectId)
103+
.build())
104+
.build())
105+
.build();
106+
}
107+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import com.authzed.api.v1.*;
2+
import com.authzed.grpcutil.BearerToken;
3+
import com.authzed.grpcutil.RetryableClient;
4+
import io.grpc.ManagedChannel;
5+
import io.grpc.ManagedChannelBuilder;
6+
import io.grpc.Status;
7+
import io.grpc.StatusRuntimeException;
8+
import org.junit.After;
9+
import org.junit.Before;
10+
import org.junit.Test;
11+
12+
import java.util.Arrays;
13+
import java.util.List;
14+
import java.util.Random;
15+
16+
import static com.authzed.grpcutil.RetryableClient.ConflictStrategy.*;
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
19+
20+
public class RetryableClientTest {
21+
22+
private ManagedChannel channel;
23+
private SchemaServiceGrpc.SchemaServiceBlockingStub schemaService;
24+
private RetryableClient retryableClient;
25+
26+
@Before
27+
public void setUp() {
28+
channel = ManagedChannelBuilder.forTarget("localhost:50051").usePlaintext().build();
29+
// Each test instance gets its own token so tests use isolated SpiceDB namespaces.
30+
String token = "tc_test_retryable_" + new Random().nextInt(100_000);
31+
BearerToken bearerToken = new BearerToken(token);
32+
schemaService = SchemaServiceGrpc.newBlockingStub(channel).withCallCredentials(bearerToken);
33+
retryableClient = new RetryableClient(channel, bearerToken);
34+
writeTestSchema();
35+
}
36+
37+
@After
38+
public void tearDown() throws InterruptedException {
39+
channel.shutdownNow().awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
40+
}
41+
42+
@Test
43+
public void testSuccessfulImport() {
44+
List<Relationship> rels = relationships("post-1", "alice");
45+
// Should complete without throwing.
46+
retryableClient.retryableBulkImportRelationships(rels, FAIL);
47+
}
48+
49+
@Test
50+
public void testSkipStrategyIgnoresConflict() {
51+
List<Relationship> rels = relationships("post-2", "alice");
52+
retryableClient.retryableBulkImportRelationships(rels, FAIL);
53+
// Re-importing the same relationships with SKIP should succeed silently.
54+
retryableClient.retryableBulkImportRelationships(rels, SKIP);
55+
}
56+
57+
@Test
58+
public void testTouchStrategyRetriesOnConflict() {
59+
List<Relationship> rels = relationships("post-3", "alice");
60+
retryableClient.retryableBulkImportRelationships(rels, FAIL);
61+
// Re-importing with TOUCH falls back to WriteRelationships and succeeds.
62+
retryableClient.retryableBulkImportRelationships(rels, TOUCH);
63+
}
64+
65+
@Test
66+
public void testFailStrategyThrowsOnConflict() {
67+
List<Relationship> rels = relationships("post-4", "alice");
68+
retryableClient.retryableBulkImportRelationships(rels, FAIL);
69+
70+
assertThatThrownBy(() -> retryableClient.retryableBulkImportRelationships(rels, FAIL))
71+
.isInstanceOf(StatusRuntimeException.class)
72+
.satisfies(e -> assertThat(((StatusRuntimeException) e).getStatus().getCode())
73+
.isEqualTo(Status.Code.ALREADY_EXISTS));
74+
}
75+
76+
@Test
77+
public void testMultipleRelationshipsImport() {
78+
List<Relationship> rels = Arrays.asList(
79+
relationship("post-multi", "writer", "alice"),
80+
relationship("post-multi", "reader", "bob")
81+
);
82+
retryableClient.retryableBulkImportRelationships(rels, FAIL);
83+
// Both relationships should be importable with TOUCH on re-import.
84+
retryableClient.retryableBulkImportRelationships(rels, TOUCH);
85+
}
86+
87+
// --- helpers ---
88+
89+
private List<Relationship> relationships(String postId, String userId) {
90+
return Arrays.asList(relationship(postId, "writer", userId));
91+
}
92+
93+
private Relationship relationship(String postId, String relation, String userId) {
94+
return Relationship.newBuilder()
95+
.setResource(ObjectReference.newBuilder()
96+
.setObjectType("post")
97+
.setObjectId(postId)
98+
.build())
99+
.setRelation(relation)
100+
.setSubject(SubjectReference.newBuilder()
101+
.setObject(ObjectReference.newBuilder()
102+
.setObjectType("user")
103+
.setObjectId(userId)
104+
.build())
105+
.build())
106+
.build();
107+
}
108+
109+
private void writeTestSchema() {
110+
String schema = "definition post {\n"
111+
+ " relation writer: user\n"
112+
+ " relation reader: user\n"
113+
+ " permission view = reader + writer\n"
114+
+ "}\n"
115+
+ "definition user {}";
116+
schemaService.writeSchema(WriteSchemaRequest.newBuilder().setSchema(schema).build());
117+
}
118+
}

0 commit comments

Comments
 (0)