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

Add background dot printer #784

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Add background dot printer #784

wants to merge 4 commits into from

Conversation

schneems
Copy link
Contributor

The start_timer emits dots in the background while the buildpack does work. This is important for things like downloading a file while on spotty internet so that the user knows the buildpack isn't stuck.

/// in a hotel or on a plane.
///
/// This function will transition your buildpack output to [`state::Background`].
#[allow(clippy::missing_panics_doc)]
Copy link
Contributor Author

@schneems schneems Feb 14, 2024

Choose a reason for hiding this comment

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

This is because of manually writing and flushing the IO that appends to the same line. We need to do that here because writeln_now writes...lines. I originally had two functions write_now and writeln_now and it was a tad hard to read and know which case to expect.

Almost every method in this file can panic in the same way so I don't want to alarm users that this method is different.

std::time::Duration::from_secs(1),
ansi_escape::wrap_ansi_escape_each_line(&ANSI::Dim, " ."),
ansi_escape::wrap_ansi_escape_each_line(&ANSI::Dim, "."),
ansi_escape::wrap_ansi_escape_each_line(&ANSI::Dim, ". "),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For dimming the dots:

The downside of this approach is that each character is wrapped in color codes. The upside is we never have to worry about accidentally getting in a state where the output is dimmed, and a panic happens, and the RESET is never written by accident.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the upside is worth it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The more I think about it the more I like the strong guarantee to not put our user's terminal in a "bad" state with an ANSI code.

// Test timer dot colorization
let expected = formatdoc! {"
- Background
- Installing\u{1b}[2;1m .\u{1b}[0m\u{1b}[2;1m.\u{1b}[0m\u{1b}[2;1m. \u{1b}[0m(< 0.1s)
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 agree that since the logic lives here (to specify what to dim) it should be tested here. I still want to make sure the output looks okay as human readable, so I'm testing once with and once without color codes.


assert_eq!(expected, String::from_utf8_lossy(&io));
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Intentionally not adding a changelog entry as the buildpack output is not (yet) shipped.

Copy link
Contributor

Choose a reason for hiding this comment

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

Now that the buildpack output has shipped, a changelog entry is warranted here.

@@ -0,0 +1,195 @@
//! This module is responsible for the logic involved in the printing to output while
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Review order comment: Everything in this file is internal to the crate. It's about half comments.

let mut io = match self.state.write.stop() {
Ok(io) => io,
Err(e) => std::panic::resume_unwind(e),
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This match and resuming of the panic was suggested by clippy for handling a thread result. The alternative would be manually panicking, which...either way, is a panic.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why not use let mut io = self.state.write.stop().expect("..."); instead and provide a message stating that the internal restrictions of the PrintGuard have been violated?

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'm not 100% sure, but this is the recommendation from stdlib
https://doc.rust-lang.org/std/thread/type.Result.html

The value contained in the Result::Err variant is the value the thread panicked with; that is, the argument the panic! macro was called with. Unlike with normal errors, this value doesn’t implement the Error trait.

Thus, a sensible way to handle a thread panic is to either:

propagate the panic with std::panic::resume_unwind

If we add an .expect() then clippy complains that this function doesn't have a # Panics section, but with this code recommended by stdlib docs, it doesn't. I figured it was better to go with the recommended version that requires fewer docs or lint disables to make clippy happy.

If it's eyebrow raising we could add a comment linking to those docs.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe, for consistency, the same should apply to the following code in this PR?

#[allow(clippy::missing_panics_doc)]
pub fn start_timer(mut self, s: impl AsRef<str>) -> BuildpackOutput<state::Background<W>> {
// Do not emit a newline after the message
write!(self.state.write, "{}", Self::style(s)).expect("Output error: UI writer closed");
self.state
.write
.flush()
.expect("Output error: UI writer closed");

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe, for consistency, the same should apply to the following code in this PR?

I went ahead and annotated my code with a comment from my prior musing:

If it's eyebrow raising we could add a comment linking to those docs.

I think that's good enough.

In this case we're handling a different class of error. This code is otherwise identical to writeln_now which writes and flushes, both can panic if the writer is closed. In that case I believe we want to add an annotation here otherwise the panic wouldn't have the information that it was caused while we were trying to write to the buildpack output.

Contrast this with the panic resume_unwind. In that case either the problem happened in a different thread and has one of two causes (if I understand correctly).

I'm not 100% sure of all the cases when that second one can be triggered, but even if we ignore it, i think it makes sense to propagate the panic in the background thread case, but write an except message here.

Err(e) => std::panic::resume_unwind(e),
};

writeln_now(&mut io, style::details(duration_format::human(&duration)));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is where the newline is added at the end. Everything up to this point prints inline.

end: String,
) -> PrintGuard<W>
where
W: Write + Send + 'static,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

FWIW the 'static lifetime requirement really tripped me up. It doesn't mean "lives statically for as long as the program" it means: "Contains only owned values, OR is a staticly declared value." For our use case it means we cannot pass a reference to a Write we must always pass the full struct.

write!(buffer, "{end}").expect("Writer should not be closed");
buffer.flush().expect("Writer should not be closed");

buffer
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Once the thread is joined it returns the Write struct we gave it so we can keep using it.

@schneems schneems marked this pull request as ready for review February 15, 2024 00:08
@schneems schneems requested a review from a team as a code owner February 15, 2024 00:08
@edmorley
Copy link
Member

Thank you for the PR. Just to set expectations timing wise - I won't have a chance to look at this PR until next week, since there are several things in my queue (both review-wise, and the Python WEB_CONCURRENCY work) ahead of it.

std::time::Duration::from_secs(1),
ansi_escape::wrap_ansi_escape_each_line(&ANSI::Dim, " ."),
ansi_escape::wrap_ansi_escape_each_line(&ANSI::Dim, "."),
ansi_escape::wrap_ansi_escape_each_line(&ANSI::Dim, ". "),
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the upside is worth it.

let mut io = match self.state.write.stop() {
Ok(io) => io,
Err(e) => std::panic::resume_unwind(e),
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not use let mut io = self.state.write.stop().expect("..."); instead and provide a message stating that the internal restrictions of the PrintGuard have been violated?

libherokubuildpack/src/buildpack_output/mod.rs Outdated Show resolved Hide resolved

assert_eq!(expected, String::from_utf8_lossy(&io));
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Now that the buildpack output has shipped, a changelog entry is warranted here.

@edmorley edmorley added enhancement New feature or request and removed skip changelog labels Feb 28, 2024
schneems and others added 4 commits February 28, 2024 10:37
The `start_timer` emits dots in the background while the buildpack does work. This is important for things like downloading a file while on spotty internet so that the user knows the buildpack isn't stuck.
Co-authored-by: Colin Casey <casey.colin@gmail.com>
@schneems
Copy link
Contributor Author

Updated with a changelog entry.

I didn't add one as I wasn't expecting the buildpack output changes to be released in an unusable state. I wanted to avoid any conflicts to reduce the cost of maintaining a long-lived branch until it was being actively worked on.

We might want to pre-populate the ## [Unreleased] section with added/changed/removed along with bullet points for each of the sub libraries when we release. It won't prevent changelog conflicts, but would make them faster and more intuitive to resolve.

@schneems
Copy link
Contributor Author

schneems commented Apr 3, 2024

I'm going to rebase this and then I want to merge it in. If there's a refactor needed or some changes needed, then I'll own it. If it's not up to standards or quality, then we need to figure out additional ways to scale out standard and quality measures.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request libherokubuildpack
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants