private void close(Status status, @Nullable Metadata metadata) {
        if (closed) {
            // 'close()' could be called twice if a call is closed with non-OK status.
            // See: https://github.com/line/armeria/issues/3799
            return;
        }
        closed = true;

        final Deadline deadline = callOptions.getDeadline();
        if (status.getCode() == Code.CANCELLED && deadline != null && deadline.isExpired()) {
            status = Status.DEADLINE_EXCEEDED;
            // Replace trailers to prevent mixing sources of status and trailers.
            metadata = new Metadata();
        } else if (metadata == null) {
            metadata = new Metadata();
        }

        if (status.getCode() == Code.DEADLINE_EXCEEDED) {
            status = status.augmentDescription("deadline exceeded after " +
                                               MILLISECONDS.toNanos(ctx.responseTimeoutMillis()) + "ns.");
        }

        final StatusAndMetadata statusAndMetadata = new StatusAndMetadata(status, metadata);

        // We want to close the user-facing ClientCall.Listener right away, before we move on to recycling
        // underlying resources.
        executor.execute(() -> {
            try (SafeCloseable ignored = ctx.push()) {
                assert listener != null;
                listener.onClose(statusAndMetadata.status(), statusAndMetadata.metadata());
            } catch (Throwable t) {
                logger.warn("Unexpected exception while closing {}", listener, t);
            }
        });

        final RequestLogBuilder logBuilder = ctx.logBuilder();
        logBuilder.responseContent(GrpcLogUtil.rpcResponse(statusAndMetadata, firstResponse), null);

        if (!status.isOk()) {
            // In the happy case, the request stream will be closed either by the client (via this.halfClose)
            // or by the server (via this.closeWhenEos). In both cases the req.close() will be called out of
            // band so we don't need to call it again.
            //
            // In the unhappy case, when exception is thrown by ClientCall.Listener, the request needs to be
            // aborted.
            req.abort(statusAndMetadata.asRuntimeException());
        }
        if (upstream != null) {
            upstream.cancel();
        }
    }
Encapsulates the state of a single client call, writing messages from the client and reading responses
from the server, passing to business logic via {@link ClientCall.Listener}.
grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java