# GPU Sync Integration Example This document shows how to integrate GPU instance syncing into the existing sync service. ## Modified Sync Flow ```typescript // src/services/sync.ts import { GpuInstanceInput, GpuPricingInput } from '../types'; // In LinodeSync or similar sync class: async syncLinode(providerId: number): Promise { const startTime = Date.now(); try { // 1. Fetch all instance types const allInstances = await this.connector.fetchInstanceTypes(); // 2. Separate GPU and regular instances const gpuInstances = allInstances.filter(inst => inst.gpus > 0); const regularInstances = allInstances.filter(inst => inst.gpus === 0); // 3. Normalize regular instances (existing logic) const normalizedRegular = regularInstances.map(inst => this.connector.normalizeInstance(inst, providerId) ); // 4. Normalize GPU instances (new logic) const normalizedGpu = gpuInstances.map(inst => this.connector.normalizeGpuInstance(inst, providerId) ); // 5. Store regular instances (existing) const regularCount = await this.repos.instances.upsertMany( providerId, normalizedRegular ); // 6. Store GPU instances (new) const gpuCount = await this.repos.gpuInstances.upsertMany( providerId, normalizedGpu ); // 7. Handle pricing // GPU pricing is embedded in metadata, so we need to extract it await this.syncGpuPricing(providerId, normalizedGpu); return { provider: 'linode', success: true, regions_synced: 0, // regions handled separately instances_synced: regularCount, gpu_instances_synced: gpuCount, // new field pricing_synced: 0, // pricing handled separately duration_ms: Date.now() - startTime }; } catch (error) { this.logger.error('Linode sync failed', { error }); return { provider: 'linode', success: false, regions_synced: 0, instances_synced: 0, gpu_instances_synced: 0, pricing_synced: 0, duration_ms: Date.now() - startTime, error: error instanceof Error ? error.message : 'Unknown error' }; } } // New method to sync GPU pricing private async syncGpuPricing( providerId: number, gpuInstances: GpuInstanceInput[] ): Promise { const regions = await this.repos.regions.findByProvider(providerId); const pricingData: GpuPricingInput[] = []; // For each GPU instance for (const gpuInstance of gpuInstances) { // Parse metadata to get pricing const metadata = gpuInstance.metadata ? JSON.parse(gpuInstance.metadata) : null; if (!metadata || !metadata.hourly_price || !metadata.monthly_price) { this.logger.warn('GPU instance missing pricing in metadata', { instance_id: gpuInstance.instance_id }); continue; } // Get DB ID for this GPU instance const dbInstance = await this.repos.gpuInstances.findByInstanceId( providerId, gpuInstance.instance_id ); if (!dbInstance) { this.logger.warn('GPU instance not found in DB after upsert', { instance_id: gpuInstance.instance_id }); continue; } // Create pricing records for all regions // (Linode prices are consistent across regions) for (const region of regions) { pricingData.push({ gpu_instance_id: dbInstance.id, region_id: region.id, hourly_price: metadata.hourly_price, monthly_price: metadata.monthly_price, currency: 'USD', available: 1 }); } } // Batch insert pricing return await this.repos.gpuPricing.upsertMany(pricingData); } ``` ## Alternative: Separate GPU Sync Method If you prefer to keep GPU sync completely separate: ```typescript // src/services/sync.ts /** * Sync GPU instances separately from regular instances */ async syncGpuInstances(providerId: number): Promise { this.logger.info('Syncing GPU instances', { providerId }); try { // Fetch all instances const allInstances = await this.connector.fetchInstanceTypes(); // Filter GPU only const gpuInstances = allInstances.filter(inst => inst.gpus > 0); this.logger.info('Found GPU instances', { count: gpuInstances.length }); if (gpuInstances.length === 0) { return 0; } // Normalize const normalized = gpuInstances.map(inst => this.connector.normalizeGpuInstance(inst, providerId) ); // Store const count = await this.repos.gpuInstances.upsertMany( providerId, normalized ); // Sync pricing await this.syncGpuPricing(providerId, normalized); this.logger.info('GPU instances synced', { count }); return count; } catch (error) { this.logger.error('GPU sync failed', { providerId, error: error instanceof Error ? error.message : 'Unknown error' }); throw error; } } ``` ## Usage in Scheduled Sync ```typescript // In your cron handler or sync orchestrator: export default { async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) { const repos = new RepositoryFactory(env.DB); // Get Linode provider const provider = await repos.providers.findByName('linode'); if (!provider) { throw new Error('Linode provider not found'); } // Initialize connector const vault = new VaultClient(env.VAULT_URL, env.VAULT_TOKEN); const connector = new LinodeConnector(vault, env); await connector.initialize(); // Sync regular instances await syncService.syncLinode(provider.id); // Sync GPU instances (if separate) await syncService.syncGpuInstances(provider.id); // Or use combined sync that handles both } }; ``` ## Query Examples ### Find Available GPU Types ```typescript const gpuTypes = await repos.gpuInstances.getAvailableGpuTypes(); // Returns: ['NVIDIA A100', 'NVIDIA RTX6000', 'NVIDIA V100'] ``` ### Search for Specific GPU Configuration ```typescript const instances = await repos.gpuInstances.search({ providerId: linodeProviderId, minGpuCount: 2, gpuType: 'NVIDIA A100', minMemoryMb: 65536 // 64GB RAM minimum }); ``` ### Get GPU Pricing for Region ```typescript const gpuInstance = await repos.gpuInstances.findByInstanceId( providerId, 'g1-gpu-rtx6000-1' ); if (gpuInstance) { const pricing = await repos.gpuPricing.findByGpuInstance(gpuInstance.id); console.log('Available in regions:', pricing.length); } ``` ### Find Cheapest GPU Option ```typescript const cheapestGpu = await repos.gpuPricing.searchByPriceRange( undefined, // no min 2.0 // max $2/hour ); // Get instance details for (const price of cheapestGpu) { const instance = await repos.gpuInstances.findById(price.gpu_instance_id); console.log(`${instance.instance_name}: $${price.hourly_price}/hr`); } ``` ## API Endpoint Example (Optional) ```typescript // src/routes/gpu.ts import { Router } from 'itty-router'; import { Env } from '../types'; import { RepositoryFactory } from '../repositories'; const router = Router({ base: '/gpu' }); /** * GET /gpu/instances * Query GPU instances with filters */ router.get('/instances', async (request, env: Env) => { const url = new URL(request.url); const gpuType = url.searchParams.get('gpu_type'); const minGpuCount = url.searchParams.get('min_gpu_count'); const providerId = url.searchParams.get('provider_id'); const repos = new RepositoryFactory(env.DB); const instances = await repos.gpuInstances.search({ providerId: providerId ? parseInt(providerId) : undefined, gpuType: gpuType || undefined, minGpuCount: minGpuCount ? parseInt(minGpuCount) : undefined }); return new Response(JSON.stringify({ data: instances }), { headers: { 'Content-Type': 'application/json' } }); }); /** * GET /gpu/types * Get available GPU types */ router.get('/types', async (request, env: Env) => { const repos = new RepositoryFactory(env.DB); const types = await repos.gpuInstances.getAvailableGpuTypes(); return new Response(JSON.stringify({ data: types }), { headers: { 'Content-Type': 'application/json' } }); }); export default router; ``` ## Testing ### Unit Test Example ```typescript // src/repositories/gpu-instances.test.ts import { describe, it, expect, beforeEach } from 'vitest'; import { GpuInstancesRepository } from './gpu-instances'; import { createMockD1 } from '../test-utils/mock-d1'; describe('GpuInstancesRepository', () => { let repo: GpuInstancesRepository; let mockDb: D1Database; beforeEach(() => { mockDb = createMockD1(); repo = new GpuInstancesRepository(mockDb); }); it('should find GPU instances by type', async () => { const instances = await repo.findByGpuType('NVIDIA A100'); expect(instances).toBeDefined(); }); it('should filter by minimum GPU count', async () => { const instances = await repo.search({ minGpuCount: 2 }); expect(instances.every(inst => inst.gpu_count >= 2)).toBe(true); }); }); ``` ## Migration Notes ### Running the Migration ```bash # Local npm run db:init # Remote (Production) npm run db:init:remote ``` ### Data Validation Query ```sql -- Check GPU instances were separated correctly SELECT 'Regular' as type, COUNT(*) as count, SUM(CASE WHEN gpu_count > 0 THEN 1 ELSE 0 END) as gpu_count_error FROM instance_types UNION ALL SELECT 'GPU' as type, COUNT(*) as count, SUM(CASE WHEN gpu_count = 0 THEN 1 ELSE 0 END) as zero_gpu_error FROM gpu_instances; -- Should show: -- Regular: count > 0, gpu_count_error = 0 -- GPU: count > 0, zero_gpu_error = 0 ``` ### Backfill Existing GPU Data (If Needed) ```sql -- If you already have GPU instances in instance_types table, -- you can migrate them: INSERT INTO gpu_instances ( provider_id, instance_id, instance_name, vcpu, memory_mb, storage_gb, transfer_tb, network_speed_gbps, gpu_count, gpu_type, gpu_memory_gb, metadata, created_at, updated_at ) SELECT provider_id, instance_id, instance_name, vcpu, memory_mb, storage_gb, transfer_tb, network_speed_gbps, gpu_count, gpu_type, NULL as gpu_memory_gb, metadata, created_at, updated_at FROM instance_types WHERE gpu_count > 0; -- Then remove from regular instances DELETE FROM instance_types WHERE gpu_count > 0; ```