Skip to content
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

Initial ChatBot implementation #1

Merged
merged 495 commits into from
Jul 16, 2020
Merged

Conversation

yuri-sergiichuk
Copy link
Contributor

@yuri-sergiichuk yuri-sergiichuk commented Jun 9, 2020

The PR is here to address SpineEventEngine/config#403 issue.
In the PR I have drafted the initial implementation of the ChatBot applications that allows notifying Spine developers about Travis CI builds failures through the Google Chat.

The idea of the app is to use Travis CI API in order to fetch the latest master branch build status for the registered repositories and notify Spine Developers to a particular Google Chat room and thread with the build state.

@yuri-sergiichuk yuri-sergiichuk self-assigned this Jun 9, 2020
@yuri-sergiichuk
Copy link
Contributor Author

@armiol PTAL.

Copy link
Collaborator

@armiol armiol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yuri-sergiichuk please refer to the latest Spine build scripts and modify this project accordingly:

  • Kotlin in Gradle scripts instead of Groovy;
  • application version defined in a separate file;
  • (perhaps, in future PRs) static code analysis is configured: Error Prone, PMD, Checkstyle;
  • (consult with @dmdashenkov) there is already Gradle 6.5, but Dmitry mentioned there was an issue in their release which breaks some of our Spine plugin code.

In addition to the project setup:

  • Public API must be documented.
  • Avoid the wildcard imports. Use the latest inspection profile from Spine. Also, it helps to use the most recent IDEA (I am using the latest EAP at the moment) — it detects more warnings and errors, some of them even inline as you type.
  • Please have a README.md stating the name of the application AND some details. So if you wish to speak of Micronaut, do that in the document body, not in the title. Also, choosing Java 11 needs some explanation in terms of the target deployment environment — I don't know the plan, so I am not able to comment on this matter.
  • (optional) I am not sure about Log4J. To me, this is a very early decision.

Copy link
Collaborator

@armiol armiol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yuri-sergiichuk I am 33% done. Please see my comments I left so far.

.gitignore Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated
```

The application will be available at `127.0.0.1:${LOCAL_PORT}` (e.g. `127.0.0.1:9090`).
Locally-supplied GCP credentials are mount into the image directly.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README.md Outdated
The application will be available at `127.0.0.1:${LOCAL_PORT}` (e.g. `127.0.0.1:9090`).
Locally-supplied GCP credentials are mount into the image directly.

For detailed ADC credentials guide for Docker see example Cloud Run [guide][cloud-run-local-guide].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea what an ADC is. I would put it in its full form first followed by a short form when mentioning for the first time. E.g. "Black-box bounded context (BBBC) is ...". Otherwise, you risk losing a reader who is going to leave for googling at this point.

// Explicitly states the encoding of the source and test source files, ensuring
// correct execution of the `javac` task.
options.encoding = "UTF-8"
options.compilerArgs.addAll(listOf("-Xlint:unchecked", "-Xlint:deprecation"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, we are migrating Spine to the compiler configuration in which we fail if there is at least one warning. So you may add -Werror here as well.

});
}

private static RegisterRepository newRegisterRepoCommand(Repository repository,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment to a similar use case above.

_info().log("Performing initial application state initialization.");
var client = ChatBotClient.inProcessClient(Application.SERVER_NAME);
var spineOrgId = newOrganizationId("SpineEventEngine");
registerOrganization(client, spineOrgId, spaceName);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we validate spaceName? Consider having a Protobuf message which would travel from this point further.

*/
@Get
public String initWatchedResources(@QueryValue String spaceName) {
_info().log("Performing initial application state initialization.");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would log the query value as well.

return "Successfully initialized";
}

private static void registerWatchedRepos(ChatBotClient client, OrganizationId spineOrgId) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please not have any operations in controllers other than taking and validating the parameters and sending out responses?

/**
* Creates a new {@link BuildState} update message of of the supplied state and the thread name.
*
* <p>If the thread name is empty, assumes that the update message should be sent
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And what if it's worse than empty? What it is null?

@yuri-sergiichuk
Copy link
Contributor Author

@armiol, PTAL again.

Copy link
Collaborator

@armiol armiol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yuri-sergiichuk 75% reviewed. See my comments.

const val junit5 = "5.6.2"
const val truth = "1.0.1"
const val micronaut = "2.0.0.RC1"
const val spineGcloud = "1.5.0"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review the Spine versions declared in different files. I.e. the gcloud-java version is here, and bootstrap version is in build.gradle.kts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a limitation of the Kotlin build scripts support that we're facing right now.

In order to be able to utilize Kotlin DSL pre-compiled plugins and configure build logic in e.g. java-convention.gradle.kts, the plugins we're using should be added as dependencies to build.gradle.kts. And we're not able to import and use deps.kt there, cause we're just going to compile it.
It's a chicken and an egg problem here.

The workaround here maybe be to fall back to awkward buildScript configurations and multiple apply from constructs.

I created a separate issue for this: #3.

cloudbuild.yaml Outdated
@@ -0,0 +1,24 @@
# TODO:2020-06-17:yuri-sergiichuk: the build fails in the cloud with
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review. The rule is either to address the todo item, or have an issue in the repository linked here.

var chatEventJson = decodeBase64Json(message.getData());
_debug().log("Received a new chat event: %s", chatEventJson);
ChatEvent chatEvent = Json.fromJson(chatEventJson, ChatEvent.class);
var client = ChatBotClient.inProcessClient(Application.SERVER_NAME);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting this line, the code does not belong to a controller.

.command(checkRepositoryBuild)
.onStreamingError(RepositoriesController::throwProcessingError)
.post();
subscriptions.forEach(botClient::cancelSubscription);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand this trick with canceling the subscriptions. Please document this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's done accordingly to the @apiNote in the post method:

@apiNote Subscriptions should be cancelled to free up client and server resources
     *         required for their maintenance. It is not possible to cancel the returned
     *         subscription in an automatic way because of the following.
     *         Subscriptions by nature are asynchronous and infinite requests.
     *         Even that we know expected types of the events produced by the command, only the
     *         client code "knows" how many of them it expects. Also, some events may not arrive
     *         because of communication or business logic reasons. That's why the returned
     *         subscriptions should be cancelled by the client code when it no longer needs it.

* Creates a new {@link BuildState} update message of of the supplied state and the thread
* name.
*
* <p>If the thread name is {@code null} or empty, assumes that the update message should be
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's settle down on what means what. I would say that if the thread name was passed as null, then a new thread should have been engaged. The empty string value should be treated as an input mistake.


// ID of the organization the repository is related to if any.
OrganizationId organization = 5;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(-)


// The display name (only if the space is a room).
string display_name = 5;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(-)

@yuri-sergiichuk
Copy link
Contributor Author

@armiol, PTAL again.

Trigger builds as recovered only if we previously had them as failed in the process.
… itself.

E.g. grab Repository and branch name from the response
Copy link
Collaborator

@alexander-yevsyukov alexander-yevsyukov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see my comments.

@yuri-sergiichuk
Copy link
Contributor Author

@alexander-yevsyukov @armiol PTAL again.

Copy link
Collaborator

@alexander-yevsyukov alexander-yevsyukov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, with some comments and suggestions

private final HangoutsChat chat;

GoogleChat(HangoutsChat chat) {
this.chat = chat;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check for null input.

@@ -0,0 +1,25 @@
syntax = "proto3";

package google.pubsub.v1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs fixing. We cannot pretend we're Google.


/**
* Creates an instance of the {@linkplain Build.State build state} of out its
* string representation.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* string representation.
* string representation, ignoring the case.

return buildState;
}
}
throw newIllegalArgumentException(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're sure that Build.State is named according to Java conventions, you can probably do this:

return Enum.valueOf(Build.State.class, state.toUpperCase());

It would throw IllegalArgumentException if there's no a member with the name.

var message = pushNotification.getMessage();
var chatEventJson = message.getData()
.toStringUtf8();
_debug().log("Received a new chat event: %s", chatEventJson);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We finish most of the log messages with periods because they are sentences. If you expect JSON to be long, how about having it starting from a new line then?

final class DistributedDelivery {

/** The number of shards used for the signal delivery. **/
private static final int NUMBER_OF_SHARDS = 50;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@armiol, do we need that many for this kind of app?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexander-yevsyukov @yuri-sergiichuk That seems to be a proper amount.

/**
* Creates a new {@code Slug} with the specified {@code value}.
*/
public static Slug create(String value) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you name these methods repoSlug(), orgSlug(), and newSlug(), you'll be able to use static imports and avoid the name of the utility class. So, instead of

var s = Slugs.forRepo(value);

... you'll get:

var s = repoSlug(value);

... which sounds a bit more English.

//
// E.g. it could be a GitHub repository slug in the format `SpineEventEngine/base`.
//
string value = 1 [(required) = true];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a regexp option here, please?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there is a particular regexp to cover all the possible variants. The slug can be an organization slug or a repository slug or some other slug as well. Basically, as stated, it is a human-readable identifier.

@yuri-sergiichuk yuri-sergiichuk merged commit b525f06 into master Jul 16, 2020
@yuri-sergiichuk yuri-sergiichuk deleted the initial-chat-implementation branch July 16, 2020 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants