feat(plugins): add TimeNow function to SchedulerService (#4337)
* feat: add TimeNow function to SchedulerService plugin Added new TimeNow RPC method to the SchedulerService host service that returns the current time in two formats: RFC3339Nano string and Unix milliseconds int64. This provides plugins with a standardized way to get current time information from the host system. The implementation includes: - TimeNowRequest/TimeNowResponse protobuf message definitions - Go host service implementation using time.Now() - Complete test coverage with format validation - Generated WASM interface code for plugin communication * feat: add LocalTimeZone field to TimeNow response Added LocalTimeZone field to TimeNowResponse message in the SchedulerService plugin host service. This field contains the server's local timezone name (e.g., 'America/New_York', 'UTC') providing plugins with timezone context alongside the existing RFC3339Nano and Unix milliseconds timestamps. The implementation includes: - New local_time_zone protobuf field definition - Go implementation using time.Now().Location().String() - Updated test coverage with timezone validation - Generated protobuf serialization/deserialization code * docs: update plugin README with TimeNow function documentation Updated the plugins README.md to document the new TimeNow function in the SchedulerService. The documentation includes detailed descriptions of the three return formats (RFC3339Nano, UnixMilli, LocalTimeZone), practical use cases, and a comprehensive Go code example showing how plugins can access current time information for logging, calculations, and timezone-aware operations. * docs: remove wrong comment from InitRequest Signed-off-by: Deluan <deluan@navidrome.org> * fix: add missing TimeNow method to namedSchedulerService Added TimeNow method implementation to namedSchedulerService struct to satisfy the scheduler.SchedulerService interface contract. This method was recently added to the interface but the namedSchedulerService wrapper was not updated, causing compilation failures in plugin tests. The implementation is a simple pass-through to the underlying scheduler service since TimeNow doesn't require any special handling for named callbacks. --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
+41
-2
@@ -196,7 +196,7 @@ See the [cache.proto](host/cache/cache.proto) file for the full API definition.
|
||||
|
||||
#### SchedulerService
|
||||
|
||||
The SchedulerService provides a unified interface for scheduling both one-time and recurring tasks. See the [scheduler.proto](host/scheduler/scheduler.proto) file for the full API.
|
||||
The SchedulerService provides a unified interface for scheduling both one-time and recurring tasks, as well as accessing current time information. See the [scheduler.proto](host/scheduler/scheduler.proto) file for the full API.
|
||||
|
||||
```protobuf
|
||||
service SchedulerService {
|
||||
@@ -208,11 +208,50 @@ service SchedulerService {
|
||||
|
||||
// Cancel any scheduled job
|
||||
rpc CancelSchedule(CancelRequest) returns (CancelResponse);
|
||||
|
||||
// Get current time in multiple formats
|
||||
rpc TimeNow(TimeNowRequest) returns (TimeNowResponse);
|
||||
}
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- **One-time scheduling**: Schedule a callback to be executed once after a specified delay.
|
||||
- **Recurring scheduling**: Schedule a callback to be executed repeatedly according to a cron expression.
|
||||
- **Current time access**: Get the current time in standardized formats for time-based operations.
|
||||
|
||||
**TimeNow Function:**
|
||||
|
||||
The `TimeNow` function returns the current time in three formats:
|
||||
|
||||
```protobuf
|
||||
message TimeNowResponse {
|
||||
string rfc3339_nano = 1; // RFC3339 format with nanosecond precision
|
||||
int64 unix_milli = 2; // Unix timestamp in milliseconds
|
||||
string local_time_zone = 3; // Local timezone name (e.g., "UTC", "America/New_York")
|
||||
}
|
||||
```
|
||||
|
||||
This allows plugins to:
|
||||
|
||||
- Get high-precision timestamps for logging and event correlation
|
||||
- Perform time-based calculations using Unix timestamps
|
||||
- Handle timezone-aware operations by knowing the server's local timezone
|
||||
|
||||
Example usage:
|
||||
|
||||
```go
|
||||
// Get current time information
|
||||
timeResp, err := scheduler.TimeNow(ctx, &scheduler.TimeNowRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use the different time formats
|
||||
timestamp := timeResp.Rfc3339Nano // "2024-01-15T10:30:45.123456789Z"
|
||||
unixMs := timeResp.UnixMilli // 1705312245123
|
||||
timezone := timeResp.LocalTimeZone // "UTC"
|
||||
```
|
||||
|
||||
Plugins using this service must implement the `SchedulerCallback` interface:
|
||||
|
||||
@@ -433,7 +472,7 @@ If no permissions are needed, use an empty permissions object: `"permissions": {
|
||||
The following permission keys correspond to host services:
|
||||
|
||||
| Permission | Host Service | Description | Required Fields |
|
||||
|---------------|--------------------|----------------------------------------------------|-------------------------------------------------------|
|
||||
| ------------- | ------------------ | -------------------------------------------------- | ----------------------------------------------------- |
|
||||
| `http` | HttpService | Make HTTP requests (GET, POST, PUT, DELETE, etc..) | `reason`, `allowedUrls` |
|
||||
| `websocket` | WebSocketService | Connect to and communicate via WebSockets | `reason`, `allowedUrls` |
|
||||
| `cache` | CacheService | Store and retrieve cached data with TTL | `reason` |
|
||||
|
||||
@@ -903,7 +903,6 @@ type InitRequest struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Empty for now
|
||||
Config map[string]string `protobuf:"bytes,1,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // Configuration specific to this plugin
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,6 @@ service LifecycleManagement {
|
||||
}
|
||||
|
||||
message InitRequest {
|
||||
// Empty for now
|
||||
map<string, string> config = 1; // Configuration specific to this plugin
|
||||
}
|
||||
|
||||
|
||||
@@ -88,3 +88,7 @@ func (n *namedSchedulerService) CancelSchedule(ctx context.Context, request *sch
|
||||
request.ScheduleId = key
|
||||
return n.svc.CancelSchedule(ctx, request)
|
||||
}
|
||||
|
||||
func (n *namedSchedulerService) TimeNow(ctx context.Context, request *scheduler.TimeNowRequest) (*scheduler.TimeNowResponse, error) {
|
||||
return n.svc.TimeNow(ctx, request)
|
||||
}
|
||||
|
||||
@@ -154,6 +154,51 @@ func (x *CancelResponse) GetError() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type TimeNowRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
}
|
||||
|
||||
func (x *TimeNowRequest) ProtoReflect() protoreflect.Message {
|
||||
panic(`not implemented`)
|
||||
}
|
||||
|
||||
type TimeNowResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Rfc3339Nano string `protobuf:"bytes,1,opt,name=rfc3339_nano,json=rfc3339Nano,proto3" json:"rfc3339_nano,omitempty"` // Current time in RFC3339Nano format
|
||||
UnixMilli int64 `protobuf:"varint,2,opt,name=unix_milli,json=unixMilli,proto3" json:"unix_milli,omitempty"` // Current time as Unix milliseconds timestamp
|
||||
LocalTimeZone string `protobuf:"bytes,3,opt,name=local_time_zone,json=localTimeZone,proto3" json:"local_time_zone,omitempty"` // Local timezone name (e.g., "America/New_York", "UTC")
|
||||
}
|
||||
|
||||
func (x *TimeNowResponse) ProtoReflect() protoreflect.Message {
|
||||
panic(`not implemented`)
|
||||
}
|
||||
|
||||
func (x *TimeNowResponse) GetRfc3339Nano() string {
|
||||
if x != nil {
|
||||
return x.Rfc3339Nano
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TimeNowResponse) GetUnixMilli() int64 {
|
||||
if x != nil {
|
||||
return x.UnixMilli
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TimeNowResponse) GetLocalTimeZone() string {
|
||||
if x != nil {
|
||||
return x.LocalTimeZone
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// go:plugin type=host version=1
|
||||
type SchedulerService interface {
|
||||
// One-time event scheduling
|
||||
@@ -162,4 +207,6 @@ type SchedulerService interface {
|
||||
ScheduleRecurring(context.Context, *ScheduleRecurringRequest) (*ScheduleResponse, error)
|
||||
// Cancel any scheduled job
|
||||
CancelSchedule(context.Context, *CancelRequest) (*CancelResponse, error)
|
||||
// Get current time in multiple formats
|
||||
TimeNow(context.Context, *TimeNowRequest) (*TimeNowResponse, error)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ service SchedulerService {
|
||||
|
||||
// Cancel any scheduled job
|
||||
rpc CancelSchedule(CancelRequest) returns (CancelResponse);
|
||||
|
||||
// Get current time in multiple formats
|
||||
rpc TimeNow(TimeNowRequest) returns (TimeNowResponse);
|
||||
}
|
||||
|
||||
message ScheduleOneTimeRequest {
|
||||
@@ -39,4 +42,14 @@ message CancelRequest {
|
||||
message CancelResponse {
|
||||
bool success = 1; // Whether cancellation was successful
|
||||
string error = 2; // Error message if cancellation failed
|
||||
}
|
||||
|
||||
message TimeNowRequest {
|
||||
// Empty request - no parameters needed
|
||||
}
|
||||
|
||||
message TimeNowResponse {
|
||||
string rfc3339_nano = 1; // Current time in RFC3339Nano format
|
||||
int64 unix_milli = 2; // Current time as Unix milliseconds timestamp
|
||||
string local_time_zone = 3; // Local timezone name (e.g., "America/New_York", "UTC")
|
||||
}
|
||||
@@ -44,6 +44,11 @@ func Instantiate(ctx context.Context, r wazero.Runtime, hostFunctions SchedulerS
|
||||
WithParameterNames("offset", "size").
|
||||
Export("cancel_schedule")
|
||||
|
||||
envBuilder.NewFunctionBuilder().
|
||||
WithGoModuleFunction(api.GoModuleFunc(h._TimeNow), []api.ValueType{i32, i32}, []api.ValueType{i64}).
|
||||
WithParameterNames("offset", "size").
|
||||
Export("time_now")
|
||||
|
||||
_, err := envBuilder.Instantiate(ctx)
|
||||
return err
|
||||
}
|
||||
@@ -134,3 +139,32 @@ func (h _schedulerService) _CancelSchedule(ctx context.Context, m api.Module, st
|
||||
ptrLen := (ptr << uint64(32)) | uint64(len(buf))
|
||||
stack[0] = ptrLen
|
||||
}
|
||||
|
||||
// Get current time in multiple formats
|
||||
|
||||
func (h _schedulerService) _TimeNow(ctx context.Context, m api.Module, stack []uint64) {
|
||||
offset, size := uint32(stack[0]), uint32(stack[1])
|
||||
buf, err := wasm.ReadMemory(m.Memory(), offset, size)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
request := new(TimeNowRequest)
|
||||
err = request.UnmarshalVT(buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
resp, err := h.TimeNow(ctx, request)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
buf, err = resp.MarshalVT()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ptr, err := wasm.WriteMemory(ctx, m, buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ptrLen := (ptr << uint64(32)) | uint64(len(buf))
|
||||
stack[0] = ptrLen
|
||||
}
|
||||
|
||||
@@ -88,3 +88,26 @@ func (h schedulerService) CancelSchedule(ctx context.Context, request *CancelReq
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
//go:wasmimport env time_now
|
||||
func _time_now(ptr uint32, size uint32) uint64
|
||||
|
||||
func (h schedulerService) TimeNow(ctx context.Context, request *TimeNowRequest) (*TimeNowResponse, error) {
|
||||
buf, err := request.MarshalVT()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ptr, size := wasm.ByteToPtr(buf)
|
||||
ptrSize := _time_now(ptr, size)
|
||||
wasm.Free(ptr)
|
||||
|
||||
ptr = uint32(ptrSize >> 32)
|
||||
size = uint32(ptrSize)
|
||||
buf = wasm.PtrToByte(ptr, size)
|
||||
|
||||
response := new(TimeNowResponse)
|
||||
if err = response.UnmarshalVT(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -256,6 +256,91 @@ func (m *CancelResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
|
||||
return len(dAtA) - i, nil
|
||||
}
|
||||
|
||||
func (m *TimeNowRequest) MarshalVT() (dAtA []byte, err error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
size := m.SizeVT()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalToSizedBufferVT(dAtA[:size])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
}
|
||||
|
||||
func (m *TimeNowRequest) MarshalToVT(dAtA []byte) (int, error) {
|
||||
size := m.SizeVT()
|
||||
return m.MarshalToSizedBufferVT(dAtA[:size])
|
||||
}
|
||||
|
||||
func (m *TimeNowRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
|
||||
if m == nil {
|
||||
return 0, nil
|
||||
}
|
||||
i := len(dAtA)
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if m.unknownFields != nil {
|
||||
i -= len(m.unknownFields)
|
||||
copy(dAtA[i:], m.unknownFields)
|
||||
}
|
||||
return len(dAtA) - i, nil
|
||||
}
|
||||
|
||||
func (m *TimeNowResponse) MarshalVT() (dAtA []byte, err error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
size := m.SizeVT()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalToSizedBufferVT(dAtA[:size])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
}
|
||||
|
||||
func (m *TimeNowResponse) MarshalToVT(dAtA []byte) (int, error) {
|
||||
size := m.SizeVT()
|
||||
return m.MarshalToSizedBufferVT(dAtA[:size])
|
||||
}
|
||||
|
||||
func (m *TimeNowResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
|
||||
if m == nil {
|
||||
return 0, nil
|
||||
}
|
||||
i := len(dAtA)
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if m.unknownFields != nil {
|
||||
i -= len(m.unknownFields)
|
||||
copy(dAtA[i:], m.unknownFields)
|
||||
}
|
||||
if len(m.LocalTimeZone) > 0 {
|
||||
i -= len(m.LocalTimeZone)
|
||||
copy(dAtA[i:], m.LocalTimeZone)
|
||||
i = encodeVarint(dAtA, i, uint64(len(m.LocalTimeZone)))
|
||||
i--
|
||||
dAtA[i] = 0x1a
|
||||
}
|
||||
if m.UnixMilli != 0 {
|
||||
i = encodeVarint(dAtA, i, uint64(m.UnixMilli))
|
||||
i--
|
||||
dAtA[i] = 0x10
|
||||
}
|
||||
if len(m.Rfc3339Nano) > 0 {
|
||||
i -= len(m.Rfc3339Nano)
|
||||
copy(dAtA[i:], m.Rfc3339Nano)
|
||||
i = encodeVarint(dAtA, i, uint64(len(m.Rfc3339Nano)))
|
||||
i--
|
||||
dAtA[i] = 0xa
|
||||
}
|
||||
return len(dAtA) - i, nil
|
||||
}
|
||||
|
||||
func encodeVarint(dAtA []byte, offset int, v uint64) int {
|
||||
offset -= sov(v)
|
||||
base := offset
|
||||
@@ -355,6 +440,37 @@ func (m *CancelResponse) SizeVT() (n int) {
|
||||
return n
|
||||
}
|
||||
|
||||
func (m *TimeNowRequest) SizeVT() (n int) {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
var l int
|
||||
_ = l
|
||||
n += len(m.unknownFields)
|
||||
return n
|
||||
}
|
||||
|
||||
func (m *TimeNowResponse) SizeVT() (n int) {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
var l int
|
||||
_ = l
|
||||
l = len(m.Rfc3339Nano)
|
||||
if l > 0 {
|
||||
n += 1 + l + sov(uint64(l))
|
||||
}
|
||||
if m.UnixMilli != 0 {
|
||||
n += 1 + sov(uint64(m.UnixMilli))
|
||||
}
|
||||
l = len(m.LocalTimeZone)
|
||||
if l > 0 {
|
||||
n += 1 + l + sov(uint64(l))
|
||||
}
|
||||
n += len(m.unknownFields)
|
||||
return n
|
||||
}
|
||||
|
||||
func sov(x uint64) (n int) {
|
||||
return (bits.Len64(x|1) + 6) / 7
|
||||
}
|
||||
@@ -915,6 +1031,191 @@ func (m *CancelResponse) UnmarshalVT(dAtA []byte) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *TimeNowRequest) UnmarshalVT(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflow
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
fieldNum := int32(wire >> 3)
|
||||
wireType := int(wire & 0x7)
|
||||
if wireType == 4 {
|
||||
return fmt.Errorf("proto: TimeNowRequest: wiretype end group for non-group")
|
||||
}
|
||||
if fieldNum <= 0 {
|
||||
return fmt.Errorf("proto: TimeNowRequest: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||
}
|
||||
switch fieldNum {
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skip(dAtA[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
||||
return ErrInvalidLength
|
||||
}
|
||||
if (iNdEx + skippy) > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
|
||||
iNdEx += skippy
|
||||
}
|
||||
}
|
||||
|
||||
if iNdEx > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *TimeNowResponse) UnmarshalVT(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflow
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
fieldNum := int32(wire >> 3)
|
||||
wireType := int(wire & 0x7)
|
||||
if wireType == 4 {
|
||||
return fmt.Errorf("proto: TimeNowResponse: wiretype end group for non-group")
|
||||
}
|
||||
if fieldNum <= 0 {
|
||||
return fmt.Errorf("proto: TimeNowResponse: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||
}
|
||||
switch fieldNum {
|
||||
case 1:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Rfc3339Nano", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflow
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLength
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLength
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Rfc3339Nano = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
case 2:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field UnixMilli", wireType)
|
||||
}
|
||||
m.UnixMilli = 0
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflow
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
m.UnixMilli |= int64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
case 3:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field LocalTimeZone", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflow
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLength
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLength
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.LocalTimeZone = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skip(dAtA[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
||||
return ErrInvalidLength
|
||||
}
|
||||
if (iNdEx + skippy) > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
|
||||
iNdEx += skippy
|
||||
}
|
||||
}
|
||||
|
||||
if iNdEx > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func skip(dAtA []byte) (n int, err error) {
|
||||
l := len(dAtA)
|
||||
|
||||
@@ -45,6 +45,10 @@ func (s SchedulerHostFunctions) CancelSchedule(ctx context.Context, req *schedul
|
||||
return s.ss.cancelSchedule(ctx, s.pluginID, req)
|
||||
}
|
||||
|
||||
func (s SchedulerHostFunctions) TimeNow(ctx context.Context, req *scheduler.TimeNowRequest) (*scheduler.TimeNowResponse, error) {
|
||||
return s.ss.timeNow(ctx, req)
|
||||
}
|
||||
|
||||
type schedulerService struct {
|
||||
// Map of schedule IDs to their callback info
|
||||
schedules map[string]*ScheduledCallback
|
||||
@@ -260,6 +264,17 @@ func (s *schedulerService) cancelSchedule(_ context.Context, pluginID string, re
|
||||
}, nil
|
||||
}
|
||||
|
||||
// timeNow returns the current time in multiple formats
|
||||
func (s *schedulerService) timeNow(_ context.Context, req *scheduler.TimeNowRequest) (*scheduler.TimeNowResponse, error) {
|
||||
now := time.Now()
|
||||
|
||||
return &scheduler.TimeNowResponse{
|
||||
Rfc3339Nano: now.Format(time.RFC3339Nano),
|
||||
UnixMilli: now.UnixMilli(),
|
||||
LocalTimeZone: now.Location().String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// runOneTimeSchedule handles the one-time schedule execution and callback
|
||||
func (s *schedulerService) runOneTimeSchedule(ctx context.Context, internalScheduleId string, delay time.Duration) {
|
||||
tmr := time.NewTimer(delay)
|
||||
|
||||
@@ -2,6 +2,7 @@ package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/core/metrics"
|
||||
"github.com/navidrome/navidrome/plugins/host/scheduler"
|
||||
@@ -164,4 +165,28 @@ var _ = Describe("SchedulerService", func() {
|
||||
Expect(ss.scheduleCount()).To(Equal(beforeCount), "Job count should remain the same after replacement")
|
||||
})
|
||||
})
|
||||
|
||||
Describe("TimeNow", func() {
|
||||
It("returns current time in RFC3339Nano, Unix milliseconds, and local timezone", func() {
|
||||
now := time.Now()
|
||||
req := &scheduler.TimeNowRequest{}
|
||||
resp, err := ss.timeNow(context.Background(), req)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(resp.UnixMilli).To(BeNumerically(">=", now.UnixMilli()))
|
||||
Expect(resp.LocalTimeZone).ToNot(BeEmpty())
|
||||
|
||||
// Validate RFC3339Nano format can be parsed
|
||||
parsedTime, parseErr := time.Parse(time.RFC3339Nano, resp.Rfc3339Nano)
|
||||
Expect(parseErr).ToNot(HaveOccurred())
|
||||
|
||||
// Validate that Unix milliseconds is reasonably close to the RFC3339Nano time
|
||||
expectedMillis := parsedTime.UnixMilli()
|
||||
Expect(resp.UnixMilli).To(Equal(expectedMillis))
|
||||
|
||||
// Validate local timezone matches the current system timezone
|
||||
expectedTimezone := now.Location().String()
|
||||
Expect(resp.LocalTimeZone).To(Equal(expectedTimezone))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user