-
Notifications
You must be signed in to change notification settings - Fork 1k
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
New ServiceContext per user REST request #2705
Conversation
A new ServiceContext is created per user request so it can be used to initialize Kafka and SR services using user credentials.
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.
Thanks @spena! This is definitely a step in the right direction, but I think we need to address the issue of matching the sandbox engine with correctly sandboxed service contexts (see my comment below #2705 (comment))
return new UserServiceContext(DefaultServiceContext.create(ksqlConfig)); | ||
} | ||
|
||
// TODO: Create a UserServiceContext using user credentials |
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.
Since this class isn't used in this PR let's introduce it in a future one.
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.
Oh, mmm, I left this code from another patch I have. I was splitting the other patch with only the refactor, and I forgot to remove it. Thanks for catching this.
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.Properties; | ||
import java.util.*; |
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.
nit: I think our guidelines prefer explicit imports
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.
Thanks Intellij optimizations hehe.
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.
You can disable that in your intellij settings: https://stackoverflow.com/a/3589885/2258040
@@ -252,8 +241,12 @@ | |||
|
|||
private String streamName; | |||
|
|||
private List<ServiceContext> serviceContexts = new ArrayList<>(); |
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.
It looks like all usages are contains
, why not use a Set
?
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.
Right. It's better to use Set
@@ -288,8 +281,14 @@ public void setUp() throws IOException, RestClientException { | |||
|
|||
streamName = KsqlIdentifierTestUtil.uniqueIdentifierName(); | |||
|
|||
when(schemaInjectorFactory.apply(any())).thenReturn(sandboxSchemaInjector); | |||
when(schemaInjectorFactory.apply(serviceContext)).thenReturn(schemaInjector); | |||
when(schemaInjectorFactory.apply(any())).thenAnswer(inv -> { |
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.
I think it would be clearer to use mockito hamcrest here:
when(schemaInjectorFactory.apply(argThat(isIn(serviceContexts)))).thenReturn(schemaInjector);
when(schemaInjectorFactory.apply(any())).thenReturn(sandboxSchemaInjector);
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.
Nice. I like that proposal better.
@Test | ||
public void shouldUseOneServiceContextPerKsqlRequest() { | ||
// Given: | ||
final ServiceContext sc = spy(TestServiceContext.create()); |
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.
let's avoid spies if possible! this leads me to two questions:
- why do we need a spy instead of a mock? can you mock whatever is necessary or use a simpler KSQL statement list?
- I'm not sure I see the connection between the test title (use one per request) and what we're actually asserting (was closed once). What if I created 10 different service contexts during that time and just closed one of them?
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.
Aa, you got the same question I had after submitting this patch. I was thinking on mocking the ServiceContext supplier and the ServiceContext itself, and keep track of the # of times a get()
and close
get called. Would that be a good approach?
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.
I like that approach :D
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.
I found a way to do it.
done.
private void setUpKsqlResource() { | ||
ksqlResource = new KsqlResource( | ||
ksqlConfig, | ||
ksqlEngine, | ||
serviceContext, | ||
() -> createTestServiceContext(), |
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.
() -> createTestServiceContext(), | |
this::createTestServiceContext, |
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.
done
doAnswer(inv -> { | ||
Object m = inv.getMock(); | ||
if (serviceContexts.contains(m)) { | ||
serviceContexts.remove(m); |
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.
what are we using this for? I don't see any tests that require us to properly maintain the list of service contexts?
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.
It's not testing anything. I used just to remove objects from the list of serviceContexts. Do you think this is not necessary? I suspect now that it might not, as this is just a test class, and list is cleared on every new test case.
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.
I will remove this code instead of fixing it. Thes are just tests. The serviceContexts list is cleared on every test, so it is harmless to leave them there.
activenessRegistrar); | ||
} | ||
|
||
private StreamedQueryResource givenStreamedQueryResource(final ServiceContext serviceContext) { |
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.
same comment as above, this should take in a Supplier<ServiceContext>
and we should call it from above
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.
done
queryResource.streamQuery(VALID_STREAM_REQUEST); | ||
|
||
// Then: | ||
Mockito.verify(sc, Mockito.times(1)).close(); |
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.
same comment, this assert doesn't really match the title of the test
final List<ParsedStatement> statements, | ||
final Map<String, Object> propertyOverrides, | ||
final String sql | ||
) { | ||
requireSandbox(serviceContext); | ||
|
||
validateOverriddenConfigProperties(propertyOverrides); | ||
final KsqlExecutionContext ctx = requireSandbox(snapshotSupplier.get()); | ||
final Injector injector = injectorFactory.apply(ctx, serviceContext); |
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.
as part of this refactor, I think we also need to make sure that KsqlEngine#getServiceContext
is removed - otherwise it circumvents the refactor. There is some complexity here, though, which is that when we create a sandbox we need to make sure that operations on the sandbox always use the same sandbox service context (e.g. there is a 1:1 mapping between sandbox engines and sandboxed ServiceContexts). Let me know if you want to get on a call to discuss this.
See discussion here for context: #2436 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that because some of the sandbox clients (e.g. the topic client) accumulate state when you have multiple statements in 1 request?
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.
I'll throw an idea into this discussion and then run away: one option here would be to pass an interface into KsqlEngine.createSandbox that takes the current service context and returns a sandbox, that createSandbox could use internally to create the sandbox service context. This way we keep the relationship between the execution context and service context in tact.
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.
There is a plan to refactor the KsqlEngine
to remove the internal ServiceContext
. I'm not sure how that would be done yet, but I wanted to submit this partial PR due to several files I had to change. Or do you think there is a relationship on this PR?
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.
Based on a manual scan I think this might be safe independent of removing it from the KsqlEngine
. For now, anywhere that is necessary that they are paired it is being accessed through KsqlEngine#getServiceContext
.
|
||
|
||
public KsqlResource( | ||
final KsqlConfig ksqlConfig, | ||
final KsqlEngine ksqlEngine, | ||
final ServiceContext serviceContext, | ||
final Supplier<ServiceContext> serviceContextFactory, |
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.
Will this be a Supplier longer term? Seems like we want to pass in some info about the principal associated with the request.
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.
I initially did not add the supplier. There was a new DefaultServiceContext created on every request. However, I did not know how to verify it on the unit tests. So, I ended up using this supplier for that.
The plan for the next patch is to use a Function instead, where I can pass user information, like serviceContextFactory.apply(principal)
.
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.
Took a pass through. The change mostly makes sense to me. Left some comments inline.
It isn't necessary to make the engine refactor for this PR.
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.
LGTM - the new unit tests are nice :)
import java.util.LinkedList; | ||
import java.util.Map; | ||
import java.util.Scanner; | ||
import java.util.*; |
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.
same here
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.
LGTM - one question inline
@@ -244,6 +245,7 @@ public void configureBaseApplication( | |||
new JacksonMessageBodyProvider(JsonMapper.INSTANCE.mapper); | |||
config.register(jsonProvider); | |||
config.register(JsonParseExceptionMapper.class); | |||
config.register(new KsqlRestServiceContextBinder(ksqlConfig)); |
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.
How will this work when we need to configure it differently for each request?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The binder will bind the factory using the class type instead of the instance of the class. Like:
bindFactory(KsqlRestServiceContextFactory.class).to(ServiceContext.class);
This way, the binder will make sure to create a new KsqlRestServiceContextFactory
for every new REST request. I then can get the user token and other security credentials using dependency injection in the factory.
One thing, though, is that I cannot add the KsqlConfig
with this code. The only way I will do it is by calling a static method of the factory to set this during the Binder initialization. After that, every new instance of the Factory will have the ksqlConfig set + the user credentials.
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.
LGTM!
Description
A new
ServiceContext
is created per user request so it can be used to initialize Kafka and SR services using user credentials. This affects onlyKsqlResource
andStreamedQueryResources
classes.This is a partial refactoring to allow KSQL impersonate users for every new request. A follow-up PR will include refactoring the
KsqlEngine
to remove the internalServiceContext
.Testing done
Update all tests to use the new
KsqlResource
andStreamedQueryResources
classes.Add a few tests to validate a
ServiceContext
is used and closed per request.Reviewer checklist