Mining/pkg/mining/errors.go
snider d533164893 fix: Comprehensive code hardening from 4-domain Opus review
Error Handling:
- Fix silent Write() error in WebSocket (events.go)
- Add error context to transport handshake messages
- Check os.MkdirAll error in zip extraction (miner.go)
- Explicitly ignore io.Copy errors on drain with comments
- Add retry logic (2 attempts) for transient stats collection failures

Resource Lifecycle:
- Add shutdown mechanism to DigestAuth goroutine
- Call Service.Stop() on context cancellation
- Add NodeService transport cleanup to Service.Stop()
- Fix WriteStdin goroutine leak on timeout with non-blocking send

API Design:
- Add profile validation (name, miner type required)
- Return 404 instead of 500 for missing profile PUT
- Make DELETE profile idempotent (return success if not found)
- Standardize error responses in node_service.go handlers

Observability:
- Add logging for P2P GetAllStats failures
- Add request ID correlation helper for handler logs
- Add logging for miner process exits (xmrig_start.go)
- Rate limit debug logs in transport hot path (1 in 100)
- Add metrics infrastructure with /metrics endpoint

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 14:33:30 +00:00

248 lines
7.5 KiB
Go

package mining
import (
"fmt"
"net/http"
)
// Error codes for the mining package
const (
ErrCodeMinerNotFound = "MINER_NOT_FOUND"
ErrCodeMinerExists = "MINER_EXISTS"
ErrCodeMinerNotRunning = "MINER_NOT_RUNNING"
ErrCodeInstallFailed = "INSTALL_FAILED"
ErrCodeStartFailed = "START_FAILED"
ErrCodeStopFailed = "STOP_FAILED"
ErrCodeInvalidConfig = "INVALID_CONFIG"
ErrCodeInvalidInput = "INVALID_INPUT"
ErrCodeUnsupportedMiner = "UNSUPPORTED_MINER"
ErrCodeNotSupported = "NOT_SUPPORTED"
ErrCodeConnectionFailed = "CONNECTION_FAILED"
ErrCodeServiceUnavailable = "SERVICE_UNAVAILABLE"
ErrCodeTimeout = "TIMEOUT"
ErrCodeDatabaseError = "DATABASE_ERROR"
ErrCodeProfileNotFound = "PROFILE_NOT_FOUND"
ErrCodeProfileExists = "PROFILE_EXISTS"
ErrCodeInternalError = "INTERNAL_ERROR"
ErrCodeInternal = "INTERNAL_ERROR" // Alias for consistency
)
// MiningError is a structured error type for the mining package
type MiningError struct {
Code string // Machine-readable error code
Message string // Human-readable message
Details string // Technical details (for debugging)
Suggestion string // What to do next
Retryable bool // Can the client retry?
HTTPStatus int // HTTP status code to return
Cause error // Underlying error
}
// Error implements the error interface
func (e *MiningError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s: %s (%v)", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
// Unwrap returns the underlying error
func (e *MiningError) Unwrap() error {
return e.Cause
}
// WithCause adds an underlying error
func (e *MiningError) WithCause(err error) *MiningError {
e.Cause = err
return e
}
// WithDetails adds technical details
func (e *MiningError) WithDetails(details string) *MiningError {
e.Details = details
return e
}
// WithSuggestion adds a suggestion for the user
func (e *MiningError) WithSuggestion(suggestion string) *MiningError {
e.Suggestion = suggestion
return e
}
// IsRetryable returns whether the error is retryable
func (e *MiningError) IsRetryable() bool {
return e.Retryable
}
// StatusCode returns the HTTP status code for this error
func (e *MiningError) StatusCode() int {
if e.HTTPStatus == 0 {
return http.StatusInternalServerError
}
return e.HTTPStatus
}
// NewMiningError creates a new MiningError
func NewMiningError(code, message string) *MiningError {
return &MiningError{
Code: code,
Message: message,
HTTPStatus: http.StatusInternalServerError,
}
}
// Predefined error constructors for common errors
// ErrMinerNotFound creates a miner not found error
func ErrMinerNotFound(name string) *MiningError {
return &MiningError{
Code: ErrCodeMinerNotFound,
Message: fmt.Sprintf("miner '%s' not found", name),
Suggestion: "Check that the miner name is correct and that it is running",
Retryable: false,
HTTPStatus: http.StatusNotFound,
}
}
// ErrMinerExists creates a miner already exists error
func ErrMinerExists(name string) *MiningError {
return &MiningError{
Code: ErrCodeMinerExists,
Message: fmt.Sprintf("miner '%s' is already running", name),
Suggestion: "Stop the existing miner first or use a different configuration",
Retryable: false,
HTTPStatus: http.StatusConflict,
}
}
// ErrMinerNotRunning creates a miner not running error
func ErrMinerNotRunning(name string) *MiningError {
return &MiningError{
Code: ErrCodeMinerNotRunning,
Message: fmt.Sprintf("miner '%s' is not running", name),
Suggestion: "Start the miner first before performing this operation",
Retryable: false,
HTTPStatus: http.StatusBadRequest,
}
}
// ErrInstallFailed creates an installation failed error
func ErrInstallFailed(minerType string) *MiningError {
return &MiningError{
Code: ErrCodeInstallFailed,
Message: fmt.Sprintf("failed to install %s", minerType),
Suggestion: "Check your internet connection and try again",
Retryable: true,
HTTPStatus: http.StatusInternalServerError,
}
}
// ErrStartFailed creates a start failed error
func ErrStartFailed(name string) *MiningError {
return &MiningError{
Code: ErrCodeStartFailed,
Message: fmt.Sprintf("failed to start miner '%s'", name),
Suggestion: "Check the miner configuration and logs for details",
Retryable: true,
HTTPStatus: http.StatusInternalServerError,
}
}
// ErrStopFailed creates a stop failed error
func ErrStopFailed(name string) *MiningError {
return &MiningError{
Code: ErrCodeStopFailed,
Message: fmt.Sprintf("failed to stop miner '%s'", name),
Suggestion: "The miner process may need to be terminated manually",
Retryable: true,
HTTPStatus: http.StatusInternalServerError,
}
}
// ErrInvalidConfig creates an invalid configuration error
func ErrInvalidConfig(reason string) *MiningError {
return &MiningError{
Code: ErrCodeInvalidConfig,
Message: fmt.Sprintf("invalid configuration: %s", reason),
Suggestion: "Review the configuration and ensure all required fields are provided",
Retryable: false,
HTTPStatus: http.StatusBadRequest,
}
}
// ErrUnsupportedMiner creates an unsupported miner type error
func ErrUnsupportedMiner(minerType string) *MiningError {
return &MiningError{
Code: ErrCodeUnsupportedMiner,
Message: fmt.Sprintf("unsupported miner type: %s", minerType),
Suggestion: "Use one of the supported miner types: xmrig, tt-miner",
Retryable: false,
HTTPStatus: http.StatusBadRequest,
}
}
// ErrConnectionFailed creates a connection failed error
func ErrConnectionFailed(target string) *MiningError {
return &MiningError{
Code: ErrCodeConnectionFailed,
Message: fmt.Sprintf("failed to connect to %s", target),
Suggestion: "Check network connectivity and try again",
Retryable: true,
HTTPStatus: http.StatusServiceUnavailable,
}
}
// ErrTimeout creates a timeout error
func ErrTimeout(operation string) *MiningError {
return &MiningError{
Code: ErrCodeTimeout,
Message: fmt.Sprintf("operation timed out: %s", operation),
Suggestion: "The operation is taking longer than expected, try again later",
Retryable: true,
HTTPStatus: http.StatusGatewayTimeout,
}
}
// ErrDatabaseError creates a database error
func ErrDatabaseError(operation string) *MiningError {
return &MiningError{
Code: ErrCodeDatabaseError,
Message: fmt.Sprintf("database error during %s", operation),
Suggestion: "This may be a temporary issue, try again",
Retryable: true,
HTTPStatus: http.StatusInternalServerError,
}
}
// ErrProfileNotFound creates a profile not found error
func ErrProfileNotFound(id string) *MiningError {
return &MiningError{
Code: ErrCodeProfileNotFound,
Message: fmt.Sprintf("profile '%s' not found", id),
Suggestion: "Check that the profile ID is correct",
Retryable: false,
HTTPStatus: http.StatusNotFound,
}
}
// ErrProfileExists creates a profile already exists error
func ErrProfileExists(name string) *MiningError {
return &MiningError{
Code: ErrCodeProfileExists,
Message: fmt.Sprintf("profile '%s' already exists", name),
Suggestion: "Use a different name or update the existing profile",
Retryable: false,
HTTPStatus: http.StatusConflict,
}
}
// ErrInternal creates a generic internal error
func ErrInternal(message string) *MiningError {
return &MiningError{
Code: ErrCodeInternalError,
Message: message,
Suggestion: "Please report this issue if it persists",
Retryable: true,
HTTPStatus: http.StatusInternalServerError,
}
}