Rate Limiter: - Extract rate limiting to pkg/mining/ratelimiter.go with proper lifecycle - Add Stop() method to gracefully shutdown cleanup goroutine - Add RateLimiter.Middleware() for Gin integration - Add ClientCount() for monitoring - Fix goroutine leak in previous inline implementation Custom Errors: - Add pkg/mining/errors.go with MiningError type - Define error codes: MINER_NOT_FOUND, INSTALL_FAILED, TIMEOUT, etc. - Add predefined error constructors (ErrMinerNotFound, ErrStartFailed, etc.) - Support error chaining with WithCause, WithDetails, WithSuggestion - Include HTTP status codes and retry policies Service: - Add Service.Stop() method for graceful cleanup - Update CLI commands to use context.Background() for Manager methods Tests: - Add comprehensive tests for RateLimiter (token bucket, multi-IP, refill) - Add comprehensive tests for MiningError (codes, status, retryable) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
247 lines
7.4 KiB
Go
247 lines
7.4 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"
|
|
)
|
|
|
|
// 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,
|
|
}
|
|
}
|