-
Notifications
You must be signed in to change notification settings - Fork 121
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
Include the project role of a request token or user in project API #989
Conversation
Motivation: I think it is necessary to filter projecs to which users belong or display their roles in the screen. Currently, a user information couldn't be obtained from `/api/v1/projects` API. So we need to call `/api/v1/projects/${projectName}` to get the metadata. That means 20 API calls should be made to list 20 projects in `/app/projects`. It will cause additional loads to servers and increase page loading time. To resolve the N+1 query problem, I propose to add a role of the requested user to `ProjectDto`. Modifications: - Add `RepositoryListener` abstraction that can be continuously updated when a new change is pushed to a `Repository`. `Repository.watch()` has a similar function but it can be updated only once. - Add `ProjectMetadata` to `DefaultProject` and use `RepositoryListener` to update `ProjectMetadata` in a project when new changes in `metadata.json` - Expose user roles in the Projects page of the new web application. - Refactor `ControlPlanePlugin` with `RepositoryListener` to simply the code Result: - The project role of a request token or user are now exposed in the project API. - Users can see their roles in the projects page. - Users can filter projects by their roles in the project page.
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.
Looks promising! 👍
common/src/main/java/com/linecorp/centraldogma/internal/api/v1/ProjectRoleDto.java
Outdated
Show resolved
Hide resolved
server/src/main/java/com/linecorp/centraldogma/server/internal/api/ProjectServiceV1.java
Outdated
Show resolved
Hide resolved
server/src/main/java/com/linecorp/centraldogma/server/internal/api/ProjectServiceV1.java
Outdated
Show resolved
Hide resolved
.../src/main/java/com/linecorp/centraldogma/server/internal/storage/project/DefaultProject.java
Outdated
Show resolved
Hide resolved
<MenuItemOption value="me">Created by me</MenuItemOption> | ||
<MenuItemOption value="ALL">All</MenuItemOption> | ||
<MenuItemOption value="MEMBER">I am a member</MenuItemOption> | ||
<MenuItemOption value="CREATOR">Created by me</MenuItemOption> |
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.
Do you have any easy way to test the UI in local?
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 prefer the following steps:
- Run
ShiroCentralDogmaTestServer.main()
using IntelliJ which is faster than./gradlew runTestShiroServer
. - If the server starts successfully, execute
npm run develop
and accesshttp://127.0.0.1:3000
.
If test
is meant to run a unit test, we can run npm run test
. I temporarily disable tests that fetch data from a mock server after upgrading dependencies. Because there were compatibility issues that I haven't solved yet.
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.
Run ShiroCentralDogmaTestServer.main() using IntelliJ which is faster than ./gradlew runTestShiroServer.
If the server starts successfully, execute npm run develop and access http://127.0.0.1:3000.
That's what I was looking for. 😉
watchDogmaRepository(repositoryManager, entry.revision()); | ||
return null; | ||
}, CONTROL_PLANE_EXECUTOR); | ||
dogmaRepository.addListener(RepositoryListener.of(MetadataService.METADATA_JSON, entries -> { |
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.
👍
xds/src/main/java/com/linecorp/centraldogma/xds/internal/ControlPlanePlugin.java
Outdated
Show resolved
Hide resolved
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.
Left some minor comments but looks good to me 👍 👍 👍
import com.linecorp.centraldogma.common.Entry; | ||
import com.linecorp.centraldogma.common.Query; | ||
|
||
import io.netty.util.internal.shaded.org.jctools.queues.MessagePassingQueue.Consumer; |
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 guess you meant to use jdk's consumer
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.
😱
@JsonProperty("creator") @Nullable Author creator, | ||
@JsonProperty("url") @Nullable String url, | ||
@JsonProperty("userRole") @Nullable ProjectRole userRole, | ||
@JsonProperty("createdAt") @Nullable String createdAt) { |
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.
Question) Even in tests, is there a need for the nullable contract since server-side always seems to fill in these fields?
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.
This constructor was added for deserializing the API responses in tests.
Lines 339 to 340 in 11e03be
.asJson(new TypeReference<List<ProjectDto>>() {}) | |
.execute(); |
It wasn't used for ?status=removed
responses but made them nullable for future usage.
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.
This constructor was added for deserializing the API responses in tests.
I understand that. I meant even in the test you linked aren't the fields always being filled in (non-null) anyways?
If it is indeed nullable, I guess it's fine to leave as-is
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 linked aren't the fields always being filled in (non-null) anyways?
Right. They are non-null for normal projects. The values are nullable for removed projects.
Lines 84 to 86 in c0673c1
return CompletableFuture.supplyAsync(() -> projectApiManager.listRemovedProjects().keySet() | |
.stream() | |
.map(ProjectDto::new) |
@@ -74,9 +96,9 @@ public String url() { | |||
} | |||
|
|||
@Nullable | |||
@JsonProperty("reposUrl") |
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.
note: checked that this field doesn't seem to be used in the previous UI also, which guarantees backwards compatibility
@@ -103,7 +104,7 @@ public CompletableFuture<Collection<Token>> listTokens(User loginUser) { | |||
@StatusCode(201) | |||
@ResponseConverter(CreateApiResponseConverter.class) | |||
public CompletableFuture<HttpResult<Token>> createToken(@Param String appId, | |||
@Param boolean isAdmin, | |||
@Param @Default("false") boolean isAdmin, |
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.
Question) Is this related to changes in this PR? Or is this a separate improvement?
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 is not related to this PR.
I've fixed it in this PR together since it looked like a trivial change.
.../src/main/java/com/linecorp/centraldogma/server/internal/storage/project/DefaultProject.java
Outdated
Show resolved
Hide resolved
return ProjectRole.OWNER; | ||
} | ||
|
||
final ProjectMetadata metadata = project.metadata(); |
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.
Question) I believe Project#metadata
is updated asynchronously. This means if a user changes his/her metadata and directly navigates to /projects
, I think it is possible that the change may not be reflected depending on whether the find
in the listener has been invoked or not.
I think it should be fine since listing "my projects" can be considered non-critical, but just wanted to make sure if this is an intentional design choice as opposed to just using a write-through cache. (I haven't checked if there are any UI paths which update metadata and navigate to /projects
immediately)
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.
Good point. As you know, the consistent model of Central Dogma is eventual consistency and does not provide a transaction mechanism.
Even before this change, users may see old content right after a commit is pushed successfully. ReplicationLagTolerantCentralDogma
is a workaround to make up for the downside of eventual consistency.
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 see, I understood that even if a user creates a project and directly navigates to the main page he/she may not see her project highlighted.
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.
When I tested, I was able to see newly created projects in most cases. But it may happen if there is a significant delay. Let's leave it a known issue for now.
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! 👍 👍 👍
this.name = requireNonNull(name, "name"); | ||
this.creator = requireNonNull(creator, "creator"); | ||
createdAt = ISO_INSTANT.format(Instant.ofEpochMilli(creationTimeMillis)); | ||
url = PROJECTS_PREFIX + '/' + name; | ||
this.userRole = userRole; |
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.
rnn?
Change.ofJsonUpsert(METADATA_JSON, Jackson.valueToTree(metadata))) | ||
.join(); | ||
lastMetadataRevision = result.revision(); | ||
this.projectMetadata = metadata; |
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.
this.projectMetadata = metadata; | |
projectMetadata = metadata; |
} else { | ||
creationTimeMillis = repos.get(REPO_DOGMA).creationTimeMillis(); | ||
author = repos.get(REPO_DOGMA).author(); | ||
} | ||
success = true; | ||
} catch (Exception e) { | ||
throw new CentralDogmaException("failed to initialize internal repositories", e); |
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:
throw new CentralDogmaException("failed to initialize internal repositories", e); | |
throw new CentralDogmaException("failed to initialize internal repositories of " + name, e); |
Please also add the project name to the exception message below.
} | ||
|
||
final Revision lastRevision = entry.revision(); | ||
if (lastRevision.compareTo(lastMetadataRevision) <= 0) { |
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: assert lastMetadataRevision != null;
We might want to assign it to a local variable not to access the field twice.
Motivation:
I think it is necessary to filter projects to which users belong or display their roles on the screen.
Currently, user information can't be obtained from
/api/v1/projects
API. So we need to call/api/v1/projects/${projectName}
to get the metadata. That means 20 API calls should be made to list 20 projects in/app/projects
. It will cause additional loads to servers and increase page loading time.To resolve the N+1 query problem, I propose to add the role of a requested user to
ProjectDto
.Modifications:
RepositoryListener
abstraction that can be continuously updated when a new change is pushed to aRepository
.Repository.watch()
has a similar function but it can be updated only once.ProjectMetadata
toDefaultProject
and useRepositoryListener
to updateProjectMetadata
in a project when new changes are made inmetadata.json
ControlPlanePlugin
withRepositoryListener
to simply the code.Result: